From bef184e47bcd8b37553f844d3539d50c27eefc04 Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Thu, 20 Jul 2023 12:27:12 +0200 Subject: [PATCH 01/12] Style fix --- .../Layout/Start/Prediction/Prediction.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Layout/Start/Prediction/Prediction.js b/frontend/src/components/Layout/Start/Prediction/Prediction.js index 36e7aa7b..dbe74665 100644 --- a/frontend/src/components/Layout/Start/Prediction/Prediction.js +++ b/frontend/src/components/Layout/Start/Prediction/Prediction.js @@ -505,17 +505,19 @@ const Prediction = () => { Loaded Model - - { - e.preventDefault(); - navigate("/ai-models/" + modelInfo.id); - }} - color="inherit" - > - ID: {modelInfo.id} - + + + { + e.preventDefault(); + navigate("/ai-models/" + modelInfo.id); + }} + color="inherit" + > + ID: {modelInfo.id} + + Name: {modelInfo.name} From 7dede1cfe05ac41a5c8791bc071eb66e23a17c74 Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Mon, 24 Jul 2023 18:33:25 +0545 Subject: [PATCH 02/12] Install latest hot fair utilties --- .github/workflows/backend_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend_build.yml b/.github/workflows/backend_build.yml index 9744c901..b2a3815c 100644 --- a/.github/workflows/backend_build.yml +++ b/.github/workflows/backend_build.yml @@ -59,7 +59,7 @@ jobs: cd ramp-code && cd colab && make install - name: Install fair utilities - run: pip install hot-fair-utilities==1.0.41 + run: pip install hot-fair-utilities - name: Install Psycopg2 run: | From 817acd0f10ced2b7ae6338b42338c587a232e5fa Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Tue, 25 Jul 2023 12:08:15 +0545 Subject: [PATCH 03/12] Implemented new feedback models and their respective API's --- backend/core/models.py | 41 ++++++--- backend/core/serializers.py | 39 +++++++-- backend/core/urls.py | 4 + backend/core/views.py | 46 ++++++++-- docker-compose.yml | 20 ++--- .../TrainingDS/DatasetEditor/AOIDetails.js | 85 ++++++++++--------- 6 files changed, 162 insertions(+), 73 deletions(-) diff --git a/backend/core/models.py b/backend/core/models.py index b4ed7435..b1d67be1 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -31,8 +31,8 @@ class DownloadStatus(models.IntegerChoices): dataset = models.ForeignKey(Dataset, to_field="id", on_delete=models.CASCADE) geom = geomodels.PolygonField(srid=4326) - download_status = models.IntegerField(default=-1, choices=DownloadStatus.choices) - last_fetched_date = models.DateTimeField(null=True, blank=True) + label_status = models.IntegerField(default=-1, choices=DownloadStatus.choices) + label_fetched = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) last_modified = models.DateTimeField(auto_now=True) @@ -42,7 +42,6 @@ class Label(models.Model): geom = geomodels.GeometryField(srid=4326) osm_id = models.BigIntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) - last_modified = models.DateTimeField(auto_now=True) class Model(models.Model): @@ -89,11 +88,11 @@ class Training(models.Model): class Feedback(models.Model): - ACTION_TYPE = ( - ("CREATE", "CREATE"), - ("MODIFY", "MODIFY"), - ("ACCEPT", "ACCEPT"), - ("INITIAL", "INITIAL"), + FEEDBACK_TYPE = ( + ("TP", "True Positive"), + ("TN", "True Negative"), + ("FP", "False Positive"), + ("FN", "False Negative"), ) geom = geomodels.GeometryField(srid=4326) training = models.ForeignKey(Training, to_field="id", on_delete=models.CASCADE) @@ -101,7 +100,29 @@ class Feedback(models.Model): zoom_level = models.PositiveIntegerField( validators=[MinValueValidator(18), MaxValueValidator(23)] ) - action = models.CharField(choices=ACTION_TYPE, max_length=10) + feedback_type = models.CharField(choices=FEEDBACK_TYPE, max_length=10) + comments = models.TextField(max_length=100,null=True,blank=True) + user = models.ForeignKey(OsmUser, to_field="osm_id", on_delete=models.CASCADE) + source_imagery = models.URLField() + + +class FeedbackAOI(models.Model): + class DownloadStatus(models.IntegerChoices): + DOWNLOADED = 1 + NOT_DOWNLOADED = -1 + RUNNING = 0 + training = models.ForeignKey(Training, to_field="id", on_delete=models.CASCADE) + geom = geomodels.PolygonField(srid=4326) + label_status = models.IntegerField(default=-1, choices=DownloadStatus.choices) + label_fetched = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) last_modified = models.DateTimeField(auto_now=True) + source_imagery = models.URLField() user = models.ForeignKey(OsmUser, to_field="osm_id", on_delete=models.CASCADE) - validated = models.BooleanField(default=False) + + +class FeedbackLabel(models.Model): + osm_id = models.BigIntegerField(null=True, blank=True) + feedback_aoi = models.ForeignKey(FeedbackAOI, to_field="id", on_delete=models.CASCADE) + geom = geomodels.PolygonField(srid=4326) + created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 35cf792e..2381ce3e 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -57,11 +57,32 @@ class Meta: read_only_fields = ( "created_at", "last_modified", - "last_fetched_date", - "download_status", + "label_fetched", + "label_status", ) +class FeedbackAOISerializer(GeoFeatureModelSerializer): + class Meta: + model = FeedbackAOI + geo_field = "geom" + fields = "__all__" + partial = True + + read_only_fields = ( + "created_at", + "last_modified", + "label_fetched", + "label_status", + "user", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["user"] = user + return super().create(validated_data) + + class FeedbackSerializer(GeoFeatureModelSerializer): class Meta: model = Feedback @@ -90,10 +111,15 @@ class Meta: # auto_bbox = True fields = "__all__" # defining all the fields to be included in curd for now , we can restrict few if we want - read_only_fields = ( - "created_at", - "last_modified", - ) + read_only_fields = ("created_at", "osm_id") + + +class FeedbackLabelSerializer(GeoFeatureModelSerializer): + class Meta: + model = FeedbackLabel + geo_field = "geom" + fields = "__all__" + read_only_fields = ("created_at", "osm_id") class LabelFileSerializer( @@ -136,6 +162,7 @@ class FeedbackParamSerializer(serializers.Serializer): batch_size = serializers.IntegerField(required=False) freeze_layers = serializers.BooleanField(required=False) + class PredictionParamSerializer(serializers.Serializer): bbox = serializers.ListField(child=serializers.FloatField(), required=True) model_id = serializers.IntegerField(required=True) diff --git a/backend/core/urls.py b/backend/core/urls.py index 8abbea60..8c395cdd 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -7,6 +7,8 @@ AOIViewSet, APIStatus, DatasetViewSet, + FeedbackAOIViewset, + FeedbackLabelViewset, FeedbackView, FeedbackViewset, GenerateGpxView, @@ -31,6 +33,8 @@ router.register(r"training", TrainingViewSet) router.register(r"model", ModelViewSet) router.register(r"feedback", FeedbackViewset) +router.register(r"feedback-aoi", FeedbackAOIViewset) +router.register(r"feedback-label", FeedbackLabelViewset) urlpatterns = [ diff --git a/backend/core/views.py b/backend/core/views.py index b6c9e5bb..ddbf8357 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -38,11 +38,22 @@ from rest_framework.views import APIView from rest_framework_gis.filters import InBBoxFilter, TMSTileFilter -from .models import AOI, Dataset, Feedback, Label, Model, Training +from .models import ( + AOI, + Dataset, + Feedback, + FeedbackAOI, + FeedbackLabel, + Label, + Model, + Training, +) from .serializers import ( AOISerializer, DatasetSerializer, + FeedbackAOISerializer, FeedbackFileSerializer, + FeedbackLabelSerializer, FeedbackParamSerializer, FeedbackSerializer, LabelSerializer, @@ -150,7 +161,30 @@ class FeedbackViewset(viewsets.ModelViewSet): queryset = Feedback.objects.all() http_method_names = ["get", "post", "patch", "delete"] serializer_class = FeedbackSerializer # connecting serializer - filterset_fields = ["training", "user", "action", "validated"] + filterset_fields = ["training", "user", "feedback_type"] + + +class FeedbackAOIViewset(viewsets.ModelViewSet): + authentication_classes = [OsmAuthentication] + permission_classes = [IsOsmAuthenticated] + permission_allowed_methods = ["GET"] + queryset = FeedbackAOI.objects.all() + http_method_names = ["get", "post", "patch", "delete"] + serializer_class = FeedbackAOISerializer + filterset_fields = [ + "training", + "user", + ] + + +class FeedbackLabelViewset(viewsets.ModelViewSet): + authentication_classes = [OsmAuthentication] + permission_classes = [IsOsmAuthenticated] + permission_allowed_methods = ["GET"] + queryset = FeedbackLabel.objects.all() + http_method_names = ["get", "post", "patch", "delete"] + serializer_class = FeedbackLabelSerializer + filterset_fields = ["feedback_aoi"] class ModelViewSet( @@ -208,7 +242,7 @@ def post(self, request, aoi_id, *args, **kwargs): """ obj = get_object_or_404(AOI, id=aoi_id) try: - obj.download_status = 0 + obj.label_status = 0 obj.save() raw_data_params = { "geometry": json.loads(obj.geom.geojson), @@ -218,12 +252,12 @@ def post(self, request, aoi_id, *args, **kwargs): result = request_rawdata(raw_data_params) file_download_url = result["download_url"] process_rawdata(file_download_url, aoi_id) - obj.download_status = 1 - obj.last_fetched_date = datetime.utcnow() + obj.label_status = 1 + obj.label_fetched = datetime.utcnow() obj.save() return Response("Success", status=status.HTTP_201_CREATED) except Exception as ex: - obj.download_status = -1 + obj.label_status = -1 obj.save() # raise ex return Response("OSM Fetch Failed", status=500) diff --git a/docker-compose.yml b/docker-compose.yml index 0080def3..ea9c9bdf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: postgres: @@ -10,8 +10,8 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=admin ports: - - '5434:5432' - + - "5434:5432" + redis: image: redis container_name: redis @@ -20,7 +20,7 @@ services: backend-api: build: - context: ./backend + context: ./backend dockerfile: Dockerfile container_name: api command: python manage.py runserver 0.0.0.0:8000 @@ -34,14 +34,14 @@ services: - 8000:8000 volumes: - ${RAMP_HOME}:/RAMP_HOME - - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE + # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: - redis - postgres backend-worker: build: - context: ./backend + context: ./backend dockerfile: Dockerfile container_name: worker command: celery -A aiproject worker --loglevel=INFO @@ -53,7 +53,7 @@ services: capabilities: [gpu] volumes: - ${RAMP_HOME}:/RAMP_HOME - - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE + # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: - backend-api - redis @@ -72,11 +72,11 @@ services: frontend: build: - context: ./frontend + context: ./frontend dockerfile: Dockerfile.frontend container_name: frontend command: npm start -- --host 0.0.0.0 --port 3000 ports: - - 3000:3000 + - 3000:3000 depends_on: - - backend-api \ No newline at end of file + - backend-api diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOIDetails.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOIDetails.js index 37add50d..3d558995 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOIDetails.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOIDetails.js @@ -1,47 +1,50 @@ -import { Typography } from '@material-ui/core'; -import React from 'react' -import { useQuery } from 'react-query'; +import { Typography } from "@material-ui/core"; +import React from "react"; +import { useQuery } from "react-query"; -import axios from '../../../../axios' +import axios from "../../../../axios"; -import {timeSince,aoiStatusText} from '../../../../utils' -const AOIDetails = props => -{ +import { timeSince, aoiStatusText } from "../../../../utils"; +const AOIDetails = (props) => { + // console.log("rendering AOIDetails",props) + const fetchAOI = async () => { + try { + const res = await axios.get(`/aoi/${props.aoiId}/`); - // console.log("rendering AOIDetails",props) - const fetchAOI = async () => { + 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.aoiId, fetchAOI, { + refetchInterval: 5000, + }); - try { - - - const res = await axios.get(`/aoi/${props.aoiId}/`); - - 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.aoiId,fetchAOI,{refetchInterval:5000}); - - return <> - {data && - {aoiStatusText(data.properties.download_status)} {data.properties.last_fetched_date && timeSince(new Date(data.properties.last_fetched_date),new Date()) } - } + return ( + <> + {data && ( + + {aoiStatusText(data.properties.label_status)}{" "} + {data.properties.label_fetched && + timeSince(new Date(data.properties.label_fetched), new Date())} + + )} -} + ); +}; -export default AOIDetails \ No newline at end of file +export default AOIDetails; From 47c8f8bfc897516da45a2586900829367f25b42f Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Thu, 27 Jul 2023 10:31:37 +0545 Subject: [PATCH 04/12] Added support for volume , Fixes #139 --- backend/Dockerfile | 1 - docker-compose.yml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index c8f1cbbb..2f67c2be 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -28,6 +28,5 @@ RUN pip install setuptools --upgrade COPY requirements.txt requirements.txt RUN pip install -r requirements.txt -RUN mkdir /app WORKDIR /app COPY . /app \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ea9c9bdf..f8ab6239 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,7 @@ services: ports: - 8000:8000 volumes: + - ./backend:/app - ${RAMP_HOME}:/RAMP_HOME # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: @@ -52,6 +53,7 @@ services: - driver: nvidia capabilities: [gpu] volumes: + - ./backend:/app - ${RAMP_HOME}:/RAMP_HOME # - ${TRAINING_WORKSPACE}:/TRAINING_WORKSPACE depends_on: @@ -80,3 +82,5 @@ services: - 3000:3000 depends_on: - backend-api + volumes: + - ./frontend:/app From 4ebd2317a012af1ee29851fd15b67c9d4817b2fe Mon Sep 17 00:00:00 2001 From: Kshitij Raj Sharma <36752999+kshitijrajsharma@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:37:08 +0545 Subject: [PATCH 05/12] Update docker-compose.yml --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f8ab6239..80dfc78e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,5 +82,3 @@ services: - 3000:3000 depends_on: - backend-api - volumes: - - ./frontend:/app From 6e755b53084af9c1e2eb1ac8c8d533f1a3c1c376 Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Mon, 31 Jul 2023 12:07:33 +0200 Subject: [PATCH 06/12] using rapideditor.org after rapiD updates --- frontend/public/rapid-logo.png | Bin 0 -> 20474 bytes .../Layout/TrainingDS/DatasetEditor/AOI.js | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 frontend/public/rapid-logo.png diff --git a/frontend/public/rapid-logo.png b/frontend/public/rapid-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6f3b6dd1b7019cc803676e00fdef8de6e54278 GIT binary patch literal 20474 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 z za3l*T;|m(Gm7m`h``5j35B2j$Cywo|Lg`}fV}FPm)MM6X)FWLF!=vsEQhiD2gjI;j z*udEjbSBBKFF-J_m;@S*@7#-a{op`0bJP{|O_k2)nH&dSoNjJPtc><>%fH#Ol_(Y~ zs}Ru+1@vq0h6i1BQGu2GH!I|JyICxyO34J$Yo&1BvyaRjMe);stVPSn5r?QEYOweo zX4GKY&wjzuj^|K6^%?V(eCTOR>4PBKbvD#b>P?$_hr1i`8U5N0@TOIHVi?3ynr`sv zGw>oAW`Q&N{r{`u+~b*S{|CO^w%NwRXp?eiVww?i2;Y(BR2XvD9MU7qsYvorPO(io z5cc>cq*x<|p^$T8&pH_Kw0abkG?~)ji6lwN_g??~{`kFK_v>~4ao?ZUbzS%UxvtlB z-`DkiYb@+cqe6D$|C@Juua44w<3!HOPayNbtxwVt(Q^IFJ3zfWz;lYBXjZEt zeE{gZb_!Q}0WPWdQ3wJrfV5QDg$c9+EK+tD`JiBV`4%KQ-*0C~A-^uK@fRTCft+(I z9naUofKn?Ie7n;@qCY*=Njai;a@27oomscjkj?^(81+ zU(c>D7dQM+Ub=s`$5DYX9!!UV_+5yCr@=^(Hk!7)o{o!@VaspziaiL7U|93=t z#W)cz2@I&3Gr9qLRQ?fyqs~L7u1ns?>AXL>$G%h7blJc0Zrb>j=C^s^7a~i440Lyc}0 za8CRAqF|Nw>NZ-YlaRiU@X}wmJTe3k`nQX{?*jrBS!m+SPA)j}k+S;H(=<-|cR58% zA;v?efb?p*RLu0dTR=5TDLl@pZxj`=@8kZJv#Oqq33T6W$0JR54mLKBj=?O9%IBQ^ zQcJ~6xZTmTyxs|vLTwti2m71UP6Cuz~qm~$8h=yu^rA!YB?XXRE#x*VDLK{qvoDvn}(IYz^v*}&{ zJ3=c}a;bU%a>95j+!V|VO>FLf!V&f9=tM!Z%)Ej0gu2`bVu`&h{Rgh!%+v}$$*Y}a z)VVFKg7}sG&Y{S;L7*nCDID8}!K`c$5zt?MA>5g%1!_I8CEc1{@WTqEoB_#yLgO>| zlPZ1Q(;9ME6IQ_yj2HhX;M>dcXnO>==UX!UJHXD}EWKsguL?Qhvr@88aW``$izs6^ z-umMIf`g1HygYk6s9!L=gL|u)W^mMps=)$jc^az)B>3fP`@h?tTD~E4dy|^t9sDl{ zuwy|(0d*(XXJ!GEyv!9^IA#U5;Dz^Zey{VLiHbD4YoHFFLN2;P!Ts)@C8Uq0*}LY< z*iMAYv*sI3*dO+opS2}!#4o0?tL4b8Kjm(&WB@4LqnMYf@zgIv0+vx)6%<;@aTlyFnAPcz%1MpLVSxkLTBuYlc)wLgD0xE4-X%*Herwa8MzgOf&Q)d1126? zmTumF6wFbi%=$@mcV}Z}!o1%XThZp-ZXeU8=JrFg4q9>4R+v?>kn(A%)nee2bF zE+kEDZWgHH1leJk%luf~?%!5`#0_ruC~sEp|5Akfj^wpycEh^qT%5e(AbAH{Js>Rd zMY!Cdyg{%CT*#-|jxlJVf~kTjw&pQt5&4ua@|J!Vc3H}Fi_`<+vNW|XP=WfOW*jsP zF=xmA40m4vH6mB}WY{W=yYAGpr9CX>zlxxaf_3rHtPSFlzW6yiW~KzH<^Lzv3p$|i z4dJn|a}!wtYE33Vd5oQHV64wlrt|;{ghb^Am;JtO3N|@ex4)asIGrpUdz*DST16z6 zv;=k)a^Lg#qeL@x*>>Suq+Ci9t?@|Mj1GimV>NH*7Pc2qhS>zJ#SMoGusx1f8Po7H z{c5SF3fw${eebCy!25rjJoAoN=sXy7`sJVHn?Q+4ZE?zb?vNVllCf9|`*-{38ACDX z=2$F zXM#)Z7t7%^JT3yXza;8lBG7d?{kyqJ3)b5<`zM8Fq;x_CfS(&5e^+ZOPMH%wr85X% z`RqPlRJde@8miK#Y#V@=9gnYFG>T*ofJ&Zg3NK*T4=%!CCaQ)*m~p4mVZng?mTt@( zre5yPNy8$jN8sO33o%aKTi}7s2N<($FhBSc!V@)-<3ozns!wewP3X~juOM2Z@3ZX$ zBwayl<~Xlt2s>SP-pijsq2U>|6NUtsiQ-R`tjhOq%$1?}3C}d2`CYeo@B3>08s*tR z$7>q|`2;Sh%=dYJJkv0h?hgH`FaXU`T*Zh^)=R?w-7P2v zjaMm94IKR$Xf0a;xNjhhu;*_>N3o;4MWo1!H{d#(aC)J|BlPh6sa9CTGC3+QPK|UK zG${omjZMnwin%DsAZq}UO`Y}TI{1ul*1FhW!Y)(rxVNq7MHGr(mt(IQwOw@R`KQoe zte;Sy*ag_=rQ5V9g^ZIG!%=rUX%#@o5K9VuoRHA4PnM6+l zYdV^crllvI`M2JVo@?G~0Cl`-w>3lh$Fl#I18f_(26@)-gxsa?+*B zh&Ii^5ZM%L?e=r1={oi4x!7ITWQ6@Z=2-8Gi{F^@LHh4fz%s^&kt0 zJMiX5lARPJCZti)GjuyBjI%~r367^Tog}sxg|kMRp=Z<2?$-aXqt{ogRBU!_S0pSc zua<2k?j|xx6hK+YqGk^7wrdVTCW?7{ePQgU3=1A9h;ZKM!1|U4@FoM&)~7?D9FLIf z8k8Ivo*YGCCPjfKhy~fgay!`yJm}^&lpSP>g_SXxOd*ra)?PjSKLLl2L>`Pz`~L$% Tuk1Sr20+|hyn7|M#puy literal 0 HcmV?d00001 diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js index 7ca4bc55..2aaf2847 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js @@ -150,7 +150,7 @@ const AOI = (props) => { // mutateFetch(layer.aoiId); // console.log("Open in Editor") window.open( - `https://mapwith.ai/rapid#background=${ + `https://rapideditor.org//rapid#background=${ props.oamImagery ? "custom:" + props.oamImagery.url : "Bing" @@ -166,7 +166,7 @@ const AOI = (props) => { RapiD logo From 8a8cf8ec94e7d01db705efdf441c241b3cddd4ba Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Mon, 31 Jul 2023 13:58:28 +0200 Subject: [PATCH 07/12] typo fix --- frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js index 2aaf2847..b5b7b911 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/AOI.js @@ -150,7 +150,7 @@ const AOI = (props) => { // mutateFetch(layer.aoiId); // console.log("Open in Editor") window.open( - `https://rapideditor.org//rapid#background=${ + `https://rapideditor.org/rapid#background=${ props.oamImagery ? "custom:" + props.oamImagery.url : "Bing" From 898ca7a30234000dc27a3e4d3d4082d519d0ceca Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Mon, 31 Jul 2023 14:44:23 +0200 Subject: [PATCH 08/12] move functions to utiles --- .../TrainingDS/DatasetEditor/DatasetMap.js | 29 ++++--------------- frontend/src/utils.js | 28 ++++++++++++++++-- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js index db432b65..11ce4017 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetEditor/DatasetMap.js @@ -23,7 +23,11 @@ import { multiPolygon } from "@turf/helpers"; import axios from "../../../../axios"; -import { approximateGeom } from "../../../../utils"; +import { + approximateGeom, + converToGeoPolygon, + converToPolygon, +} from "../../../../utils"; import DatasetEditorHeader from "./DatasetEditorHeader"; import AuthContext from "../../../../Context/AuthContext"; const DatasetMap = (props) => { @@ -415,29 +419,6 @@ const DatasetMap = (props) => { const _onEditStop = (e) => { setIsEditing(false); }; - const converToPolygon = (layer) => { - const allPoly = []; - layer.forEach((element) => { - const x = element.latlngs.map((e) => [e.lat, e.lng]); - allPoly.push([x]); - }); - return allPoly; - }; - - const converToGeoPolygon = (layer) => { - if (layer.length === 0) return []; - const allPoly = converToPolygon(layer); - - // console.log("converToGeoPolygon",allPoly) - - const newAll = []; - allPoly.forEach((element) => { - const x = [...element[0], element[0][0]]; - - newAll.push([x]); - }); - return newAll; - }; const blueOptions = { color: "#03002e", width: 10, opacity: 1 }; diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 281356a0..f9a2b5c3 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -3,7 +3,7 @@ export const timeSince = (when, then) => { const seconds = (then.getTime() - when.getTime()) / 1000; if (seconds > 86400 * 30) - return " +" + ((seconds / (86400 * 30))).toFixed() + " month(s) ago"; + return " +" + (seconds / (86400 * 30)).toFixed() + " month(s) ago"; if (seconds > 604800) return " +" + (seconds / 604800).toFixed() + " week(s) ago"; if (seconds > 86400) @@ -164,4 +164,28 @@ export const timeSpan = (start, end) => { const delta = e - s; return (Math.abs(delta) / 36e5).toFixed(2); } else return ""; -}; \ No newline at end of file +}; + +export const converToPolygon = (layer) => { + const allPoly = []; + layer.forEach((element) => { + const x = element.latlngs.map((e) => [e.lat, e.lng]); + allPoly.push([x]); + }); + return allPoly; +}; + +export const converToGeoPolygon = (layer) => { + if (layer.length === 0) return []; + const allPoly = converToPolygon(layer); + + // console.log("converToGeoPolygon",allPoly) + + const newAll = []; + allPoly.forEach((element) => { + const x = [...element[0], element[0][0]]; + + newAll.push([x]); + }); + return newAll; +}; From 358a468b8c462c2e1922face430276925a06bcb7 Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Tue, 1 Aug 2023 11:54:32 +0200 Subject: [PATCH 09/12] submit feedback and saved to DB --- .../Start/Prediction/EditableGeoJSON.js | 264 ++++++++++-------- .../Layout/Start/Prediction/Prediction.js | 113 ++++---- 2 files changed, 212 insertions(+), 165 deletions(-) diff --git a/frontend/src/components/Layout/Start/Prediction/EditableGeoJSON.js b/frontend/src/components/Layout/Start/Prediction/EditableGeoJSON.js index b6eac26f..bfc1e18f 100644 --- a/frontend/src/components/Layout/Start/Prediction/EditableGeoJSON.js +++ b/frontend/src/components/Layout/Start/Prediction/EditableGeoJSON.js @@ -1,10 +1,14 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import L from "leaflet"; import { GeoJSON } from "react-leaflet"; import "@geoman-io/leaflet-geoman-free"; import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css"; - +import { useMutation } from "react-query"; +// import L from "leaflet"; +import AuthContext from "../../../../Context/AuthContext"; +import axios from "../../../../axios"; +import { converToGeoPolygon } from "../../../../utils"; function deg2tile(lat_deg, lon_deg, zoom) { const lat_rad = (Math.PI / 180) * lat_deg; const n = Math.pow(2, zoom); @@ -34,32 +38,32 @@ function tile2boundingbox(xtile, ytile, zoom) { return L.latLngBounds(cornerNW, cornerSE); } -function addTileBoundaryLayer( - mapref, - addedTiles, - tileX, - tileY, - zoom, - setAddedTiles, - tileBoundaryLayer -) { - const key = `${tileX}_${tileY}_${zoom}`; +// function addTileBoundaryLayer( +// mapref, +// addedTiles, +// tileX, +// tileY, +// zoom, +// setAddedTiles, +// tileBoundaryLayer +// ) { +// const key = `${tileX}_${tileY}_${zoom}`; - if (!addedTiles.has(key)) { - console.log("Key doesn't present in map"); - const bounds = tile2boundingbox(tileX, tileY, zoom); - tileBoundaryLayer = L.rectangle(bounds, { - color: "yellow", - fill: false, - pmIgnore: true, - }); - tileBoundaryLayer.name = "Tile Box"; - mapref.addLayer(tileBoundaryLayer); - mapref.fitBounds(tileBoundaryLayer.getBounds()); - addedTiles.add(key); - setAddedTiles(addedTiles); - } -} +// if (!addedTiles.has(key)) { +// console.log("Key doesn't present in map"); +// const bounds = tile2boundingbox(tileX, tileY, zoom); +// tileBoundaryLayer = L.rectangle(bounds, { +// color: "yellow", +// fill: false, +// pmIgnore: true, +// }); +// tileBoundaryLayer.name = "Tile Box"; +// mapref.addLayer(tileBoundaryLayer); +// mapref.fitBounds(tileBoundaryLayer.getBounds()); +// addedTiles.add(key); +// setAddedTiles(addedTiles); +// } +// } function getFeatureStyle(feature) { let color = "red"; @@ -84,6 +88,10 @@ const EditableGeoJSON = ({ setModifiedCount, setDeletedCount, tileBoundaryLayer, + modelId, + trainingId, + sourceImagery, + refestchFeeedback, }) => { const onPMCreate = (event) => { console.log("Created"); @@ -109,86 +117,131 @@ const EditableGeoJSON = ({ corner.lng, predictionZoomlevel ); - addTileBoundaryLayer( - mapref, - addedTiles, - tileX, - tileY, - predictionZoomlevel, - setAddedTiles - ); + // addTileBoundaryLayer( + // mapref, + // addedTiles, + // tileX, + // tileY, + // predictionZoomlevel, + // setAddedTiles + // ); } mapref.removeLayer(createdLayer); }; + const { accessToken } = useContext(AuthContext); + + const submitFeedback = async (layer) => { + try { + // console.log("layer", layer); + const newAOI = { + id: Math.random(), + latlngs: layer.getLatLngs()[0], + }; + const points = JSON.stringify( + converToGeoPolygon([newAOI])[0][0].reduce( + (p, c, i) => p + c[1] + " " + c[0] + ",", + "" + ) + ).slice(1, -2); + + const polygon = "SRID=4326;POLYGON((" + points + "))"; + + let body = { + geom: polygon, + zoom_level: predictionZoomlevel, + feedback_type: "TN", + source_imagery: sourceImagery, + training: trainingId, + comments: "comments is not support yet", + }; + + const headers = { + "access-token": accessToken, + }; + const res = await axios.post(`/feedback/`, body, { headers }); + console.log("res ", res); + refestchFeeedback(); + } catch (error) { + console.log("Error in submitting feedback", error); + } finally { + } + }; + const { mutate: mutateSubmitFeedback } = useMutation(submitFeedback); + const onEachFeature = (feature, layer) => { - layer.on({ - "pm:update": (event) => { - const bounds = event.layer.getBounds(); - const corners = [bounds.getSouthWest(), bounds.getNorthEast()]; + // layer.on({ + // "pm:update": (event) => { + // const bounds = event.layer.getBounds(); + // const corners = [bounds.getSouthWest(), bounds.getNorthEast()]; - for (const corner of corners) { - const [tileX, tileY] = deg2tile( - corner.lat, - corner.lng, - predictionZoomlevel - ); - addTileBoundaryLayer( - mapref, - addedTiles, - tileX, - tileY, - predictionZoomlevel, - setAddedTiles - ); - } + // for (const corner of corners) { + // const [tileX, tileY] = deg2tile( + // corner.lat, + // corner.lng, + // predictionZoomlevel + // ); + // // addTileBoundaryLayer( + // // mapref, + // // addedTiles, + // // tileX, + // // tileY, + // // predictionZoomlevel, + // // setAddedTiles + // // ); + // } - const editedLayer = event.target; - const editedData = editedLayer.toGeoJSON(); - const editedFeatureIndex = data.features.findIndex( - (feature) => feature.id === editedData.id - ); - const newData = { ...data }; - newData.features[editedFeatureIndex] = editedData; - setPredictions(newData); - if (feature.properties.action !== "MODIFY") { - feature.properties.action = "MODIFY"; - setModifiedCount((prevCount) => prevCount + 1); - } - }, - "pm:remove": (event) => { - const bounds = event.layer.getBounds(); - const corners = [bounds.getSouthWest(), bounds.getNorthEast()]; + // const editedLayer = event.target; + // const editedData = editedLayer.toGeoJSON(); + // const editedFeatureIndex = data.features.findIndex( + // (feature) => feature.id === editedData.id + // ); + // const newData = { ...data }; + // newData.features[editedFeatureIndex] = editedData; + // setPredictions(newData); + // if (feature.properties.action !== "MODIFY") { + // feature.properties.action = "MODIFY"; + // setModifiedCount((prevCount) => prevCount + 1); + // } + // }, + // "pm:remove": (event) => { + // const bounds = event.layer.getBounds(); + // const corners = [bounds.getSouthWest(), bounds.getNorthEast()]; - for (const corner of corners) { - const [tileX, tileY] = deg2tile( - corner.lat, - corner.lng, - predictionZoomlevel - ); - addTileBoundaryLayer( - mapref, - addedTiles, - tileX, - tileY, - predictionZoomlevel, - setAddedTiles - ); - } - const deletedLayer = event.layer; - const newFeatures = data.features.filter( - (feature) => - feature.properties.id !== deletedLayer.feature.properties.id - ); - setPredictions({ ...data, features: newFeatures }); - setDeletedCount((prevCount) => prevCount + 1); - }, - }); + // for (const corner of corners) { + // const [tileX, tileY] = deg2tile( + // corner.lat, + // corner.lng, + // predictionZoomlevel + // ); + // // addTileBoundaryLayer( + // // mapref, + // // addedTiles, + // // tileX, + // // tileY, + // // predictionZoomlevel, + // // setAddedTiles + // // ); + // } + // const deletedLayer = event.layer; + // const newFeatures = data.features.filter( + // (feature) => + // feature.properties.id !== deletedLayer.feature.properties.id + // ); + // setPredictions({ ...data, features: newFeatures }); + // setDeletedCount((prevCount) => prevCount + 1); + // }, + // }); layer.on("click", (e) => { console.log(e); if (feature.properties.action === "INITIAL") { const popupContent = `
- +

+ This feedback will be presented on the model (id: ${modelId}, training id: ${trainingId}) for improvements +

+ Comments: +
+
`; const popup = L.popup() @@ -200,23 +253,10 @@ const EditableGeoJSON = ({ .querySelector("#rightButton") .addEventListener("click", () => { feature.properties.action = "ACCEPT"; - const bounds = layer.getBounds(); - const corners = [bounds.getSouthWest(), bounds.getNorthEast()]; - for (const corner of corners) { - const [tileX, tileY] = deg2tile( - corner.lat, - corner.lng, - predictionZoomlevel - ); - addTileBoundaryLayer( - mapref, - addedTiles, - tileX, - tileY, - predictionZoomlevel, - setAddedTiles - ); - } + console.log("popup layer ", layer); + // handle submitting feedback + mutateSubmitFeedback(layer); + popup.close(); }); } }); diff --git a/frontend/src/components/Layout/Start/Prediction/Prediction.js b/frontend/src/components/Layout/Start/Prediction/Prediction.js index dbe74665..cd98a351 100644 --- a/frontend/src/components/Layout/Start/Prediction/Prediction.js +++ b/frontend/src/components/Layout/Start/Prediction/Prediction.js @@ -16,7 +16,7 @@ import { Link, Select, } from "@mui/material"; - +import L from "leaflet"; import React, { useContext, useEffect, useRef, useState } from "react"; import { FeatureGroup, @@ -25,7 +25,6 @@ import { TileLayer, useMapEvents, } from "react-leaflet"; -import L from "leaflet"; import { useMutation, useQuery } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; import axios from "../../../../axios"; @@ -56,9 +55,9 @@ const Prediction = () => { const [apiCallInProgress, setApiCallInProgress] = useState(false); const [confidence, setConfidence] = useState(90); const [totalPredictionsCount, settotalPredictionsCount] = useState(0); - const [DeletedCount, setDeletedCount] = useState(0); - const [CreatedCount, setCreatedCount] = useState(0); - const [ModifiedCount, setModifiedCount] = useState(0); + // const [DeletedCount, setDeletedCount] = useState(0); + // const [CreatedCount, setCreatedCount] = useState(0); + // const [ModifiedCount, setModifiedCount] = useState(0); const [map, setMap] = useState(null); const [zoom, setZoom] = useState(15); const [responseTime, setResponseTime] = useState(0); @@ -232,9 +231,7 @@ const Prediction = () => { const updatedPredictions = addIdsToPredictions(res.data); setPredictions(updatedPredictions); settotalPredictionsCount(updatedPredictions.features.length); - setCreatedCount(0); - setModifiedCount(0); - setDeletedCount(0); + if (addedTiles.size > 0) { console.log("Map has tileboundarylayer"); } @@ -359,7 +356,37 @@ const Prediction = () => { return { ...predictions, features }; } const navigate = useNavigate(); + const getFeedback = async (trainingId) => { + if (!modelInfo || !modelInfo.trainingId) return; + try { + const headers = { + "access-token": accessToken, + }; + const res = await axios.get( + `/feedback/?training=${modelInfo.trainingId}`, + null, + { + headers, + } + ); + if (res.error) { + } else { + console.log("getFeedback ", res.data); + return res.data; + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const { data: feedbackData, refetch: refetchFeedback } = useQuery( + "getfeedback" + (modelInfo && modelInfo.trainingId), + getFeedback, + { + refetchInterval: 120000, + } + ); return ( <> @@ -386,18 +413,24 @@ const Prediction = () => { )} - {predictions && ( + {predictions && dataset && ( { + refetchFeedback(); + }} /> )} @@ -453,48 +486,22 @@ const Prediction = () => { Response: {responseTime} sec - {predictions && ( - - - Feedback - + + + + Feedback + + + Initial Predictions: + {totalPredictionsCount} + + {feedbackData && feedbackData.features && ( - Initial Predictions: - {totalPredictionsCount} + Total feedbacks count:{feedbackData.features.length} - {CreatedCount > 0 && ( - - Total Created: - {CreatedCount} - - )} - {ModifiedCount > 0 && ( - - Total Modified: - {ModifiedCount} - - )} - {DeletedCount > 0 && ( - - Total Deleted: - {DeletedCount} - - )} - {CreatedCount + ModifiedCount + DeletedCount > 1 && - !feedbackSubmitted && ( - - Submit my feedback - - )} - - )} + )} + + {loading ? ( From b2ec8829edfffd302c34a56c474cdf1cbcb0ce23 Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Tue, 1 Aug 2023 13:58:28 +0200 Subject: [PATCH 10/12] showing feedback for each trining --- .../Layout/AIModels/AIModelEditor/Feedback.js | 60 +++++++++++++++++++ .../AIModels/AIModelEditor/Trainings.js | 27 +++++---- 2 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js new file mode 100644 index 00000000..353b0104 --- /dev/null +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js @@ -0,0 +1,60 @@ +import { IconButton, Tooltip } from "@mui/material"; +import React, { useContext } from "react"; +import InfoIcon from "@mui/icons-material/Info"; +import FeedbackIcon from "@mui/icons-material/Feedback"; +import { useQuery } from "react-query"; +import axios from "../../../../axios"; +import AuthContext from "../../../../Context/AuthContext"; +import FindReplaceIcon from "@mui/icons-material/FindReplace"; +const Feedback = ({ trainingId }) => { + const { accessToken } = useContext(AuthContext); + 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); + return res.data; + } + } catch (e) { + console.log("isError", e); + } finally { + } + }; + const { data: feedbackData, isLoading } = useQuery( + "getFeedback" + trainingId, + getFeedback, + { + refetchInterval: 10000, + } + ); + + return ( + <> + {feedbackData && + feedbackData.features && + feedbackData.features.length > 0 && ( + + + + + + )} + {isLoading && ( + + + + )} + + ); +}; +export default Feedback; diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js index f6526afb..49515c33 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js @@ -20,6 +20,7 @@ import Popup from "./Popup"; import InfoIcon from "@mui/icons-material/Info"; import AuthContext from "../../../../Context/AuthContext"; +import Feedback from "./Feedback"; const DEFAULT_FILTER = { items: [{ columnField: "created_date", id: 8537, operatorValue: "contains" }], @@ -151,16 +152,22 @@ const TrainingsList = (props) => { }, { field: "popup", - headerName: "Info", - width: 10, - renderCell: (params) => ( - handlePopupOpen(params.row)} - aria-label="popup" - > - - - ), + headerName: "Info/Feedback", + width: 100, + renderCell: (params) => { + console.log("params in info row", params); + return ( +
+ handlePopupOpen(params.row)} + aria-label="popup" + > + + + +
+ ); + }, }, { field: "accuracy", From fb5e1fee9625868ff073d5c0ff101afb9f6ae68d Mon Sep 17 00:00:00 2001 From: Omran NAJJAR Date: Tue, 1 Aug 2023 14:30:48 +0200 Subject: [PATCH 11/12] no need for logs --- .../src/components/Layout/AIModels/AIModelEditor/Trainings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js index 49515c33..4c160786 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js @@ -155,7 +155,7 @@ const TrainingsList = (props) => { headerName: "Info/Feedback", width: 100, renderCell: (params) => { - console.log("params in info row", params); + // console.log("params in info row", params); return (
Date: Tue, 1 Aug 2023 14:37:13 +0200 Subject: [PATCH 12/12] less logs --- .../src/components/Layout/AIModels/AIModelEditor/Feedback.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js index 353b0104..709bd4a9 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Feedback.js @@ -19,7 +19,7 @@ const Feedback = ({ trainingId }) => { if (res.error) { } else { - console.log(`/feedback/?training=${trainingId}`, res.data); + // console.log(`/feedback/?training=${trainingId}`, res.data); return res.data; } } catch (e) {