Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new point geom in maplibre terradraw, inject to ODK collect #1966

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading