Skip to content

Commit

Permalink
feat(mapper): add new point geom in maplibre terradraw, inject to ODK…
Browse files Browse the repository at this point in the history
… collect (#1966)
  • Loading branch information
spwoodcock authored Dec 9, 2024
1 parent aacabbc commit 6fdbf96
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 39 deletions.
1 change: 1 addition & 0 deletions src/mapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@turf/bbox": "^7.1.0",
"@turf/buffer": "^7.1.0",
"@turf/helpers": "^7.1.0",
"@watergis/maplibre-gl-terradraw": "^0.5.1",
"drizzle-orm": "^0.35.3",
"flatgeobuf": "^3.36.0",
"maplibre-gl": "^4.7.1",
Expand Down
20 changes: 20 additions & 0 deletions src/mapper/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 4 additions & 23 deletions src/mapper/src/lib/components/dialog-task-actions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,17 @@
import { mapTask, finishTask, resetTask } from '$lib/db/events';
import type { ProjectData } from '$lib/types';
import { getTaskStore } from '$store/tasks.svelte.ts';
import { getAlertStore } from '$store/common.svelte.ts';
type Props = {
isTaskActionModalOpen: boolean;
toggleTaskActionModal: (value: boolean) => void;
selectedTab: string;
projectData: ProjectData;
clickMapNewFeature: () => void;
};
const taskStore = getTaskStore();
const alertStore = getAlertStore();
let { isTaskActionModalOpen, toggleTaskActionModal, selectedTab, projectData }: Props = $props();
function mapNewFeature() {
const xformId = projectData?.odk_form_id;
if (!xformId) {
return;
}
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
document.location.href = `odkcollect://form/${xformId}`;
} else {
alertStore.setAlert({
variant: 'warning',
message: 'Requires a mobile phone with ODK Collect.',
});
}
}
let { isTaskActionModalOpen, toggleTaskActionModal, selectedTab, projectData, clickMapNewFeature }: Props = $props();
</script>

{#if taskStore.selectedTaskId && selectedTab === 'map' && isTaskActionModalOpen && (taskStore.selectedTaskState === 'UNLOCKED_TO_MAP' || taskStore.selectedTaskState === 'LOCKED_FOR_MAPPING')}
Expand All @@ -57,11 +38,11 @@
<p class="text-[#333] text-xl font-barlow-semibold">Task #{taskStore.selectedTaskId}</p>
<div
onclick={() => {
mapNewFeature();
clickMapNewFeature();
}}
onkeydown={(e: KeyboardEvent) => {
if (e.key === 'Enter') {
mapNewFeature();
clickMapNewFeature();
}
}}
role="button"
Expand Down
94 changes: 84 additions & 10 deletions src/mapper/src/lib/components/map/main.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import '$styles/page.css';
import '$styles/button.css';
import '@watergis/maplibre-gl-terradraw/dist/maplibre-gl-terradraw.css'
import '@hotosm/ui/dist/hotosm-ui';
import { onMount, tick } from 'svelte';
import { onMount } from 'svelte';
import {
MapLibre,
GeoJSON,
Expand All @@ -17,11 +18,12 @@
ControlButton,
} from 'svelte-maplibre';
import maplibre from 'maplibre-gl';
import MaplibreTerradrawControl from '@watergis/maplibre-gl-terradraw'
import { Protocol } from 'pmtiles';
import { polygon } from '@turf/helpers';
import { buffer } from '@turf/buffer';
import { bbox } from '@turf/bbox';
import type { Position } from 'geojson';
import type { GeoJSON as GeoJSONType, Position, Geometry as GeoJSONGeometry } from 'geojson';
import LocationArcImg from '$assets/images/locationArc.png';
import LocationDotImg from '$assets/images/locationDot.png';
Expand All @@ -40,7 +42,6 @@
import { projectSetupStep as projectSetupStepEnum } from '$constants/enums.ts';
import { baseLayers, osmStyle, pmtilesStyle } from '$constants/baseLayers.ts';
import { getEntitiesStatusStore } from '$store/entities.svelte.ts';
import type { GeoJSON as GeoJSONType } from 'geojson';
type bboxType = [number, number, number, number];
Expand All @@ -50,9 +51,11 @@
toggleActionModal: (value: 'task-modal' | 'entity-modal' | null) => void;
projectId: number;
setMapRef: (map: maplibregl.Map | undefined) => void;
draw?: boolean;
handleDrawnGeom?: ((geojson: GeoJSONGeometry) => void) | null;
}
let { projectOutlineCoords, entitiesUrl, toggleActionModal, projectId, setMapRef }: Props = $props();
let { projectOutlineCoords, entitiesUrl, toggleActionModal, projectId, setMapRef, draw = false, handleDrawnGeom }: Props = $props();
const taskStore = getTaskStore();
const projectSetupStepStore = getProjectSetupStepStore();
Expand Down Expand Up @@ -104,6 +107,29 @@
// allBaseLayers = layers;
// }
// })
let displayDrawHelpText: boolean = $state(false);
const drawControl = new MaplibreTerradrawControl({
modes: [
'point',
// 'polygon',
// 'linestring',
// 'delete',
],
// Note We do not open the toolbar options, allowing the user
// to simply click with a pre-defined mode active
// open: true,
});
$effect(() => {
projectSetupStep = +projectSetupStepStore.projectSetupStep;
});
// set the map ref to parent component
$effect(() => {
if (map) {
setMapRef(map);
}
});
// using this function since outside click of entity layer couldn't be tracked via FillLayer
function handleMapClick(e: maplibregl.MapMouseEvent) {
Expand Down Expand Up @@ -154,14 +180,55 @@
}
});
$effect(() => {
projectSetupStep = +projectSetupStepStore.projectSetupStep;
});
// 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');
// set the map ref to parent component
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-outline')) map.removeLayer('td-polygon-outline');
if (map.getSource('td-polygon-outline')) map.removeSource('td-polygon-outline');
}
}
// Add draw layer & handle emitted geom
$effect(() => {
if (map) {
setMapRef(map);
if (draw) {
map?.addControl(drawControl, 'top-left');
displayDrawHelpText = true;
const drawInstance = drawControl.getTerraDrawInstance();
if (drawInstance && handleDrawnGeom) {
drawInstance.start();
drawInstance.setMode('point');
drawInstance.on('finish', (id: string, _context: any) => {
// Save the drawn geometry location, then delete all geoms from store
const features: { id: string; geometry: GeoJSONGeometry }[] = drawInstance.getSnapshot();
const drawnFeature = features.find((geom) => geom.id === id);
let firstGeom: GeoJSONGeometry = null;
if (drawnFeature && drawnFeature.geometry) {
firstGeom = drawnFeature.geometry;
} else {
console.error(`Feature with id ${id} not found or has no geometry.`);
}
drawInstance.stop();
if (firstGeom) {
removeTerraDrawLayers();
handleDrawnGeom(firstGeom);
}
});
};
} else {
removeTerraDrawLayers();
map?.removeControl(drawControl);
displayDrawHelpText = false;
}
});
Expand Down Expand Up @@ -386,4 +453,11 @@
<p class="uppercase font-barlow-medium text-base">please select a task / feature for mapping</p>
</div>
{/if}

<!-- Help for drawing a new geometry -->
{#if displayDrawHelpText}
<div class="absolute top-5 w-fit bg-[#F097334D] z-10 left-[50%] translate-x-[-50%] p-1">
<p class="uppercase font-barlow-medium text-base">Click on the map to create a new point</p>
</div>
{/if}
</MapLibre>
2 changes: 1 addition & 1 deletion src/mapper/src/lib/components/qrcode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Snippet } from 'svelte';
import SlDialog from '@shoelace-style/shoelace/dist/components/dialog/dialog.component.js';
import { generateQrCode, downloadQrCode } from '$lib/utils/qrcode';
import { generateQrCode, downloadQrCode } from '$lib/odk/qrcode';
interface Props {
infoDialogRef: SlDialog | null;
Expand Down
26 changes: 26 additions & 0 deletions src/mapper/src/lib/odk/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Geometry as GeoJSONGeometry } from 'geojson';

import { getAlertStore } from '$store/common.svelte.ts';
import { geojsonGeomToJavarosa } from '$lib/odk/javarosa';

const alertStore = getAlertStore();

export function openOdkCollectNewFeature(xFormId: string, geom: GeoJSONGeometry) {
if (!xFormId || !geom) {
return;
}

const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

const javarosaGeom = geojsonGeomToJavarosa(geom);

if (isMobile) {
// TODO we need to update the form to support task_id=${}&
document.location.href = `odkcollect://form/${xFormId}?new_feature=${javarosaGeom}`;
} else {
alertStore.setAlert({
variant: 'warning',
message: 'Requires a mobile phone with ODK Collect.',
});
}
}
43 changes: 43 additions & 0 deletions src/mapper/src/lib/odk/javarosa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Geometry as GeoJSONGeometry } from 'geojson';

export function geojsonGeomToJavarosa(geometry: GeoJSONGeometry) {
if (geometry.type === 'GeometryCollection') {
console.error('Unsupported GeoJSON type: GeometryCollection');
return;
}

if (!geometry || !geometry.type || !geometry.coordinates) {
throw new Error('Invalid GeoJSON feature: Missing geometry or coordinates.');
}

// Normalize geometries into a common structure for processing
let coordinates: any[] = [];
switch (geometry.type) {
case 'Point':
coordinates = [[geometry.coordinates]]; // [[x, y]]
break;
case 'LineString':
case 'MultiPoint':
coordinates = [geometry.coordinates]; // [[x, y], [x, y]]
break;
case 'Polygon':
case 'MultiLineString':
coordinates = geometry.coordinates; // [[[x, y], [x, y]]]
break;
case 'MultiPolygon':
coordinates = geometry.coordinates.flat(); // Flatten [[[...]], [[...]]]
break;
default:
throw new Error(`Unsupported GeoJSON geometry type: ${geometry}`);
}

// Convert to JavaRosa format
const javarosaGeometry = coordinates
.flatMap((polygonOrLine) =>
polygonOrLine.map(([longitude, latitude]: [number, number]) => `${latitude} ${longitude} 0.0 0.0`),
)
.join(';');

// Must append a final ; to finish the geom
return `${javarosaGeometry};`;
}
File renamed without changes.
Loading

0 comments on commit 6fdbf96

Please sign in to comment.