Skip to content

Commit

Permalink
feat(mapper): add navigation mode to map (pitch angle / rotate) (#1973)
Browse files Browse the repository at this point in the history
* feat(assets): icon, image add

* feat(common): getCommonStore add

* fix(+page): manage selectedTab state on store

* feat(main): navigation mode integration

* feat(geolocation): navigation mode integration
  • Loading branch information
NSUWAL123 authored Dec 10, 2024
1 parent 3201089 commit 4af490f
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 32 deletions.
Binary file added src/mapper/src/assets/images/arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 36 additions & 7 deletions src/mapper/src/lib/components/map/geolocation.svelte
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
<script lang="ts">
import { GeoJSON, SymbolLayer } from 'svelte-maplibre';
import { GeoJSON, SymbolLayer, type LngLatLike } from 'svelte-maplibre';
import type { FeatureCollection } from 'geojson';
import { GetDeviceRotation } from '$lib/utils/getDeviceRotation';
import { getAlertStore } from '$store/common.svelte.ts';
const alertStore = getAlertStore();
import { getCommonStore } from '$store/common.svelte.ts';
interface Props {
map: maplibregl.Map | undefined;
toggleGeolocationStatus?: boolean;
toggleGeolocationStatus: boolean;
toggleNavigationMode: boolean;
}
let { map = $bindable(), toggleGeolocationStatus = $bindable(false) }: Props = $props();
const alertStore = getAlertStore();
const commonStore = getCommonStore();
let { map, toggleGeolocationStatus, toggleNavigationMode }: Props = $props();
let coords: [number, number] | undefined = $state();
let coords: LngLatLike | undefined = $state();
let rotationDeg: number | undefined = $state();
let watchId: number | undefined = $state();
// if bottom sheet is open, add bottom padding for better visibility of location arrow
$effect(() => {
if (commonStore.selectedTab === 'map') {
map?.setPadding({ bottom: 0, top: 0, left: 0, right: 0 });
} else {
map?.setPadding({ bottom: 300, top: 0, left: 0, right: 0 });
}
});
// zoom to user's current location
$effect(() => {
if (toggleNavigationMode) {
map?.setCenter(coords as LngLatLike);
map?.setZoom(18);
}
});
$effect(() => {
if (map && toggleGeolocationStatus) {
// zoom to user's current location
Expand All @@ -34,6 +54,10 @@
watchId = navigator.geolocation.watchPosition(
function (pos) {
coords = [pos.coords.longitude, pos.coords.latitude];
if (toggleNavigationMode) {
// if user is in navigation mode, update the map center according to user's live location since swiping map isn't possible
map?.setCenter(coords);
}
},
function (error) {
alert(`ERROR: ${error.message}`);
Expand Down Expand Up @@ -91,6 +115,9 @@
});
sensor.addEventListener('reading', (event: Event) => {
rotationDeg = GetDeviceRotation(sensor.quaternion);
// rotate map according to device orientation
if (toggleNavigationMode) map.rotateTo(rotationDeg || 0, { duration: 0 });
});
Promise.all([
Expand All @@ -114,7 +141,9 @@
hoverCursor="pointer"
layout={{
// if orientation true (meaning the browser supports device orientation sensor show location dot with orientation sign)
'icon-image': ['case', ['==', ['get', 'orientation'], true], 'locationArc', 'locationDot'],
'icon-image': !toggleNavigationMode
? ['case', ['==', ['get', 'orientation'], true], 'locationArc', 'locationDot']
: ['case', ['==', ['get', 'orientation'], true], 'arrow', 'locationDot'],
'icon-allow-overlap': true,
'text-offset': [0, -2],
'text-size': 12,
Expand Down
69 changes: 54 additions & 15 deletions src/mapper/src/lib/components/map/main.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import '$styles/page.css';
import '$styles/button.css';
import '@watergis/maplibre-gl-terradraw/dist/maplibre-gl-terradraw.css'
import '@watergis/maplibre-gl-terradraw/dist/maplibre-gl-terradraw.css';
import '@hotosm/ui/dist/hotosm-ui';
import { onMount } from 'svelte';
import {
Expand All @@ -18,7 +18,7 @@
ControlButton,
} from 'svelte-maplibre';
import maplibre from 'maplibre-gl';
import MaplibreTerradrawControl from '@watergis/maplibre-gl-terradraw'
import MaplibreTerradrawControl from '@watergis/maplibre-gl-terradraw';
import { Protocol } from 'pmtiles';
import { polygon } from '@turf/helpers';
import { buffer } from '@turf/buffer';
Expand All @@ -29,6 +29,7 @@
import LocationDotImg from '$assets/images/locationDot.png';
import BlackLockImg from '$assets/images/black-lock.png';
import RedLockImg from '$assets/images/red-lock.png';
import Arrow from '$assets/images/arrow.png';
import Legend from '$lib/components/map/legend.svelte';
import LayerSwitcher from '$lib/components/map/layer-switcher.svelte';
Expand All @@ -55,7 +56,15 @@
handleDrawnGeom?: ((geojson: GeoJSONGeometry) => void) | null;
}
let { projectOutlineCoords, entitiesUrl, toggleActionModal, projectId, setMapRef, draw = false, handleDrawnGeom }: Props = $props();
let {
projectOutlineCoords,
entitiesUrl,
toggleActionModal,
projectId,
setMapRef,
draw = false,
handleDrawnGeom,
}: Props = $props();
const taskStore = getTaskStore();
const projectSetupStepStore = getProjectSetupStepStore();
Expand All @@ -67,6 +76,7 @@
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(
Expand Down Expand Up @@ -183,18 +193,18 @@
// Workaround due to bug in @watergis/mapbox-gl-terradraw
function removeTerraDrawLayers() {
if (map) {
if (map.getLayer('td-point')) map.removeLayer('td-point');
if (map.getSource('td-point')) map.removeSource('td-point');
if (map.getLayer('td-point')) map.removeLayer('td-point');
if (map.getSource('td-point')) map.removeSource('td-point');
if (map.getLayer('td-linestring')) map.removeLayer('td-linestring');
if (map.getSource('td-linestring')) map.removeSource('td-linestring');
if (map.getLayer('td-linestring')) map.removeLayer('td-linestring');
if (map.getSource('td-linestring')) map.removeSource('td-linestring');
if (map.getLayer('td-polygon')) map.removeLayer('td-polygon');
if (map.getSource('td-polygon')) map.removeSource('td-polygon');
if (map.getLayer('td-polygon')) map.removeLayer('td-polygon');
if (map.getSource('td-polygon')) map.removeSource('td-polygon');
if (map.getLayer('td-polygon-outline')) map.removeLayer('td-polygon-outline');
if (map.getSource('td-polygon-outline')) map.removeSource('td-polygon-outline');
}
if (map.getLayer('td-polygon-outline')) map.removeLayer('td-polygon-outline');
if (map.getSource('td-polygon-outline')) map.removeSource('td-polygon-outline');
}
}
// Add draw layer & handle emitted geom
$effect(() => {
Expand Down Expand Up @@ -224,7 +234,7 @@
handleDrawnGeom(firstGeom);
}
});
};
}
} else {
removeTerraDrawLayers();
map?.removeControl(drawControl);
Expand Down Expand Up @@ -269,6 +279,20 @@
}
}
// 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')) {
Expand Down Expand Up @@ -308,14 +332,19 @@
{ id: 'LOCKED_FOR_VALIDATION', url: RedLockImg },
{ id: 'locationArc', url: LocationArcImg },
{ id: 'locationDot', url: LocationDotImg },
{ id: 'arrow', url: Arrow },
]}
>
<!-- Controls -->
<NavigationControl position="top-left" />
<ScaleControl />
<Control class="flex flex-col gap-y-2" position="top-left">
<ControlGroup>
<ControlButton on:click={() => (toggleGeolocationStatus = !toggleGeolocationStatus)}
<ControlButton
on:click={() => {
toggleGeolocationStatus = !toggleGeolocationStatus;
toggleNavigationMode = false;
}}
><hot-icon
name="geolocate"
class={`!text-[1.2rem] cursor-pointer duration-200 ${toggleGeolocationStatus ? 'text-red-600' : 'text-[#52525B]'}`}
Expand All @@ -331,6 +360,16 @@
>
</ControlGroup></Control
>
<Control class="flex flex-col gap-y-2" position="top-left">
<ControlGroup>
<ControlButton on:click={() => (toggleNavigationMode = !toggleNavigationMode)}
><hot-icon
name="send"
class={`!text-[1.2rem] cursor-pointer duration-200 ${toggleNavigationMode ? 'text-red-600' : 'text-[#52525B]'}`}
></hot-icon></ControlButton
>
</ControlGroup></Control
>
<Control class="flex flex-col gap-y-2" position="bottom-right">
<LayerSwitcher
{map}
Expand All @@ -342,7 +381,7 @@
</Control>
<!-- Add the Geolocation GeoJSON layer to the map -->
{#if toggleGeolocationStatus}
<Geolocation bind:map bind:toggleGeolocationStatus></Geolocation>
<Geolocation {map} {toggleGeolocationStatus} {toggleNavigationMode}></Geolocation>
{/if}
<!-- The task area geojson -->
<GeoJSON id="tasks" data={taskStore.featcol} promoteId="fid">
Expand Down
19 changes: 10 additions & 9 deletions src/mapper/src/routes/[projectId]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import { getTaskStore, getTaskEventStream } from '$store/tasks.svelte.ts';
import { getEntitiesStatusStore, getEntityStatusStream } from '$store/entities.svelte.ts';
import More from '$lib/components/more/index.svelte';
import { getProjectSetupStepStore } from '$store/common.svelte.ts';
import { getProjectSetupStepStore, getCommonStore } from '$store/common.svelte.ts';
import { projectSetupStep as projectSetupStepEnum } from '$constants/enums.ts';
interface Props {
Expand All @@ -34,14 +34,15 @@
let maplibreMap: maplibregl.Map | undefined = $state(undefined);
let tabGroup: SlTabGroup;
let selectedTab: string = $state('map');
let openedActionModal: 'entity-modal' | 'task-modal' | null = $state(null);
let isTaskActionModalOpen: boolean = $state(false);
let infoDialogRef: SlDialog | null = $state(null);
let isDrawEnabled: boolean = $state(false);
const taskStore = getTaskStore();
const entitiesStore = getEntitiesStatusStore();
const commonStore = getCommonStore();
const taskEventStream = getTaskEventStream(data.projectId);
const entityStatusStream = getEntityStatusStream(data.projectId);
Expand Down Expand Up @@ -136,7 +137,7 @@
toggleTaskActionModal={(value) => {
openedActionModal = value ? 'task-modal' : null;
}}
{selectedTab}
selectedTab={commonStore.selectedTab}
projectData={data?.project}
clickMapNewFeature={() => {
openedActionModal = null;
Expand All @@ -148,18 +149,18 @@
toggleTaskActionModal={(value) => {
openedActionModal = value ? 'entity-modal' : null;
}}
{selectedTab}
selectedTab={commonStore.selectedTab}
projectData={data?.project}
/>
{#if selectedTab !== 'map'}
{#if commonStore.selectedTab !== 'map'}
<BottomSheet onClose={() => tabGroup.show('map')}>
{#if selectedTab === 'events'}
{#if commonStore.selectedTab === 'events'}
<More projectData={data?.project} zoomToTask={(taskId) => zoomToTask(taskId)}></More>
{/if}
{#if selectedTab === 'offline'}
{#if commonStore.selectedTab === 'offline'}
<BasemapComponent projectId={data.project.id}></BasemapComponent>
{/if}
{#if selectedTab === 'qrcode'}
{#if commonStore.selectedTab === 'qrcode'}
<QRCodeComponent {infoDialogRef} projectName={data.project.name} projectOdkToken={data.project.odk_token}>
<!-- Open ODK Button (Hide if it's project walkthrough step) -->
{#if +projectSetupStepStore.projectSetupStep !== projectSetupStepEnum['odk_project_load']}
Expand Down Expand Up @@ -210,7 +211,7 @@
placement="bottom"
no-scroll-controls
onsl-tab-show={(e: CustomEvent<{ name: string }>) => {
selectedTab = e.detail.name;
commonStore.setSelectedTab(e.detail.name);
if (
e.detail.name !== 'qrcode' &&
+projectSetupStepStore.projectSetupStep === projectSetupStepEnum['odk_project_load']
Expand Down
12 changes: 11 additions & 1 deletion src/mapper/src/store/common.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ let alert: AlertDetails | undefined = $state({ variant: null, message: '' });
let projectSetupStep: number | null = $state(null);
let projectBasemaps: Basemap[] = $state([]);
let projectPmtilesUrl: string | null = $state(null);
let selectedTab: string = $state('map');

function getCommonStore() {
return {
get selectedTab() {
return selectedTab;
},
setSelectedTab: (tab: string) => (selectedTab = tab),
};
}

function getAlertStore() {
return {
Expand Down Expand Up @@ -61,4 +71,4 @@ function getProjectBasemapStore() {
};
}

export { getAlertStore, getProjectSetupStepStore, getProjectBasemapStore };
export { getAlertStore, getProjectSetupStepStore, getProjectBasemapStore, getCommonStore };
3 changes: 3 additions & 0 deletions src/mapper/static/assets/icons/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 4af490f

Please sign in to comment.