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 0000000000..185d0d0cef Binary files /dev/null and b/src/mapper/src/assets/images/location.png differ 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 2c069f9809..053bf55e27 100644 --- a/src/mapper/src/lib/components/map/main.svelte +++ b/src/mapper/src/lib/components/map/main.svelte @@ -75,8 +75,6 @@ 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 = $state(null); // Trigger adding the PMTiles layer to baselayers, if PmtilesUrl is set let allBaseLayers: maplibregl.StyleSpecification[] = $derived( @@ -151,13 +149,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 +281,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 +324,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