From 3e418b51851d988c01213de785a43e286d39331e Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:41:59 +0545 Subject: [PATCH] Mapper Frontend: routing to feature (#1963) * feat(entities): store selected entity coordinate to store * feat(maplibre-gl-directions): package add for maplibre routing feature * feat(main): store clicked entity coordinate to store * feat(geolocation): route path add from user location to selected entity * fix(icon): icon add * fix(button): add css for button type link * feat(dialog-entities-actions): navigate btn add, other button syling update * fix(geolocation): ts type add * feat(entities): entityToNavigate & toggleGeolocation state add to store * fix(dialog-entities-actions): navigate to entity function trigger on btn click * refactor(main): geolocation status handle on child component, select payload for setSelectedEntityCoordinate action * fix(geolocation): handle toggle geolocation on geolocation component itself * feat(location): location img add * fix(main): upate entity fill-color * feat(maplibre-directions): custom styling for direction routes * fix(geolocation): load destination location image, custom layer style load * fix(entities): turn off geolocation by default * feat(geolocation): show tooltip popup to users indicating geolocation feature to be turned on * fix(main): remove zoom buttons from map --- src/mapper/package.json | 1 + src/mapper/pnpm-lock.yaml | 32 +++ src/mapper/src/assets/images/location.png | Bin 0 -> 2233 bytes src/mapper/src/assets/maplibre-directions.ts | 68 ++++++ .../components/dialog-entities-actions.svelte | 75 +++++-- .../src/lib/components/map/geolocation.svelte | 195 +++++++++++++++--- src/mapper/src/lib/components/map/main.svelte | 72 +++---- src/mapper/src/store/entities.svelte.ts | 32 +++ src/mapper/src/styles/button.css | 14 ++ .../static/assets/icons/arrow-repeat.svg | 4 + src/mapper/static/assets/icons/direction.svg | 4 + 11 files changed, 398 insertions(+), 99 deletions(-) create mode 100644 src/mapper/src/assets/images/location.png create mode 100644 src/mapper/src/assets/maplibre-directions.ts create mode 100644 src/mapper/static/assets/icons/arrow-repeat.svg create mode 100644 src/mapper/static/assets/icons/direction.svg diff --git a/src/mapper/package.json b/src/mapper/package.json index e1e2839b9c..3793f036b2 100644 --- a/src/mapper/package.json +++ b/src/mapper/package.json @@ -45,6 +45,7 @@ "@electric-sql/client": "^0.8.0", "@electric-sql/pglite": "^0.2.14", "@hotosm/ui": "0.2.0-b5", + "@maplibre/maplibre-gl-directions": "^0.7.1", "@tiptap/core": "^2.10.3", "@tiptap/pm": "^2.10.3", "@tiptap/starter-kit": "^2.10.3", diff --git a/src/mapper/pnpm-lock.yaml b/src/mapper/pnpm-lock.yaml index 48b13effbd..673743a220 100644 --- a/src/mapper/pnpm-lock.yaml +++ b/src/mapper/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@hotosm/ui': specifier: 0.2.0-b5 version: 0.2.0-b5(@types/react@18.3.3) + '@maplibre/maplibre-gl-directions': + specifier: ^0.7.1 + version: 0.7.1(@types/geojson@7946.0.14)(maplibre-gl@4.7.1) '@shoelace-style/shoelace': specifier: ^2.15.1 version: 2.18.0(@types/react@18.3.3) @@ -1079,6 +1082,11 @@ packages: resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} engines: {node: '>=6.0.0'} + '@maplibre/maplibre-gl-directions@0.7.1': + resolution: {integrity: sha512-n2dQkMM1+LO77bnVXXp+rZwD1OVa0UNbP3mgGyDxE6NLFEPU/p+w3G/pxnDfwuapzyHYeevdpYpxrVbwABU4BQ==} + peerDependencies: + maplibre-gl: ^4.0.0 + '@maplibre/maplibre-gl-style-spec@20.4.0': resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} hasBin: true @@ -1098,6 +1106,11 @@ packages: '@petamoriken/float16@3.9.0': resolution: {integrity: sha512-rYUZ+VFjPHD0NT2JYKj64NxXxrV642IiyaUxxorTEj0S3hT7B5Ixezyc9Fn+XvSk0ETEBp5VWjGIErzh0ug0Xw==} + '@placemarkio/polyline@1.2.0': + resolution: {integrity: sha512-PjXntwUKQFTM/MgXIZHBOtuU2rAkmPgfrIxweOJEf1vyytQJNLDMI4YIRO3LUkt++F4TyAQHjPoRsteYa+gtVQ==} + peerDependencies: + '@types/geojson': '*' + '@playwright/test@1.49.0': resolution: {integrity: sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==} engines: {node: '>=18'} @@ -2830,6 +2843,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4723,6 +4741,14 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} + '@maplibre/maplibre-gl-directions@0.7.1(@types/geojson@7946.0.14)(maplibre-gl@4.7.1)': + dependencies: + '@placemarkio/polyline': 1.2.0(@types/geojson@7946.0.14) + maplibre-gl: 4.7.1 + nanoid: 5.0.9 + transitivePeerDependencies: + - '@types/geojson' + '@maplibre/maplibre-gl-style-spec@20.4.0': dependencies: '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -4748,6 +4774,10 @@ snapshots: '@petamoriken/float16@3.9.0': optional: true + '@placemarkio/polyline@1.2.0(@types/geojson@7946.0.14)': + dependencies: + '@types/geojson': 7946.0.14 + '@playwright/test@1.49.0': dependencies: playwright: 1.49.0 @@ -6655,6 +6685,8 @@ snapshots: nanoid@3.3.8: {} + nanoid@5.0.9: {} + natural-compare@1.4.0: {} node-fetch-native@1.6.4: {} diff --git a/src/mapper/src/assets/images/location.png b/src/mapper/src/assets/images/location.png new file mode 100644 index 0000000000000000000000000000000000000000..185d0d0cef05043bfd269e18f4559aa80611e240 GIT binary patch literal 2233 zcmV;q2uAmbP)`|L4wZ>?#SU6;J_nKoyR6-87}j!w$CB-X;S2sR7zV0+kO{Lt62YK&W3@galL~ z5~YeDMNL0|=!cf3kXWh;Es5859DALlZPL__q~%dsm571~as1ewnZvmoT2)aKdmeXo z*1JE++R=Jv_ul{9x%b?2&b$`AwzpxKncXtQk7~Ix0j( zgr4kNf4yf}-xd*eiogwshZ1>sf-Lo5OMZ9)F%zrMCqBD&c|)=B0rv1+xwUOn0%k{k zdyhSb2bv3o{~{)06M8(?_9gbb-30Y9Bmmu|NbSGb+CTbx#DH<3&)j-zgERB*U2^y> zBoOINezV+BJDY}wXJHVdLXX|@nNKLk-Oq$iBhm1S4)bj$%|c^BpU$>tJmNo!iPePa z;4Nw0pHB}KPQm~R26!dc-sX~bxQay9e3TXUO?GYC2m?gkqF>2vxIuX|AYXkAst{(J zQ;clXEjv=7&-Aoi=P(UPaaW;=DO)7A-O+mJ$lnkVk#!wq>w>wr+C|osXQ{5!`+Hdg{|qn!U2RNw9peB9Vk(T zH7Ro3dk~O7LW>VW$?WONs4*CPr;cwT0D**_$hK|KqcLg>ZC#DM{ns7@AXwL-qt!JE zxmKXM2kbid7R1YQOtgcPn=UrYuF!vqR7R@(HL>KMM~n~!tGjQjL#GH>wjG&Q>q^L2 zFew*x#|{0XgID_8Saw5}NbQxw)rbL`(A1nSyy52@-!0m*tq#NibN@Wew=X2Jrgh-x zK*>$D%i#cGpyZKn4SWl&)3jh1-IC{2aqG?bm;STZbJiX{JSAdpm9x*lFyd(Pt$}Z$ zbuJtXBcuC0enfr;Ew-4Qcf&L;`|hFwv}iQLDBkm#{74=F(cK6!9fU~;=7p9fZ8R3h zOiv;tGJyUT24V8Afq$WiR=^<2@(v>;N-490B4Z_d5OvZ(lLR~L?_0uOiJFi~sIa*|U znYTamimpY#3ceE{eG6T*`2{ZfeT1u=Is~Z zlR#1M@od{MIddIiLi;ZCwAum<7FA<8D}R|c*W`k`q#Szij4=>FM>G8+zH_JpCaxKWY?uTc{znjoo-RFt5QGw1UtzIvzlSA`P4t~GtyvS=L7pBM=@pZ9 zDVuxyTKY$xMMxrvL=ZVUWMKRUmL6}x=D?}C?W{dBR$p;b(T#j+5yD|qy z|Ah#MEVkC#H~Nfx*^jV9knZpY>HP2?5D}4j45Ss%c=yI92xcQ3G4Q)|f8lnD@D7bM zTC59JYM8rAiZ_BPMNGM&3)9Y>6md6b#0U1;mUSO27VU#^x>6(JM9EF%bPgXP#+bA5 zOjp;3OKSd4dQCN|gAMfaxjg~t&Su}9v@3-vuq=Rn_t$}q;{Zg0&WJB1iA-gR6` zy6@OzBVDBz_QW2c(VGd1jl3alk;}i03NqHc@e9OwM7&bh!X)DGV#W8i=0_{$WsJQ= zzZbnP=$tC1= zwe|e}x12u^ez!621&6UUNn6k-ZjXPRMe4D2M2Z$@nVnt?YLU}va+Vi_iuTd(hZe{s z-X`JwdH6)mnX?pcPnyV@#0jlglRx^BOl*DzpGZXHp_aZ;<7ve~NTkpja_tA9V|I&$ z`K&#c$Z39=#0s4r8agjQyG8Fe$4cEF=brt3NvzPCvuOO)V$#korfZdcjRccup>z4X z1L6fUgv3Gg^RNjnrb)EW8b$7-+M2lB{~0_Bi}xboBwpyUsYd9%-2VxDz_imo3KB20 zhFD#8YY7G!-7+F7szT@$C3;rxy?^QZ`n{D!E3zaPlT?M!`e8IO;5~fl`$Txi^n+@m zB9YHCd_VLyofF+{_e+7xdnS3n-b{bt$4PjEUY$C1>KNjGG(7>1ow_3K00000NkvXX Hu0mjfi?uU5 literal 0 HcmV?d00001 diff --git a/src/mapper/src/assets/maplibre-directions.ts b/src/mapper/src/assets/maplibre-directions.ts new file mode 100644 index 0000000000..8338ffb6ed --- /dev/null +++ b/src/mapper/src/assets/maplibre-directions.ts @@ -0,0 +1,68 @@ +// snapline: line from waypoint to road +export const layers = [ + { + id: 'maplibre-gl-directions-snapline', + type: 'line', + source: 'maplibre-gl-directions', + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-dasharray': [2, 2], + 'line-color': '#000000', + 'line-opacity': 0.65, + 'line-width': 2, + }, + filter: ['==', ['get', 'type'], 'SNAPLINE'], + }, + + // primary route + { + id: 'maplibre-gl-directions-routeline', + type: 'line', + source: 'maplibre-gl-directions', + layout: { + 'line-cap': 'butt', + 'line-join': 'round', + }, + paint: { + 'line-width': 12, + 'line-opacity': 1, + 'line-color': '#4285f4', + }, + filter: ['==', ['get', 'route'], 'SELECTED'], + }, + + // alternative route + { + id: 'maplibre-gl-directions-alt-routeline', + type: 'line', + source: 'maplibre-gl-directions', + layout: { + 'line-cap': 'butt', + 'line-join': 'round', + }, + paint: { + 'line-width': 12, + 'line-opacity': 0.8, + 'line-color': '#547fff', + }, + filter: ['==', ['get', 'route'], 'ALT'], + }, + + // only apply icon-style to the destination waypoint as we have icon for the origin + { + id: 'maplibre-gl-directions-waypoint', + type: 'symbol', + source: 'maplibre-gl-directions', + layout: { + 'icon-image': 'location', + 'icon-anchor': 'bottom', + 'icon-ignore-placement': true, + 'icon-overlap': 'always', + 'icon-size': 0.5, + }, + filter: ['all', ['==', ['get', 'type'], 'WAYPOINT'], ['==', ['get', 'category'], 'DESTINATION']], + }, +]; diff --git a/src/mapper/src/lib/components/dialog-entities-actions.svelte b/src/mapper/src/lib/components/dialog-entities-actions.svelte index 93701eb5b4..fad23fe75d 100644 --- a/src/mapper/src/lib/components/dialog-entities-actions.svelte +++ b/src/mapper/src/lib/components/dialog-entities-actions.svelte @@ -19,6 +19,8 @@ const selectedEntity = $derived( entitiesStore.entitiesStatusList?.find((entity) => entity.osmid === selectedEntityOsmId), ); + const selectedEntityCoordinate = $derived(entitiesStore.selectedEntityCoordinate); + const entityToNavigate = $derived(entitiesStore.entityToNavigate); const mapFeature = () => { const xformId = projectData?.odk_form_id; @@ -43,6 +45,13 @@ alertStore.setAlert({ message: 'Requires a mobile phone with ODK Collect.', variant: 'warning' }); } }; + + const navigateToEntity = () => { + if (!entitiesStore.toggleGeolocation) { + entitiesStore.setToggleGeolocation(true); + } + entitiesStore.setEntityToNavigate(selectedEntityCoordinate); + }; {#if isTaskActionModalOpen && selectedTab === 'map' && selectedEntity} @@ -68,7 +77,6 @@

Feature {selectedEntity?.osmid}

{ await entitiesStore.syncEntityStatus(projectData?.id); }} @@ -78,8 +86,14 @@ role="button" tabindex="0" size="small" - class="secondary w-fit ml-auto" + class="link w-fit ml-auto" + disabled={entitiesStore.syncEntityStatusLoading} > + SYNC STATUS
@@ -92,24 +106,47 @@

{#if selectedEntity?.status !== 'SURVEY_SUBMITTED'} - { - mapFeature(); - }} - onkeydown={(e: KeyboardEvent) => { - if (e.key === 'Enter') { +
+ { + navigateToEntity(); + }} + onkeydown={(e: KeyboardEvent) => { + if (e.key === 'Enter') { + navigateToEntity(); + } + }} + role="button" + tabindex="0" + > + + NAVIGATE HERE + + { mapFeature(); - } - }} - role="button" - tabindex="0" - > - MAP FEATURE IN ODK - + }} + onkeydown={(e: KeyboardEvent) => { + if (e.key === 'Enter') { + mapFeature(); + } + }} + role="button" + tabindex="0" + > + + MAP FEATURE IN ODK + +
{/if} diff --git a/src/mapper/src/lib/components/map/geolocation.svelte b/src/mapper/src/lib/components/map/geolocation.svelte index 8f9ab11c9e..7fdc3ef67f 100644 --- a/src/mapper/src/lib/components/map/geolocation.svelte +++ b/src/mapper/src/lib/components/map/geolocation.svelte @@ -1,25 +1,88 @@ - - - + + + tooltipRef.hide()} + onkeydown={(e: KeyboardEvent) => { + e.key === 'Enter' && tooltipRef.hide(); + }} + role="button" + tabindex="0" + > +
+ For the best experience, turn on location + +
+ { + entitiesStore.setToggleGeolocation(!entitiesStore.toggleGeolocation); + if (!entitiesStore.toggleGeolocation) { + exitNavigationMode(); + } + }} + > + + +
+
+
+ +{#if entitiesStore.toggleGeolocation} + + + +{/if} + +{#if entitiesStore.toggleGeolocation && entityToNavigate} +
+
+

Distance: {entityDistance}m

+ { + e.key === 'Enter' && exitNavigationMode(); + }} + role="button" + tabindex="0" + size="small" + class="secondary" + disabled={entitiesStore.syncEntityStatusLoading} + > + Exit Navigation + +
+
+{/if} diff --git a/src/mapper/src/lib/components/map/main.svelte b/src/mapper/src/lib/components/map/main.svelte index 69dd581b25..24444c844f 100644 --- a/src/mapper/src/lib/components/map/main.svelte +++ b/src/mapper/src/lib/components/map/main.svelte @@ -75,9 +75,8 @@ let loaded: boolean = $state(false); let selectedBaselayer: string = $state('OSM'); let taskAreaClicked: boolean = $state(false); - let toggleGeolocationStatus: boolean = $state(false); - let toggleNavigationMode: boolean = $state(false); let projectSetupStep: number | null = $state(null); + // Trigger adding the PMTiles layer to baselayers, if PmtilesUrl is set let allBaseLayers: maplibregl.StyleSpecification[] = $derived( projectBasemapStore.projectPmtilesUrl @@ -151,13 +150,17 @@ const clickedTaskFeature = map?.queryRenderedFeatures(e.point, { layers: ['task-fill-layer'], }); - // if clicked point contains entity then set it's osm id else set null to store if (clickedEntityFeature && clickedEntityFeature?.length > 0) { const clickedEntityId = clickedEntityFeature[0]?.properties?.osm_id; entitiesStore.setSelectedEntity(clickedEntityId); + entitiesStore.setSelectedEntityCoordinate({ + entityId: clickedEntityId, + coordinate: [e.lngLat.lng, e.lngLat.lat], + }); } else { entitiesStore.setSelectedEntity(null); + entitiesStore.setSelectedEntityCoordinate(null); } // if clicked point contains task layer @@ -279,20 +282,6 @@ } } - // if navigation mode on, tilt map by 50 degrees - $effect(() => { - if (toggleNavigationMode && toggleGeolocationStatus) { - map?.setPitch(50); - } else { - map?.setPitch(0); - } - }); - - // if map loaded, turn on geolocation by default - $effect(() => { - if (loaded) toggleGeolocationStatus = true; - }); - onMount(async () => { // Register pmtiles protocol if (!maplibre.config.REGISTERED_PROTOCOLS.hasOwnProperty('pmtiles')) { @@ -336,22 +325,8 @@ ]} > - + - - - { - toggleGeolocationStatus = !toggleGeolocationStatus; - toggleNavigationMode = false; - }} - > - - - - (toggleNavigationMode = !toggleNavigationMode)} - > - - {#if toggleGeolocationStatus} - - {/if} + + + + \ No newline at end of file diff --git a/src/mapper/static/assets/icons/direction.svg b/src/mapper/static/assets/icons/direction.svg new file mode 100644 index 0000000000..956614aed0 --- /dev/null +++ b/src/mapper/static/assets/icons/direction.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file