diff --git a/.gitignore b/.gitignore index db7d4292..e54e1552 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ frontend/.pnp.js # testing frontend/coverage +postgres-data/* # production frontend/build diff --git a/backend/core/admin.py b/backend/core/admin.py index ea57bef1..ccd5af55 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -7,13 +7,13 @@ @admin.register(Dataset) -class DatasetAdmin(geoadmin.GeoModelAdmin): +class DatasetAdmin(geoadmin.OSMGeoAdmin): list_display = ["name", "created_by"] @admin.register(Model) -class ModelAdmin(geoadmin.GeoModelAdmin): - list_display = ["get_dataset_id", "name", "status", "created_at"] +class ModelAdmin(geoadmin.OSMGeoAdmin): + list_display = ["get_dataset_id", "name", "status", "created_at", "created_by"] def get_dataset_id(self, obj): return obj.dataset.id @@ -22,7 +22,7 @@ def get_dataset_id(self, obj): @admin.register(Training) -class TrainingAdmin(geoadmin.GeoModelAdmin): +class TrainingAdmin(geoadmin.OSMGeoAdmin): list_display = [ "get_model_id", "description", @@ -37,3 +37,13 @@ def get_model_id(self, obj): return obj.model.id get_model_id.short_description = "Model" + + +@admin.register(FeedbackAOI) +class FeedbackAOIAdmin(geoadmin.OSMGeoAdmin): + list_display = ["training", "user"] + + +@admin.register(Feedback) +class FeedbackAdmin(geoadmin.OSMGeoAdmin): + list_display = ["feedback_type", "training", "user", "created_at"] diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 2381ce3e..a1e6fa43 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -1,3 +1,4 @@ +from django.conf import settings from login.models import OsmUser from rest_framework import serializers from rest_framework_gis.serializers import ( @@ -102,16 +103,14 @@ def to_representation(self, instance): return ret -class LabelSerializer( - GeoFeatureModelSerializer -): # serializers are used to translate models objects to api +class LabelSerializer(GeoFeatureModelSerializer): class Meta: model = Label - geo_field = "geom" # this will be used as geometry in order to create geojson api , geofeatureserializer will let you create api in geojson + geo_field = "geom" # auto_bbox = True - fields = "__all__" # defining all the fields to be included in curd for now , we can restrict few if we want + fields = "__all__" - read_only_fields = ("created_at", "osm_id") + # read_only_fields = ("created_at", "osm_id") class FeedbackLabelSerializer(GeoFeatureModelSerializer): @@ -119,15 +118,21 @@ class Meta: model = FeedbackLabel geo_field = "geom" fields = "__all__" - read_only_fields = ("created_at", "osm_id") + # read_only_fields = ("created_at", "osm_id") -class LabelFileSerializer( - GeoFeatureModelSerializer -): # serializers are used to translate models objects to api +class LabelFileSerializer(GeoFeatureModelSerializer): class Meta: model = Label - geo_field = "geom" # this will be used as geometry in order to create geojson api , geofeatureserializer will let you create api in geojson + geo_field = "geom" + # auto_bbox = True + fields = ("osm_id",) + + +class FeedbackLabelFileSerializer(GeoFeatureModelSerializer): + class Meta: + model = FeedbackLabel + geo_field = "geom" # auto_bbox = True fields = ("osm_id",) @@ -160,7 +165,55 @@ class FeedbackParamSerializer(serializers.Serializer): training_id = serializers.IntegerField(required=True) epochs = serializers.IntegerField(required=False) batch_size = serializers.IntegerField(required=False) - freeze_layers = serializers.BooleanField(required=False) + zoom_level = serializers.ListField(child=serializers.IntegerField(), required=False) + + def validate_training_id(self, value): + try: + Training.objects.get(id=value) + except Training.DoesNotExist: + raise serializers.ValidationError("Training doesn't exist") + + return value + + def validate(self, data): + training_id = data.get("training_id") + + try: + fd_aois = FeedbackAOI.objects.filter(training=training_id) + except FeedbackAOI.DoesNotExist: + raise serializers.ValidationError( + "No feedback AOI is associated with Training" + ) + + if fd_aois.filter( + label_status=FeedbackAOI.DownloadStatus.NOT_DOWNLOADED + ).exists(): + raise serializers.ValidationError( + "Not all AOIs have their labels downloaded" + ) + + if "epochs" in data and ( + data["epochs"] > settings.EPOCHS_LIMIT or data["epochs"] <= 0 + ): + raise serializers.ValidationError( + f"Epochs should be 1 - {settings.EPOCHS_LIMIT} on this server" + ) + + if "batch_size" in data and ( + data["batch_size"] > settings.BATCH_SIZE_LIMIT or data["batch_size"] <= 0 + ): + raise serializers.ValidationError( + f"Batch size should be 1 - {settings.BATCH_SIZE_LIMIT} on this server" + ) + + if "zoom_level" in data: + for zoom in data["zoom_level"]: + if zoom < 19 or zoom > 21: + raise serializers.ValidationError( + "Zoom level must be between 19 and 21" + ) + + return data class PredictionParamSerializer(serializers.Serializer): diff --git a/backend/core/tasks.py b/backend/core/tasks.py index 8f020b81..4cd34642 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -10,8 +10,12 @@ import ramp.utils import tensorflow as tf from celery import shared_task -from core.models import AOI, Feedback, Label, Training -from core.serializers import FeedbackFileSerializer, LabelFileSerializer +from core.models import AOI, Feedback, FeedbackAOI, FeedbackLabel, Label, Training +from core.serializers import ( + FeedbackFileSerializer, + FeedbackLabelFileSerializer, + LabelFileSerializer, +) from core.utils import bbox, download_imagery, get_start_end_download_coords from django.conf import settings from django.contrib.gis.db.models.aggregates import Extent @@ -65,34 +69,32 @@ def train_model( shutil.rmtree(training_input_image_source) os.makedirs(training_input_image_source) if feedback: - feedback_objects = Feedback.objects.filter( - training__id=feedback, - validated=True, - ) - bbox_feedback = feedback_objects.aggregate(Extent("geom"))[ - "geom__extent" - ] - bbox_geo = GEOSGeometry( - f"POLYGON(({bbox_feedback[0]} {bbox_feedback[1]},{bbox_feedback[2]} {bbox_feedback[1]},{bbox_feedback[2]} {bbox_feedback[3]},{bbox_feedback[0]} {bbox_feedback[3]},{bbox_feedback[0]} {bbox_feedback[1]}))" - ) - print(training_input_image_source) - print(bbox_feedback) - with open( - os.path.join(training_input_image_source, "labels_bbox.geojson"), - "w", - encoding="utf-8", - ) as f: - f.write(bbox_geo.geojson) + try: + aois = FeedbackAOI.objects.filter(training=feedback) + except FeedbackAOI.DoesNotExist: + raise ValueError( + f"No Feedback AOI is attached with supplied training id:{dataset_id}, Create AOI first", + ) + + else: + try: + aois = AOI.objects.filter(dataset=dataset_id) + except AOI.DoesNotExist: + raise ValueError( + f"No AOI is attached with supplied dataset id:{dataset_id}, Create AOI first", + ) + for obj in aois: + bbox_coords = bbox(obj.geom.coords[0]) for z in zoom_level: zm_level = z print( f"""Running Download process for - feedback {training_id} - dataset : {dataset_id} , zoom : {zm_level}""" + aoi : {obj.id} - dataset : {dataset_id} , zoom : {zm_level}""" ) try: tile_size = DEFAULT_TILE_SIZE # by default - bbox_coords = list(bbox_feedback) + start, end = get_start_end_download_coords( bbox_coords, zm_level, tile_size ) @@ -107,49 +109,17 @@ def train_model( except Exception as ex: raise ex - else: - try: - aois = AOI.objects.filter(dataset=dataset_id) - except AOI.DoesNotExist: - raise ValueError( - f"No AOI is attached with supplied dataset id:{dataset_id}, Create AOI first", - ) - - for obj in aois: - bbox_coords = bbox(obj.geom.coords[0]) - for z in zoom_level: - zm_level = z - print( - f"""Running Download process for - aoi : {obj.id} - dataset : {dataset_id} , zoom : {zm_level}""" - ) - try: - tile_size = DEFAULT_TILE_SIZE # by default - - start, end = get_start_end_download_coords( - bbox_coords, zm_level, tile_size - ) - # start downloading - download_imagery( - start, - end, - zm_level, - base_path=training_input_image_source, - source=source_imagery, - ) - except Exception as ex: - raise ex - ## -----------LABEL GENERATOR--------- - logging.debug("Label Generator started") + logging.info("Label Generator started") + aoi_list = [r.id for r in aois] + logging.info(aoi_list) + if feedback: - feedback_objects = Feedback.objects.filter( - training__id=feedback, - validated=True, - ) - serialized_field = FeedbackFileSerializer(feedback_objects, many=True) + label = FeedbackLabel.objects.filter(feedback_aoi__in=aoi_list) + logging.info(label) + + serialized_field = FeedbackLabelFileSerializer(label, many=True) else: - aoi_list = [r.id for r in aois] label = Label.objects.filter(aoi__in=aoi_list) serialized_field = LabelFileSerializer(label, many=True) diff --git a/backend/core/urls.py b/backend/core/urls.py index 8c395cdd..cde78096 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -11,11 +11,13 @@ FeedbackLabelViewset, FeedbackView, FeedbackViewset, + GenerateFeedbackAOIGpxView, GenerateGpxView, LabelViewSet, ModelViewSet, PredictionView, - RawdataApiView, + RawdataApiAOIView, + RawdataApiFeedbackView, TrainingViewSet, TrainingWorkspaceDownloadView, TrainingWorkspaceView, @@ -39,15 +41,22 @@ urlpatterns = [ path("", include(router.urls)), - path("label/osm/fetch//", RawdataApiView.as_view()), + path("label/osm/fetch//", RawdataApiAOIView.as_view()), + path( + "label/feedback/osm/fetch//", + RawdataApiFeedbackView.as_view(), + ), # path("download//", download_training_data), path("training/status//", run_task_status), path("training/publish//", publish_training), path("prediction/", PredictionView.as_view()), - path("apply/feedback/", FeedbackView.as_view()), + path("feedback/training/submit/", FeedbackView.as_view()), path("status/", APIStatus.as_view()), path("geojson2osm/", geojson2osmconverter, name="geojson2osmconverter"), path("aoi/gpx//", GenerateGpxView.as_view()), + path( + "feedback-aoi/gpx//", GenerateFeedbackAOIGpxView.as_view() + ), path("workspace/", TrainingWorkspaceView.as_view()), path( "workspace/download//", TrainingWorkspaceDownloadView.as_view() diff --git a/backend/core/utils.py b/backend/core/utils.py index 11c59186..8612258c 100644 --- a/backend/core/utils.py +++ b/backend/core/utils.py @@ -2,16 +2,19 @@ import json import math import os +import re +from datetime import datetime from uuid import uuid4 from xml.dom import ValidationErr from zipfile import ZipFile import requests from django.conf import settings +from gpxpy.gpx import GPX, GPXTrack, GPXTrackSegment, GPXWaypoint from tqdm import tqdm -from .models import AOI, Label -from .serializers import LabelSerializer +from .models import AOI, FeedbackAOI, FeedbackLabel, Label +from .serializers import FeedbackLabelSerializer, LabelSerializer def get_dir_size(directory): @@ -79,7 +82,6 @@ def latlng2tile(zoom, lat, lng, tile_size): def get_start_end_download_coords(bbox_coords, zm_level, tile_size): - # start point where we will start downloading the tiles start_point_lng = bbox_coords[0] # getting the starting lat lng @@ -110,9 +112,14 @@ def get_start_end_download_coords(bbox_coords, zm_level, tile_size): return start, end +import logging + + def download_image(url, base_path, source_name): response = requests.get(url) image = response.content + url = re.sub(r"\.(png|jpeg)$", "", url) + logging.info(url) url_splitted_list = url.split("/") filename = f"{base_path}/{source_name}-{url_splitted_list[-2]}-{url_splitted_list[-1]}-{url_splitted_list[-3]}.png" @@ -205,7 +212,7 @@ def request_rawdata(request_params): return response_back -def process_rawdata(file_download_url, aoi_id): +def process_rawdata(file_download_url, aoi_id, feedback=False): """This will create temp directory , Downloads file from URL provided, Unzips it Finds a geojson file , Process it and finally removes processed Geojson file and downloaded zip file from Directory""" @@ -234,7 +241,7 @@ def process_rawdata(file_download_url, aoi_id): print(f"""Geojson file{fileName} from API wrote to disk""") break geojson_file = f"""{geojson_file_path}{fileName}""" - process_geojson(geojson_file, aoi_id) + process_geojson(geojson_file, aoi_id, feedback) remove_file(file_temp_path) remove_file(geojson_file) @@ -244,28 +251,68 @@ def remove_file(path: str) -> None: os.unlink(path) -def process_feature(feature, aoi_id, dataset_id): +def gpx_generator(geom_json): + """Generates GPX for give geojson geometry + + Args: + geom_json (_type_): _description_ + + Returns: + xml: gpx + """ + + gpx = GPX() + gpx_track = GPXTrack() + gpx.tracks.append(gpx_track) + gpx_segment = GPXTrackSegment() + gpx_track.segments.append(gpx_segment) + for point in geom_json["coordinates"][0]: + # Append each point as a GPXWaypoint to the GPXTrackSegment + gpx_segment.points.append(GPXWaypoint(point[1], point[0])) + gpx.creator = "fAIr" + gpx_track.name = "Don't Edit this Boundary" + gpx_track.description = "Map inside this boundary and go back to fAIr UI" + gpx.time = datetime.now() + gpx.link = "https://github.com/hotosm/fAIr" + gpx.link_text = "AI Assisted Mapping - fAIr : HOTOSM" + return gpx.to_xml() + + +def process_feature(feature, aoi_id, foreign_key_id, feedback=False): """Multi thread process of features""" properties = feature["properties"] osm_id = properties["osm_id"] geometry = feature["geometry"] + if feedback: + if FeedbackLabel.objects.filter( + osm_id=int(osm_id), feedback_aoi__training=foreign_key_id + ).exists(): + FeedbackLabel.objects.filter( + osm_id=int(osm_id), feedback_aoi__training=foreign_key_id + ).delete() + + label = FeedbackLabelSerializer( + data={"osm_id": int(osm_id), "geom": geometry, "feedback_aoi": aoi_id} + ) - if Label.objects.filter(osm_id=int(osm_id), aoi__dataset=dataset_id).exists(): - - Label.objects.filter(osm_id=int(osm_id), aoi__dataset=dataset_id).delete() - # print(f"Existing record Found and Dropped {osm_id}") - - label = LabelSerializer( - data={"osm_id": int(osm_id), "geom": geometry, "aoi": aoi_id} - ) + else: + if Label.objects.filter( + osm_id=int(osm_id), aoi__dataset=foreign_key_id + ).exists(): + Label.objects.filter( + osm_id=int(osm_id), aoi__dataset=foreign_key_id + ).delete() + + label = LabelSerializer( + data={"osm_id": int(osm_id), "geom": geometry, "aoi": aoi_id} + ) if label.is_valid(): - label.save() # update if it exists create if not + label.save() else: raise ValidationErr(label.errors) - # print(f"Created {osm_id}") -def process_geojson(geojson_file_path, aoi_id): +def process_geojson(geojson_file_path, aoi_id, feedback=False): """Responsible for Processing Geojson file from directory , Opens the file reads the record , Checks either record present or not if not inserts into database @@ -278,7 +325,10 @@ def process_geojson(geojson_file_path, aoi_id): ValidationErr: _description_ """ print("Geojson Processing Started") - dataset_id = AOI.objects.get(id=aoi_id).dataset + if feedback: + foreign_key_id = FeedbackAOI.objects.get(id=aoi_id).training + else: + foreign_key_id = AOI.objects.get(id=aoi_id).dataset max_workers = ( (os.cpu_count() - 1) if os.cpu_count() != 1 else 1 ) # leave one cpu free always @@ -289,7 +339,9 @@ def process_geojson(geojson_file_path, aoi_id): data = json.load(f) with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [ - executor.submit(process_feature, feature, aoi_id, dataset_id) + executor.submit( + process_feature, feature, aoi_id, foreign_key_id, feedback + ) for feature in data["features"] ] for f in tqdm(futures, total=len(data["features"])): diff --git a/backend/core/views.py b/backend/core/views.py index 500df8d9..42ebb504 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -27,7 +27,6 @@ from django_filters.rest_framework import DjangoFilterBackend from drf_yasg.utils import swagger_auto_schema from geojson2osm import geojson2osm -from gpxpy.gpx import GPX, GPXTrack, GPXTrackSegment, GPXWaypoint from hot_fair_utilities import polygonize, predict, vectorize from login.authentication import OsmAuthentication from login.permissions import IsOsmAuthenticated @@ -66,6 +65,7 @@ download_imagery, get_dir_size, get_start_end_download_coords, + gpx_generator, process_rawdata, request_rawdata, ) @@ -105,6 +105,11 @@ def create(self, validated_data): existing_trainings = Training.objects.filter(model_id=model_id).exclude( status__in=["FINISHED", "FAILED"] ) + if existing_trainings.exists(): + raise ValidationError( + "Another training is already running or submitted for this model." + ) + model = get_object_or_404(Model, id=model_id) if not Label.objects.filter( aoi__in=AOI.objects.filter(dataset=model.dataset) @@ -113,11 +118,6 @@ def create(self, validated_data): "Error: No labels associated with the model, Create AOI & Labels for Dataset" ) - if existing_trainings.exists(): - raise ValidationError( - "Another training is already running or submitted for this model." - ) - epochs = validated_data["epochs"] batch_size = validated_data["batch_size"] @@ -196,7 +196,12 @@ class FeedbackLabelViewset(viewsets.ModelViewSet): queryset = FeedbackLabel.objects.all() http_method_names = ["get", "post", "patch", "delete"] serializer_class = FeedbackLabelSerializer - filterset_fields = ["feedback_aoi"] + bbox_filter_field = "geom" + filter_backends = ( + InBBoxFilter, # it will take bbox like this api/v1/label/?in_bbox=-90,29,-89,35 , + ) + bbox_filter_include_overlapping = True + filterset_fields = ["feedback_aoi", "feedback_aoi__training"] class ModelViewSet( @@ -238,12 +243,49 @@ class LabelViewSet(viewsets.ModelViewSet): filterset_fields = ["aoi", "aoi__dataset"] -class RawdataApiView(APIView): +class RawdataApiFeedbackView(APIView): + authentication_classes = [OsmAuthentication] + permission_classes = [IsOsmAuthenticated] + + def post(self, request, feedbackaoi_id, *args, **kwargs): + """Downloads available osm data as labels within given feedback aoi + + Args: + request (_type_): _description_ + feedbackaoi_id (_type_): _description_ + + Returns: + status: Success/Failed + """ + obj = get_object_or_404(FeedbackAOI, id=feedbackaoi_id) + try: + obj.label_status = 0 + obj.save() + raw_data_params = { + "geometry": json.loads(obj.geom.geojson), + "filters": {"tags": {"polygon": {"building": []}}}, + "geometryType": ["polygon"], + } + result = request_rawdata(raw_data_params) + file_download_url = result["download_url"] + process_rawdata(file_download_url, feedbackaoi_id, feedback=True) + obj.label_status = 1 + obj.label_fetched = datetime.utcnow() + obj.save() + return Response("Success", status=status.HTTP_201_CREATED) + except Exception as ex: + obj.label_status = -1 + obj.save() + # raise ex + return Response("OSM Fetch Failed", status=500) + + +class RawdataApiAOIView(APIView): authentication_classes = [OsmAuthentication] permission_classes = [IsOsmAuthenticated] def post(self, request, aoi_id, *args, **kwargs): - """Downloads available osm data as labels within given aoi + """Downloads available osm data as labels within given feedback Args: request (_type_): _description_ @@ -366,6 +408,15 @@ def run_task_status(request, run_id: str): class FeedbackView(APIView): + """Applies Associated feedback to Training Published Checkpoint + + Args: + APIView (_type_): _description_ + + Returns: + _type_: _description_ + """ + authentication_classes = [OsmAuthentication] permission_classes = [IsOsmAuthenticated] @@ -374,19 +425,22 @@ class FeedbackView(APIView): ) def post(self, request, *args, **kwargs): res_serializer = FeedbackParamSerializer(data=request.data) - if res_serializer.is_valid(raise_exception=True): + + if res_serializer.is_valid(): deserialized_data = res_serializer.data training_id = deserialized_data["training_id"] training_instance = Training.objects.get(id=training_id) + if Training.objects.filter( + model_id=training_instance.model, status__in=["RUNNING", "SUBMITTED"] + ).exists(): + raise ValidationError( + "Another training/feedback is in progress or submitted for this model." + ) - unique_zoom_levels = ( - Feedback.objects.filter(training__id=training_id, validated=True) - .values("zoom_level") - .distinct() - ) - zoom_level = [z["zoom_level"] for z in unique_zoom_levels] + zoom_level = deserialized_data.get("zoom_level", [19, 20]) epochs = deserialized_data.get("epochs", 20) batch_size = deserialized_data.get("batch_size", 8) + instance = Training.objects.create( model=training_instance.model, status="SUBMITTED", @@ -406,7 +460,7 @@ def post(self, request, *args, **kwargs): zoom_level=instance.zoom_level, source_imagery=instance.source_imagery, feedback=training_id, - freeze_layers=instance.freeze_layers, + freeze_layers=True, # True by default for feedback ) if not instance.source_imagery: instance.source_imagery = instance.model.dataset.source_imagery @@ -415,6 +469,8 @@ def post(self, request, *args, **kwargs): print(f"Saved Feedback train model request to queue with id {task.id}") return HttpResponse(status=200) + return Response(res_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + DEFAULT_TILE_SIZE = 256 @@ -578,21 +634,18 @@ def get(self, request, aoi_id: int): # Convert the polygon field to GPX format geom_json = json.loads(aoi.geom.json) # Create a new GPX object - gpx = GPX() - gpx_track = GPXTrack() - gpx.tracks.append(gpx_track) - gpx_segment = GPXTrackSegment() - gpx_track.segments.append(gpx_segment) - for point in geom_json["coordinates"][0]: - # Append each point as a GPXWaypoint to the GPXTrackSegment - gpx_segment.points.append(GPXWaypoint(point[1], point[0])) - gpx.creator = "fAIr Backend" - gpx_track.name = f"AOI of id {aoi_id} , Don't Edit this Boundary" - gpx_track.description = "This is coming from AI Assisted Mapping - fAIr : HOTOSM , Map inside this boundary and go back to fAIr UI" - gpx.time = datetime.now() - gpx.link = "https://github.com/hotosm/fAIr" - gpx.link_text = "AI Assisted Mapping - fAIr : HOTOSM" - return HttpResponse(gpx.to_xml(), content_type="application/xml") + gpx_xml = gpx_generator(geom_json) + return HttpResponse(gpx_xml, content_type="application/xml") + + +class GenerateFeedbackAOIGpxView(APIView): + def get(self, request, feedback_aoi_id: int): + aoi = get_object_or_404(FeedbackAOI, id=feedback_aoi_id) + # Convert the polygon field to GPX format + geom_json = json.loads(aoi.geom.json) + # Create a new GPX object + gpx_xml = gpx_generator(geom_json) + return HttpResponse(gpx_xml, content_type="application/xml") class TrainingWorkspaceView(APIView): diff --git a/docker-compose.yml b/docker-compose.yml index 80dfc78e..7d949280 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,8 @@ services: - POSTGRES_DB=ai - POSTGRES_USER=postgres - POSTGRES_PASSWORD=admin + volumes: + - ./postgres-data:/var/lib/postgresql/data ports: - "5434:5432" @@ -35,7 +37,7 @@ services: volumes: - ./backend:/app - ${RAMP_HOME}:/RAMP_HOME - # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE + - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: - redis - postgres @@ -45,7 +47,7 @@ services: context: ./backend dockerfile: Dockerfile container_name: worker - command: celery -A aiproject worker --loglevel=INFO + command: celery -A aiproject worker --loglevel=INFO --concurrency=1 deploy: resources: reservations: @@ -55,7 +57,7 @@ services: volumes: - ./backend:/app - ${RAMP_HOME}:/RAMP_HOME - # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE + - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: - backend-api - redis diff --git a/frontend/public/hot-marker.png b/frontend/public/hot-marker.png new file mode 100644 index 00000000..d052f4e2 Binary files /dev/null and b/frontend/public/hot-marker.png differ diff --git a/frontend/public/learn/1.png b/frontend/public/learn-resources/1.png similarity index 100% rename from frontend/public/learn/1.png rename to frontend/public/learn-resources/1.png diff --git a/frontend/public/learn/10.png b/frontend/public/learn-resources/10.png similarity index 100% rename from frontend/public/learn/10.png rename to frontend/public/learn-resources/10.png diff --git a/frontend/public/learn/11.png b/frontend/public/learn-resources/11.png similarity index 100% rename from frontend/public/learn/11.png rename to frontend/public/learn-resources/11.png diff --git a/frontend/public/learn/12.png b/frontend/public/learn-resources/12.png similarity index 100% rename from frontend/public/learn/12.png rename to frontend/public/learn-resources/12.png diff --git a/frontend/public/learn/13.png b/frontend/public/learn-resources/13.png similarity index 100% rename from frontend/public/learn/13.png rename to frontend/public/learn-resources/13.png diff --git a/frontend/public/learn/14.png b/frontend/public/learn-resources/14.png similarity index 100% rename from frontend/public/learn/14.png rename to frontend/public/learn-resources/14.png diff --git a/frontend/public/learn/15.png b/frontend/public/learn-resources/15.png similarity index 100% rename from frontend/public/learn/15.png rename to frontend/public/learn-resources/15.png diff --git a/frontend/public/learn/16.png b/frontend/public/learn-resources/16.png similarity index 100% rename from frontend/public/learn/16.png rename to frontend/public/learn-resources/16.png diff --git a/frontend/public/learn/17.png b/frontend/public/learn-resources/17.png similarity index 100% rename from frontend/public/learn/17.png rename to frontend/public/learn-resources/17.png diff --git a/frontend/public/learn/18.png b/frontend/public/learn-resources/18.png similarity index 100% rename from frontend/public/learn/18.png rename to frontend/public/learn-resources/18.png diff --git a/frontend/public/learn/19.png b/frontend/public/learn-resources/19.png similarity index 100% rename from frontend/public/learn/19.png rename to frontend/public/learn-resources/19.png diff --git a/frontend/public/learn/2.png b/frontend/public/learn-resources/2.png similarity index 100% rename from frontend/public/learn/2.png rename to frontend/public/learn-resources/2.png diff --git a/frontend/public/learn/20.png b/frontend/public/learn-resources/20.png similarity index 100% rename from frontend/public/learn/20.png rename to frontend/public/learn-resources/20.png diff --git a/frontend/public/learn/21.png b/frontend/public/learn-resources/21.png similarity index 100% rename from frontend/public/learn/21.png rename to frontend/public/learn-resources/21.png diff --git a/frontend/public/learn/22.png b/frontend/public/learn-resources/22.png similarity index 100% rename from frontend/public/learn/22.png rename to frontend/public/learn-resources/22.png diff --git a/frontend/public/learn/3.png b/frontend/public/learn-resources/3.png similarity index 100% rename from frontend/public/learn/3.png rename to frontend/public/learn-resources/3.png diff --git a/frontend/public/learn/4.png b/frontend/public/learn-resources/4.png similarity index 100% rename from frontend/public/learn/4.png rename to frontend/public/learn-resources/4.png diff --git a/frontend/public/learn/5.png b/frontend/public/learn-resources/5.png similarity index 100% rename from frontend/public/learn/5.png rename to frontend/public/learn-resources/5.png diff --git a/frontend/public/learn/6.png b/frontend/public/learn-resources/6.png similarity index 100% rename from frontend/public/learn/6.png rename to frontend/public/learn-resources/6.png diff --git a/frontend/public/learn/7.png b/frontend/public/learn-resources/7.png similarity index 100% rename from frontend/public/learn/7.png rename to frontend/public/learn-resources/7.png diff --git a/frontend/public/learn/8.png b/frontend/public/learn-resources/8.png similarity index 100% rename from frontend/public/learn/8.png rename to frontend/public/learn-resources/8.png diff --git a/frontend/public/learn/9.png b/frontend/public/learn-resources/9.png similarity index 100% rename from frontend/public/learn/9.png rename to frontend/public/learn-resources/9.png diff --git a/frontend/public/learn/osm.png b/frontend/public/learn-resources/osm.png similarity index 100% rename from frontend/public/learn/osm.png rename to frontend/public/learn-resources/osm.png diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index ed4b03e9..fa53e6fa 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -24,12 +24,13 @@ import DatasetCurrent from "./DatasetCurrent"; import FeedbackToast from "./FeedbackToast"; import FeedbackPopup from "./FeedbackPopup"; import FormGroup from "@mui/material/FormGroup"; +import LoadingButton from "@mui/lab/LoadingButton/LoadingButton"; const AIModelEditor = (props) => { let { id } = useParams(); const [error, setError] = useState(null); const [epochs, setEpochs] = useState(20); - const [zoomLevel, setZoomLevel] = useState([19, 20]); + const [zoomLevel, setZoomLevel] = useState([19, 20, 21]); const [popupOpen, setPopupOpen] = useState(false); const [sourceImagery, setSourceImagery] = React.useState(null); const [freezeLayers, setFreezeLayers] = useState(false); @@ -50,6 +51,7 @@ const AIModelEditor = (props) => { if (res.error) setError(res.error.response.statusText); else { // console.log("getmodel", res.data); + getFeedbackCount(res.data.published_training); return res.data; } } catch (e) { @@ -78,23 +80,26 @@ const AIModelEditor = (props) => { refetchInterval: 60000, } ); - const getFeedbackCount = async () => { + const [isLoadingFeedbackCount, setIsLoadingFeedbackCount] = useState(true); + const getFeedbackCount = async (trainingId) => { try { - const response = await axios.get( - `/feedback/?training=${data.published_training}` - ); + if (!trainingId) return; + setFeedbackCount(0); + const response = await axios.get(`/feedback/?training=${trainingId}`); setFeedbackData(response.data); - const feedbackCount = response.data.features.length; - setFeedbackCount(feedbackCount); + console.log(`/feedback/?training=${trainingId}`, response.data); + setFeedbackCount(response.data.features.length); + setIsLoadingFeedbackCount(false); } catch (error) { console.error("Error fetching feedback information:", error); } }; - useEffect(() => { - if (data?.published_training) { - getFeedbackCount(); - } - }, [data]); + + // useEffect(() => { + // if (data?.published_training) { + // getFeedbackCount(); + // } + // }, [data]); const handleFeedbackClick = async (trainingId) => { getFeedbackCount(); @@ -153,13 +158,13 @@ const AIModelEditor = (props) => { {data && ( - {data.published_training && ( + {/* {data.published_training && ( - )} + )} */} Model ID: {data.id} @@ -316,7 +321,7 @@ const AIModelEditor = (props) => { }} /> - + {/* Freeze Layers @@ -333,7 +338,7 @@ const AIModelEditor = (props) => { /> - + */} @@ -353,18 +358,26 @@ const AIModelEditor = (props) => { - + {data && data.published_training && ( + { + //handleFeedbackClick(data.published_training); + // add logic to view feedbacks here + navigate( + `/ai-models/${data.id}/${data.published_training}/feedback` + ); + }} + disabled={feedbackCount <= 0} + loading={isLoadingFeedbackCount} + > + {feedbackCount > 0 + ? "View Feedbacks" + : "No feedback for published training"} + + )} {error && ( diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/FeedbackPopup.js b/frontend/src/components/Layout/AIModels/AIModelEditor/FeedbackPopup.js index 07a26a58..958dbc21 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/FeedbackPopup.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/FeedbackPopup.js @@ -21,41 +21,25 @@ const useStyles = makeStyles((theme) => ({ })); const FeedbackPopup = ({ - feedbackData, onClose, sourceImagery, trainingId, + feedbackData, }) => { const classes = useStyles(); const [open, setOpen] = useState(true); const [loading, setLoading] = useState(false); const [freezeLayers, setFreezeLayers] = useState(false); - const { accessToken } = useContext(AuthContext); const actionCounts = { CREATE: 0, MODIFY: 0, ACCEPT: 0, }; + const [epochs, setEpochs] = useState(2); const [batchSize, setBatchSize] = useState(1); - feedbackData.features.forEach((feature) => { - switch (feature.properties.action) { - case "CREATE": - actionCounts.CREATE++; - break; - case "MODIFY": - actionCounts.MODIFY++; - break; - case "ACCEPT": - actionCounts.ACCEPT++; - break; - default: - break; - } - }); - const handleClose = () => { setOpen(false); onClose(); diff --git a/frontend/src/components/Layout/AIModels/AIModels.js b/frontend/src/components/Layout/AIModels/AIModels.js index 318ca618..f9e4d367 100644 --- a/frontend/src/components/Layout/AIModels/AIModels.js +++ b/frontend/src/components/Layout/AIModels/AIModels.js @@ -1,32 +1,55 @@ -import { Box, Button } from '@mui/material'; -import React, { useContext } from 'react' -import { Route, Routes } from 'react-router-dom'; -import AuthContext from '../../../Context/AuthContext'; -import AIModelEditor from './AIModelEditor/AIModelEditor'; -import AIModelNew from './AIModelNew/AIModelNew'; -import AIModelsList from './AIModelsList/AIModelsList'; +import { Box, Button } from "@mui/material"; +import React, { useContext } from "react"; +import { Route, Routes } from "react-router-dom"; +import AuthContext from "../../../Context/AuthContext"; +import AIModelEditor from "./AIModelEditor/AIModelEditor"; +import AIModelNew from "./AIModelNew/AIModelNew"; +import AIModelsList from "./AIModelsList/AIModelsList"; +import Feedback from "../Feedback/Feedback"; -const AIModels = props => { +const AIModels = (props) => { + const { accessToken } = useContext(AuthContext); - const {accessToken} = useContext(AuthContext) + return ( + <> + {accessToken && ( + + {/* } /> */} + } + /> + } + /> + } /> + } + /> + + )} - return <> - -{accessToken && - {/* } /> */} - } /> - } /> - } /> - } - - {!accessToken && -
-  arrow -
} - - + {!accessToken && ( +
+  arrow +
+ )} -} + ); +}; -export default AIModels; \ No newline at end of file +export default AIModels; diff --git a/frontend/src/components/Layout/Feedback/Feedback.js b/frontend/src/components/Layout/Feedback/Feedback.js new file mode 100644 index 00000000..7e34673b --- /dev/null +++ b/frontend/src/components/Layout/Feedback/Feedback.js @@ -0,0 +1,484 @@ +import React, { useContext, useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import AuthContext from "../../../Context/AuthContext"; +import axios from "../../../axios"; +import { useMutation, useQuery } from "react-query"; +import { + Alert, + AlertTitle, + Grid, + LinearProgress, + Stack, + Typography, +} from "@mui/material"; +import { EditControl } from "react-leaflet-draw"; +import { GeoJSON, Marker, Popup } from "react-leaflet"; +import L from "leaflet"; +import "leaflet-draw/dist/leaflet.draw.css"; +import centroid from "@turf/centroid"; +import polygonize from "@turf/polygonize"; +import { + FeatureGroup, + LayersControl, + MapContainer, + Polygon, + TileLayer, + useMapEvents, +} from "react-leaflet"; +import FeedbackAOI from "./FeedbackAOI"; +import { approximateGeom, converToGeoPolygon } from "../../../utils"; +import { icon } from "leaflet"; +import FeedackTraining from "./FeedbackTraining"; +const ICON = icon({ + iconUrl: "/hot-marker.png", + iconSize: new L.Point(100, 100), + className: "leaflet-div-icon", +}); +const Feedback = (props) => { + let { id, trainingId } = useParams(); + + const { accessToken } = useContext(AuthContext); + + const [sourceImagery, setSourceImagery] = useState(""); + const [AOIs, setAOIs] = useState(null); + const getSourceImagery = async () => { + try { + const response = await axios.get(`/training/${trainingId}/`); + setSourceImagery(response.data.source_imagery); + getImagery(response.data.source_imagery); + } catch (error) { + console.error(error); + } + }; + const [feedbackData, setFeedbackData] = useState(null); + const { + mutate, + data: trainingData, + isLoading: isLoadingTraining, + } = useMutation(getSourceImagery); + const getFeedback = async () => { + try { + const headers = { + "access-token": accessToken, + }; + const res = await axios.get(`/feedback/?training=${trainingId}`, null, { + headers, + }); + + if (res.error) { + } else { + // console.log(`/feedback/?training=${trainingId}`, res.data); + mutate(); + setFeedbackData(res.data); + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const [originalAOIs, setOriginalAOIs] = useState(null); + const [datasetId, setDatasetId] = useState(null); + const getOriginalAOIs = async () => { + try { + const headers = { + "access-token": accessToken, + }; + const res = await axios.get(`/model/${id}`, null, { + headers, + }); + + if (res.error) { + } else { + const datasetId = res.data.dataset; + setDatasetId(datasetId); + const resAOIs = await axios.get(`/aoi/?dataset=${datasetId}`, null, { + headers, + }); + setOriginalAOIs(resAOIs.data); + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + // const { data: feedbackData, isLoading } = useQuery( + // "getFeedback" + trainingId, + // getFeedback, + // { + // refetchInterval: 120000, + // } + // ); + useEffect(() => { + getFeedback(); + getOriginalAOIs(); + return () => {}; + }, []); + + const [zoom, setZoom] = useState(15); + const [bounds, setBounds] = useState({}); + + const [map, setMap] = useState(null); + + const [windowSize, setWindowSize] = useState([ + window.innerWidth, + window.innerHeight, + ]); + const { mutate: getImagery, data: oamImagery } = useMutation(async (url) => { + const res = await axios.get(url.replace("/{z}/{x}/{y}", "")); + if (res.error) { + // setError(res.error.response.statusText); + return; + } + if (map) + map.setView([res.data.center[1], res.data.center[0]], res.data.center[2]); + console.log("OAM data", res.data, "map", map); + return res.data; + }); + + // useEffect(() => { + // console.log("map && oamImagery", map, oamImagery); + + // if (map && oamImagery) { + // // map.setView( + // // ([oamImagery.center[1], oamImagery.center[0]], oamImagery.center[2]) + // // ); + // // setZoom(oamImagery.center[2]); + // } + // return () => {}; + // }, [map, oamImagery]); + + function getFeatureStyle(feature) { + return { + color: "green", + weight: 3, + }; + } + const [error, setError] = useState(""); + const createDB = async ({ geom, leafletId }) => { + try { + const body = { + geom: geom, + training: trainingId, + source_imagery: sourceImagery, + }; + + const headers = { + "access-token": accessToken, + }; + const res = await axios.post(`/feedback-aoi/`, body, { headers }); + console.log("res ", res); + + if (res.error) { + setError(JSON.stringify(res.error)); + } else { + // add aoi ID to the state after insert + setRefresh(Math.random()); + return res.data; + } + } catch (e) { + console.log("isError", e); + setError(e); + } finally { + } + }; + const { mutate: mutateCreateDB, data: createResult } = useMutation(createDB); + const _onCreate = (e) => { + console.log("_onCreate", e); + const { layerType, layer } = e; + + const { _leaflet_id } = layer; + + // call the API and add the AOI to DB + const newAOI = { + id: _leaflet_id, + latlngs: layer.getLatLngs()[0], + area: L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]), + }; + const points = JSON.stringify( + converToGeoPolygon([newAOI])[0][0].reduce( + (p, c, i) => p + c[1] + " " + c[0] + ",", + "" + ) + ).slice(1, -2); + // console.log("points",points) + const approximated = approximateGeom(points); + const polygon = "SRID=4326;POLYGON((" + approximated + "))"; + + console.log("converToPolygon([layer])", polygon); + mutateCreateDB({ + geom: polygon, + leafletId: _leaflet_id, + polyTemp: converToGeoPolygon([newAOI])[0][0], + }); + }; + const [refresh, setRefresh] = useState(Math.random()); + const getLabels = async (box) => { + try { + console.log(" getLabels for box", box); + + const headers = { + "access-token": accessToken, + }; + const res = await axios.get( + `/feedback-label/?in_bbox=${box._southWest.lng},${box._southWest.lat},${box._northEast.lng},${box._northEast.lat}`, + { headers } + ); + console.log("res from getLabels ", res); + if (res.error) setError(res.error); + else { + // show on the map + let leafletGeoJSON = new L.GeoJSON(res.data); + const newLayers = []; + leafletGeoJSON.eachLayer((layer) => { + const { _leaflet_id, feature } = layer; + // console.log("on get labels layer",layer,layer.getLatLngs(),L.GeometryUtil.geodesicArea(layer.getLatLngs())) + newLayers.push({ + id: _leaflet_id, + aoiId: -1, + feature: feature, + type: "label", + latlngs: layer.getLatLngs()[0], + }); + }); + + return res.data; + } + } catch (e) { + console.log("isError", e); + setError(e); + } finally { + } + }; + const { mutate: mutategetLabels, data: labelsData } = useMutation(getLabels); + + function MyComponent() { + const map = useMapEvents({ + zoomend: (e) => { + const { _animateToZoom } = e.target; + console.log("zoomend", e, _animateToZoom); + setZoom(_animateToZoom); + }, + moveend: (e) => { + const { _animateToZoom, _layers } = e.target; + console.log("moveend", e, e.target.getBounds()); + console.log("zoom is", _animateToZoom); + // console.log("see the map ", map); + + if (_animateToZoom >= 18) { + mutategetLabels(e.target.getBounds()); + } else { + // remote labels layer + } + }, + }); + return null; + } + const navigate = useNavigate(); + const onEachFeatureOriginalAOIs = (feature, layer) => { + layer.bindPopup("Original dataset AOI"); + }; + return ( + <> + {!feedbackData && ( + + + + + + + + + + + + )} + + {feedbackData && ( + + + {oamImagery && ( + + + {oamImagery && ( + + )} + + + {/* e.type === "aoi") + )} + /> */} + + + {feedbackData && + feedbackData.features.map((f, indx) => { + return ( + + + Feedback + + + ); + })} + + + + {zoom >= 18 && ( + + )} + + { + // _onFeatureGroupReady(reactFGref, geoJsonLoadedFile); + // if (zoom >= 19) { + // _onFeatureGroupReadyLabels(reactFGref, geoJsonLoadedLabels); + // } else { + // setgeoJsonLoadedLabels(null); + // } + }} + > + + + + )} + + + + + { + e.preventDefault(); + navigate("/ai-models/" + id); + }} + > + Model id: {id} + + , Training id: {trainingId} + + + { + e.preventDefault(); + navigate("/training-datasets/" + datasetId); + }} + > + Original dataset id: {datasetId} + + + + Total feedbacks: {feedbackData && feedbackData.features.length} + {" "} + + Zoom: {zoom.toFixed(1)} + + + + + + Mappers feedback is shown in markers on the map + + + + + {oamImagery && ( + + )} + + + + + )} + + ); +}; + +export default Feedback; diff --git a/frontend/src/components/Layout/Feedback/FeedbackAOI.js b/frontend/src/components/Layout/Feedback/FeedbackAOI.js new file mode 100644 index 00000000..9baa3049 --- /dev/null +++ b/frontend/src/components/Layout/Feedback/FeedbackAOI.js @@ -0,0 +1,336 @@ +import React, { useContext, useEffect, useState } from "react"; +import { + Avatar, + Grid, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + Pagination, + SvgIcon, + Typography, +} from "@mui/material"; +import Tooltip from "@mui/material/Tooltip"; +import { styled } from "@mui/material/styles"; +import DeleteIcon from "@mui/icons-material/Delete"; +import MapIcon from "@mui/icons-material/Map"; +import FolderIcon from "@mui/icons-material/Folder"; +import { MapTwoTone, ZoomInMap } from "@mui/icons-material"; +import usePagination from "./Pagination"; +import { makeStyles, withStyles } from "@material-ui/core/styles"; +import ScreenshotMonitorIcon from "@mui/icons-material/ScreenshotMonitor"; + +import PlaylistRemoveIcon from "@mui/icons-material/PlaylistRemove"; +import { useMutation, useQuery } from "react-query"; +import axios from "../../../axios"; +import AOIDetails from "./FeedbackAOIDetails"; +import AuthContext from "../../../Context/AuthContext"; +import FeedbackAOIDetails from "./FeedbackAOIDetails"; +import area from "@turf/area"; +const Demo = styled("div")(({ theme }) => ({ + backgroundColor: theme.palette.background.paper, +})); + +const ListItemWithWiderSecondaryAction = withStyles({ + secondaryAction: { + paddingRight: 96, + }, +})(ListItem); + +const PER_PAGE = 5; +const FeedbackAOI = (props) => { + const [dense, setDense] = useState(true); + const getFeedbackAOIs = async () => { + try { + const res = await axios.get( + `/feedback-aoi/?training=${props.trainingId}` + ); + + if (res.error) { + // setError(res.error.response.statusText); + } else { + console.log(`/feedback-aoi/?training=${props.trainingId}`, res.data); + props.setAOIs(res.data); + return res.data; + } + } catch (e) { + // setError(e); + } finally { + } + }; + const { data, isLoading, refetch } = useQuery( + "getFeedbackAOIs" + props.trainingId, + getFeedbackAOIs, + { refetchInterval: 60000 } + ); + const count = Math.ceil(data ? data?.features.length / PER_PAGE : 0); + let [page, setPage] = useState(1); + let _DATA = usePagination(data ? data?.features : [], PER_PAGE); + const handleChange = (e, p) => { + setPage(p); + _DATA.jump(p); + }; + // console.log("_DATA", _DATA); + useEffect(() => { + refetch(); + return () => {}; + }, [props.refresh]); + + const { accessToken } = useContext(AuthContext); + const fetchOSMLebels = async (aoiId) => { + try { + const headers = { + "access-token": accessToken, + }; + + const res = await axios.post( + `/label/feedback/osm/fetch/${aoiId}/`, + null, + { + headers, + } + ); + + if (res.error) { + // setMapError(res.error.response.statusText); + console.log(res.error.response.statusText); + } else { + // success full fetch + props.refresh(); + return res.data; + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const { mutate: mutateFetch, data: fetchResult } = + useMutation(fetchOSMLebels); + const DeleteAOI = async (id) => { + try { + const headers = { + "access-token": accessToken, + }; + + const res = await axios.delete(`/feedback-aoi/${id}`, { + headers, + }); + + if (res.error) { + console.log(res); + console.log(res.error.response.statusText); + } else { + // success full fetch + + refetch(); + return res.data; + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const { mutate: mutateDeleteAOI } = useMutation(DeleteAOI); + return ( + <> + + + + List of feedback area of Interests{` (${data?.features.length})`} + + + + {data && data.features && data.features.length > PER_PAGE && ( + + )} + + {data && + data.features && + data.features.length > 0 && + _DATA.currentData().map((layer) => { + // console.log(layer); + return ( + + + + + + + + Area: {area(layer).toLocaleString()} sqm
+ + {parseInt(layer.area) < 5000 ? ( + <> + Area seems to be very small for an AOI +
+ Make sure it is not a Label + + ) : ( + "" + )} +
+ {/* add here a container to get the AOI status from DB */} + {layer.id && ( + + )} + + } + /> + + {/* + + */} + + { + const url = `https://rapideditor.org/rapid#background=${ + props.sourceImagery + ? "custom:" + props.sourceImagery + : "Bing" + }&datasets=fbRoads,msBuildings&disable_features=boundaries&map=16.00/17.9253/120.4841&gpx=&gpx=https://fair-dev.hotosm.org/api/v1/feedback-aoi/gpx/${ + layer.id + }`; + console.log(url); + window.open(url, "_blank", "noreferrer"); + }} + > + {/* */} + RapiD logo + + + + { + console.log( + "props.sourceImagery", + props.sourceImagery + ); + const url = `https://www.openstreetmap.org/edit/#background=${ + props.sourceImagery + ? "custom:" + props.sourceImagery + : "Bing" + }&disable_features=boundaries&gpx=https://fair-dev.hotosm.org/api/v1/feedback-aoi/gpx/${ + layer.id + }&map=10.70/18.9226/81.6991`; + console.log(url); + window.open(url, "_blank", "noreferrer"); + }} + > + {/* */} + OSM logo + + + + { + mutateFetch(layer.id); + console.log( + "call raw data API to fetch OSM labels" + ); + }} + > + + + + {/* { + + console.log("Remove labels ") + }}> + + */} + + { + const lat = + layer.latlngs.reduce(function ( + accumulator, + curValue + ) { + return accumulator + curValue.lat; + }, + 0) / layer.latlngs.length; + const lng = + layer.latlngs.reduce(function ( + accumulator, + curValue + ) { + return accumulator + curValue.lng; + }, + 0) / layer.latlngs.length; + // [lat, lng] are the centroid of the polygon + props.selectAOIHandler([lat, lng], 17); + }} + > + + + + + { + mutateDeleteAOI(layer.id); + }} + > + + + + +
+ ); + })} +
+
+ {props.mapLayers && props.mapLayers.length === 0 && ( + + No AOIs yet, start creating one by selecting AOIs on the top and + create a polygon + + )} +
+ + ); +}; +export default FeedbackAOI; diff --git a/frontend/src/components/Layout/Feedback/FeedbackAOIDetails.js b/frontend/src/components/Layout/Feedback/FeedbackAOIDetails.js new file mode 100644 index 00000000..50d6dfa2 --- /dev/null +++ b/frontend/src/components/Layout/Feedback/FeedbackAOIDetails.js @@ -0,0 +1,50 @@ +import { Typography } from "@material-ui/core"; +import React from "react"; +import { useQuery } from "react-query"; + +import axios from "../../../axios"; + +import { timeSince, aoiStatusText } from "../../../utils"; +const FeedbackAOIDetails = (props) => { + // console.log("rendering AOIDetails",props) + const fetchAOI = async () => { + try { + const res = await axios.get(`/feedback-aoi/${props.id}/`); + + if (res.error) { + // setMapError(res.error.response.statusText); + console.log(res.error.response.statusText); + } else { + // success full fetch + // console.log("API details, ",props.aoiId,res.data); + return res.data; + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const { data } = useQuery("fetchAOI" + props.id, fetchAOI, { + refetchInterval: 5000, + }); + + return ( + <> + {data && ( + + {aoiStatusText(data.properties.label_status)}{" "} + {data.properties.label_fetched && + timeSince(new Date(data.properties.label_fetched), new Date())} + + )} + + ); +}; + +export default FeedbackAOIDetails; diff --git a/frontend/src/components/Layout/Feedback/FeedbackTraining.js b/frontend/src/components/Layout/Feedback/FeedbackTraining.js new file mode 100644 index 00000000..4bc93512 --- /dev/null +++ b/frontend/src/components/Layout/Feedback/FeedbackTraining.js @@ -0,0 +1,147 @@ +import LoadingButton from "@mui/lab/LoadingButton/LoadingButton"; +import { + Alert, + AlertTitle, + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Grid, + Link, + TextField, +} from "@mui/material"; +import React, { useContext, useState } from "react"; +import AuthContext from "../../../Context/AuthContext"; +import axios from "../../../axios"; +import { useMutation } from "react-query"; +import { useNavigate } from "react-router-dom"; + +const FeedackTraining = (props) => { + const [epochs, setEpochs] = useState(20); + const [batchSize, setBatchSize] = useState(8); + const [error, setError] = useState(null); + const { accessToken } = useContext(AuthContext); + const navigate = useNavigate(); + const submitFeedbackTraining = async () => { + try { + setError(null); + const headers = { + "access-token": accessToken, + }; + const body = { + training_id: props.trainingId, + epochs: epochs, + batch_size: batchSize, + zoom_level: [19, 20, 21], + }; + const res = await axios.post(`/feedback/training/submit/`, body, { + headers, + }); + console.log("/feedback/training/submit", res); + if (res.error) { + setError( + res.error.response.statusText + + " / " + + JSON.stringify(res.error.response.data) + ); + return; + } + console.log("training submitted", res.data); + + return res.data; + } catch (e) { + console.log("isError"); + setError(JSON.stringify(e)); + } finally { + } + }; + + const { + mutate, + isLoading, + status, + error: apiError, + } = useMutation(submitFeedbackTraining); + return ( + + setEpochs(Math.max(0, parseInt(e.target.value)))} + inputProps={{ min: 1, step: 1 }} + fullWidth + margin="normal" + /> + setBatchSize(Math.max(0, parseInt(e.target.value)))} + inputProps={{ min: 1, step: 1 }} + fullWidth + margin="normal" + /> + + Freeze Layers + + setFreezeLayers(e.target.checked)} + name="freeze-layers" + disabled={true} + /> + } + label="Freeze Layers" + /> + + + { + mutate(); + }} + loading={isLoading} + > + Apply Feedback training to Model + + + {status === "success" && !error && ( + + + Training is submitted successfully, go to{" "} + { + e.preventDefault(); + navigate("/ai-models/" + props.modelId); + }} + > + Model id: {props.modelId} + {" "} + for more details + + + )} + {apiError && ( + + {apiError} + + )} + {error && ( + + {error} + + )} + + ); +}; + +export default FeedackTraining; diff --git a/frontend/src/components/Layout/Feedback/Pagination.js b/frontend/src/components/Layout/Feedback/Pagination.js new file mode 100644 index 00000000..24dfbaeb --- /dev/null +++ b/frontend/src/components/Layout/Feedback/Pagination.js @@ -0,0 +1,29 @@ +import React, { useState } from "react"; + +function usePagination(data, itemsPerPage) { + const [currentPage, setCurrentPage] = useState(1); + const maxPage = Math.ceil(data.length / itemsPerPage); + + function currentData() { + const begin = (currentPage - 1) * itemsPerPage; + const end = begin + itemsPerPage; + return data.slice(begin, end); + } + + function next() { + setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage)); + } + + function prev() { + setCurrentPage((currentPage) => Math.max(currentPage - 1, 1)); + } + + function jump(page) { + const pageNumber = Math.max(1, page); + setCurrentPage((currentPage) => Math.min(pageNumber, maxPage)); + } + + return { next, prev, jump, currentData, currentPage, maxPage }; +} + +export default usePagination; diff --git a/frontend/src/components/Layout/Learn/Learn.js b/frontend/src/components/Layout/Learn/Learn.js index cdae6135..60ef63db 100644 --- a/frontend/src/components/Layout/Learn/Learn.js +++ b/frontend/src/components/Layout/Learn/Learn.js @@ -23,105 +23,105 @@ const Learn = () => { title: "Step 1: First Login & Click on the button Start Creating Dataset", description: "To create a new training dataset, start by clicking on the 'Start Creating Dataset' button.", - image: "/learn/1.png", + image: "/learn-resources/1.png", }, { id: 2, title: "Step 2: Click on the button Create New", description: "After clicking on 'Start Creating Dataset', click on the 'Create New' button to create a new dataset.", - image: "/learn/2.png", + image: "/learn-resources/2.png", }, { id: 3, title: "Step 3: Click on the input field , Give your Dataset Name", description: "To name your dataset, click on the input field and type in your desired name & click on the 'Create Training Dataset'", - image: "/learn/3.png", + image: "/learn-resources/3.png", }, { id: 4, title: "Step 4: Find Image to Train", description: "After creating Dataset You will see following screen , Now Open Open Aerial Map in New Tab , https://openaerialmap.org/", - image: "/learn/4.png", + image: "/learn-resources/4.png", }, { id: 5, title: "Step 5: Find Drone Image and Copy TMS URL", description: "Look for your area and find good Drone Image for training . After finding , Click on Copy Image URL TMS", - image: "/learn/5.png", + image: "/learn-resources/5.png", }, { id: 6, title: "Step 6: Paste TMS To Open Aerial Imagery Tab", description: "Paste your TMS URL that you have copied from Open Aerial Map Drone Image", - image: "/learn/6.png", + image: "/learn-resources/6.png", }, { id: 7, title: "Step 7: Zoom to Layer and Visualize Image", description: "Click on Zoom to layer next to your Image name in OAM Block Top Right side of screen", - image: "/learn/7.png", + image: "/learn-resources/7.png", }, { id: 8, title: "Step 8: Create Area of Interest for Training", description: "Click on top left map buttons below zoom , To create AOI , AOI will be used to create labels. Labels are Buildings that will be acting as input for model inside AOI", - image: "/learn/8.png", + image: "/learn-resources/8.png", }, { id: 9, title: "Step 9: Fetch Exisiting OSM Buildings in your Area of Interest", description: "Click on Fetch OSM Data button next to OSM Logo Inside List of Area of Interest on Right side.", - image: "/learn/9.png", + image: "/learn-resources/9.png", }, { id: 10, title: "Step 10: Visualize each Buildings to check their accuracy", description: "Zoom to Level 20 in Map to see OSM Buildings that you have just fetched", - image: "/learn/10.png", + image: "/learn-resources/10.png", }, { id: 11, title: "Step 11: Correct your labels", description: "For training it is crucial that buildings are aligned exactly on Drone image, More good your input is more good your output will be. If you want to edit it Click on OSM Logo and Fix buildings / labels in your AOI", - image: "/learn/11.png", + image: "/learn-resources/11.png", }, { id: 12, title: "Step 12: Upload your fixes", description: "You should see your AOI in ID , Fix your labels and Upload your changes to OSM", - image: "/learn/osm.png", + image: "/learn-resources/osm.png", }, { id: 13, title: "Step 13: Come Back to fAIr , Fetch OSM Labels", description: "After you do your changes on OSM , Comeback to fAIr and fetch new labels, Give it a few min to update our database ( Click on button next to OSM Logo on AOI to fetch -Step 9)", - image: "/learn/12.png", + image: "/learn-resources/12.png", }, { id: 14, title: "Step 14: Create Model for you Dataset", description: "Click on View Models on your dataset page , You will see this page. Click on Create New", - image: "/learn/13.png", + image: "/learn-resources/13.png", }, { id: 15, title: "Step 15: Provide Model Metadata", description: "Give your model name , Choose Your Dataset from Dropdown , Select BaseModel : RAMP is default for now , Click on Create AI Model", - image: "/learn/14.png", + image: "/learn-resources/14.png", }, { id: 16, diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js index b5b7b911..f4d32575 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js @@ -84,7 +84,7 @@ const AOI = (props) => { return ( <> - + List of Area of Interests{` (${props.mapLayers.length})`} diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.css b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.css index 31f7c992..0b4bb2c4 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.css +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.css @@ -2,11 +2,6 @@ text-align: center; } -.leaflet-tile-container img -{ - padding: 1px; -} - .margin1 { margin-left: 2px !important; @@ -55,14 +50,6 @@ margin-top: -20px; } -.card -{ - border: 2px solid ; - border-radius: 10px; - box-shadow: 5px 5px 5px #cdcdcd; - margin-bottom: 5px; - padding: 10px 5px; -} .small-checkbox { diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js index 7968727c..6a45c947 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetEditor.js @@ -9,6 +9,7 @@ import MapActions from "./MapActions"; import axios from "../../../../axios"; import { useMutation } from "react-query"; import { useParams } from "react-router-dom"; +import DatasetEditorHeader from "./DatasetEditorHeader"; function DatasetEditor() { const [mapLayers, setMapLayers] = useState([]); @@ -40,6 +41,14 @@ function DatasetEditor() { if (res.error) setError(res.error.response.statusText); console.log("dataset", res.data); + setOAMImagery({ + center: [0, 0], + name: "Private", + minzoom: 0, + maxzoom: 23, + attribution: res.data.source_imagery, + url: res.data.source_imagery, + }); return res.data; } catch (e) { console.log("isError"); @@ -54,7 +63,7 @@ function DatasetEditor() { return () => {}; }, []); - + const [zoom, setZoom] = useState(15); return ( <> {isLoading && "Loading ............"} @@ -77,9 +86,20 @@ function DatasetEditor() { setgeoJSON(null); }} dataset={dataset} + setZoom={setZoom} > - + + + { return ( <> - - - - - Dataset {props.dsId} : {props.dsName} - - + + + Dataset {props.dsId} : {props.dsName} + - - - Zoom: {props.zoom && +props.zoom.toFixed(1)} -
- {"Editing " + props.editMode} -
-
- - - { - console.log("changed", e); - props.setEditMode(e.target.value); - if (e.target.value === "aoi") { - // console.log("leaflet-bar a",document.querySelectorAll(".leaflet-bar a")) + + Zoom: {props.zoom && +props.zoom.toFixed(1)} + + {/* + + { + console.log("changed", e); + props.setEditMode(e.target.value); + if (e.target.value === "aoi") { + // console.log("leaflet-bar a",document.querySelectorAll(".leaflet-bar a")) - document.querySelectorAll(".leaflet-bar a").forEach((e) => { - e.style.backgroundColor = "rgb(51, 136, 255)"; - console.log("leaflet-bar a", e.style); - }); - } else { - console.log( - "leaflet-bar a", - document.querySelectorAll(".leaflet-bar a") - ); + document.querySelectorAll(".leaflet-bar a").forEach((e) => { + e.style.backgroundColor = "rgb(51, 136, 255)"; + console.log("leaflet-bar a", e.style); + }); + } else { + console.log( + "leaflet-bar a", + document.querySelectorAll(".leaflet-bar a") + ); - document.querySelectorAll(".leaflet-bar a").forEach((e) => { - e.style.backgroundColor = "#D73434"; - console.log("leaflet-bar a", e.style); - }); - } - }} - > - - } - label="AOIs" - labelPlacement="top" - /> - {/* - } - label="Labels" - labelPlacement="top" - /> */} - - + document.querySelectorAll(".leaflet-bar a").forEach((e) => { + e.style.backgroundColor = "#D73434"; + console.log("leaflet-bar a", e.style); + }); + } + }} + > + + + + */} - {/* */} -
- - - -
-
+ +
); }; diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js index 11ce4017..19804fde 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js @@ -564,6 +564,7 @@ const DatasetMap = (props) => { const { _animateToZoom } = e.target; console.log("zoomend", e, _animateToZoom); setZoom(_animateToZoom); + props.setZoom(_animateToZoom); }, moveend: (e) => { const { _animateToZoom, _layers } = e.target; @@ -595,18 +596,9 @@ const DatasetMap = (props) => { return null; } + // console.log("props.oamImagery", props.oamImagery); return ( <> - - {mapError && Error: {mapError} } { style={{ height: "800px", width: "100%", - marginTop: "75px", }} zoom={zoom} + zoomDelta={0.25} + wheelPxPerZoomLevel={Math.round(36 / 0.5)} + zoomSnap={0} + scrollWheelZoom={true} + // inertia={true} whenCreated={setMap} > @@ -683,16 +679,12 @@ const DatasetMap = (props) => { { - console.log( - "selectedLayer", - document.querySelector('input[name="selectedLayer"]:checked') - .value - ); - _onCreate( - e, - document.querySelector('input[name="selectedLayer"]:checked') - .value - ); + // console.log( + // "selectedLayer", + // document.querySelector('input[name="selectedLayer"]:checked') + // .value + // ); + _onCreate(e, "aoi"); }} onEdited={_onEdited} onDeleted={_onDeleted} diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/TileServerList.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/TileServerList.js index f6889205..2731eeee 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/TileServerList.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/TileServerList.js @@ -61,21 +61,34 @@ const TileServerList = (props) => { try { setLoading(true); setInputError(false); - const res = await axios.get(url.replace("/{z}/{x}/{y}", "")); + if (url.includes("openaerial")) { + const res = await axios.get(url.replace("/{z}/{x}/{y}", "")); - if (!res) { - setInputError("Invalid OAM Link"); - return; - } - if (res.error) setInputError(res.error.response.statusText); + if (!res) { + setInputError("Invalid OAM Link"); + return; + } + if (res.error) setInputError(res.error.response.statusText); - props.addImagery(res.data, url); - setImageryDetails(res.data); - props.navigateToCenter(res.data.center); - // console.log("getImageryDetails url",url ) + props.addImagery(res.data, url); + setImageryDetails(res.data); + props.navigateToCenter(res.data.center); + // console.log("getImageryDetails url",url ) - mutateSaveImageryToDataset({ id: props.dataset.id, url }); - return res.data; + mutateSaveImageryToDataset({ id: props.dataset.id, url }); + return res.data; + } else { + // private imagery + mutateSaveImageryToDataset({ id: props.dataset.id, url }); + const obj = { + center: [0, 0], + name: "Private", + minzoom: 15, + maxzoom: 23, + }; + props.addImagery(obj, url); + setImageryDetails(obj); + } } catch (e) { console.log("isError"); // setInputError(e); @@ -96,8 +109,17 @@ const TileServerList = (props) => { console.log("useEffect ", props); // setOAMURL(props.dataset.source_imagery) - if (props.dataset.source_imagery) + if (props.dataset.source_imagery.includes("openaerial")) getImageryDetails(props.dataset.source_imagery); + else { + // load imagery + setImageryDetails({ + center: [0, 0], + name: "Private", + minzoom: 15, + maxzoom: 23, + }); + } return () => {}; }, []); @@ -198,11 +220,11 @@ const TileServerList = (props) => { setInputError(false); let trimmedValue = e.target.value.trim(); let regUrl = /^(https?|chrome):\/\/[^\s$.?#].[^\s]*$/; - let endsWithPng = trimmedValue.endsWith(".png"); - if (endsWithPng) { - trimmedValue = trimmedValue.slice(0, -4); - } - let hasZXY = trimmedValue.endsWith("{z}/{x}/{y}"); + // let endsWithPng = trimmedValue.endsWith(".png"); + // if (endsWithPng) { + // trimmedValue = trimmedValue.slice(0, -4); + // } + let hasZXY = trimmedValue.includes("{z}/{x}/{y}"); let isValid = regUrl.test(trimmedValue) && hasZXY && diff --git a/frontend/src/components/Layout/TrainingDS/DatasetNew/DatasetNew.js b/frontend/src/components/Layout/TrainingDS/DatasetNew/DatasetNew.js index de4aeaf5..91957014 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetNew/DatasetNew.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetNew/DatasetNew.js @@ -91,12 +91,14 @@ const DatasetNew = (props) => { fullWidth onChange={(e) => { let trimmedValue = e.target.value.trim(); - let regUrl = /^(https?|chrome):\/\/[^\s$.?#].[^\s]*$/; - let endsWithPng = trimmedValue.endsWith(".png"); - if (endsWithPng) { - trimmedValue = trimmedValue.slice(0, -4); - } - let hasZXY = trimmedValue.endsWith("{z}/{x}/{y}"); + // let regUrl = /^(https?|chrome):\/\/[^\s$.?#].[^\s]*$/; + // let endsWithPng = + // trimmedValue.endsWith(".png") || + // trimmedValue.endsWith(".jpeg"); + // if (endsWithPng) { + // trimmedValue = trimmedValue.slice(0, -4); + // } + let hasZXY = trimmedValue.includes("{z}/{x}/{y}"); let isValid = regUrl.test(trimmedValue) && hasZXY && @@ -123,7 +125,7 @@ const DatasetNew = (props) => { !oamURL || !regUrl.test(oamURL) || DSName.trim() === "" || - !oamURL.endsWith("{z}/{x}/{y}") || + !oamURL.includes("{z}/{x}/{y}") || isLoading } > diff --git a/frontend/src/index.css b/frontend/src/index.css index 82e48df5..1a670d0d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -146,3 +146,27 @@ code { { background-color: transparent !important; } + +.leaflet-tile-container img, .leaflet-pane.leaflet-tile-pane img, div.leaflet-pane.leaflet-tile-pane img +{ + padding: 1px; +} + +.card +{ + border: 2px solid ; + border-radius: 10px; + box-shadow: 5px 5px 5px #cdcdcd; + margin-bottom: 5px; + padding: 10px 5px; +} + +.margin-left-12 +{ + margin-left: 12px !important; +} + +.leaflet-interactive { + width: 30px !important; + height: 30px !important; +} \ No newline at end of file diff --git a/frontend/src/routes.js b/frontend/src/routes.js index 4cafd3bc..f4e26b4d 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -13,7 +13,9 @@ const Documentation = React.lazy(() => ); const Learn = React.lazy(() => import("./components/Layout/Learn/Learn")); const Start = React.lazy(() => import("./components/Layout/Start/Start")); -const Why = React.lazy(() => import("./components/Layout/Why/Why")); +const Feedback = React.lazy(() => + import("./components/Layout/Feedback/Feedback") +); export const publicRoutes = [ // add here all route you wish to implement and associate each one with a component, recommended to use lazy loading @@ -23,12 +25,13 @@ export const publicRoutes = [ element: , }, { path: "/ai-models/*", name: "Training Datasets", element: }, + { path: "/documentation", name: "Training Datasets", element: , }, - { path: "/learn/*", name: "Learn", element: }, + { path: "/learn", name: "Learn", element: }, { path: "/start-mapping/*", name: "Training Datasets", element: }, // { path: "/why-fair", name: "Training Datasets", element: }, { path: "/authenticate", name: "Authenticate", element: },