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`Kc$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