Skip to content

Commit

Permalink
feat(mapper): flatgeobuf maplibre component for loading features (#1851)
Browse files Browse the repository at this point in the history
* fix: add 'anon' defualt if no username provided for event

* docs: comments describing parts of tasks store

* fix: on hover effect for tasks using promoteId

* feat: add selectedTaskGeom to task store when task area selected

* feat: wip add flatgeobuf layer wrapper component around GeoJSON component

* feat: flatgeobuf logic only load bbox and filter by extent

* fix(typescript): remaining typing errors for flatgeobuf code
  • Loading branch information
spwoodcock authored Nov 5, 2024
1 parent 293f44d commit 6724b3f
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 43 deletions.
3 changes: 2 additions & 1 deletion src/mapper/src/lib/components/bottom-sheet.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { onMount } from 'svelte';
let bottomSheetRef: any = $state();
Expand All @@ -12,7 +13,7 @@
interface Props {
onClose: () => void;
children?: import('svelte').Snippet;
children?: Snippet;
}
let { onClose, children }: Props = $props();
Expand Down
2 changes: 1 addition & 1 deletion src/mapper/src/lib/components/event-card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<!-- Action Text and Task ID -->
<div class="text-base mr-4">
<span class="text-[#555555] font-medium font-archivo">
{record?.event} by {record.username}
{record?.event} by {record.username || 'anon'}
</span>

<!-- Date and Time -->
Expand Down
105 changes: 105 additions & 0 deletions src/mapper/src/lib/components/map/flatgeobuf-layer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { onDestroy } from 'svelte';
import { GeoJSON as MapLibreGeoJSON } from 'svelte-maplibre';
import { getId, updatedSourceContext, addSource, removeSource } from 'svelte-maplibre';
import type { HeaderMeta } from 'flatgeobuf';
import type { GeoJSON, Polygon, FeatureCollection } from 'geojson';
import { flatgeobufToGeoJson, filterGeomsCentroidsWithin } from '$lib/utils/flatgeobuf';
type bboxType = [number, number, number, number];
interface Props {
id?: string;
url: string;
extent?: bboxType | Polygon | null;
extractGeomCols: boolean,
metadataFunc?: (headerMetadata: HeaderMeta) => void;
promoteId?: string;
children?: Snippet;
}
let {
id = getId('flatgeobuf'),
url,
extent,
extractGeomCols = false,
promoteId = undefined,
metadataFunc,
children,
}: Props = $props();
const { map, self: sourceId } = updatedSourceContext();
let sourceObj: maplibregl.GeoJSONSource | undefined = $state();
let first = $state(true);
let geojsonData: GeoJSON = $state({type: 'FeatureCollection', features: []});
// Set currentSourceId as reactive property once determined from context
let currentSourceId: string | undefined = $state();
$effect(() => {
currentSourceId = $sourceId;
});
// Deserialise flatgeobuf to GeoJSON, reactive to bbox/extent changes
async function updateGeoJSONData() {
const featcol: FeatureCollection | null = await flatgeobufToGeoJson(url, extent, metadataFunc, extractGeomCols);
// If there is no data, set to an empty FeatureCollection to avoid
// re-adding layer if the bbox extent is updated
if (!featcol) {
geojsonData = {
type: 'FeatureCollection',
features: [],
};
} else if (extent && "type" in extent && extent.type === 'Polygon') {
geojsonData = filterGeomsCentroidsWithin(featcol, extent);
} else {
geojsonData = featcol;
}
currentSourceId = id;
addSourceToMap();
}
$effect(() => {
updateGeoJSONData();
});
function addSourceToMap() {
if (!$map) return;
const initialData: maplibregl.SourceSpecification = {
type: 'geojson',
data: geojsonData,
promoteId,
};
// Use the currentSourceId in addSource
addSource($map, currentSourceId!, initialData, (sourceId) => sourceId === currentSourceId, () => {
sourceObj = $map?.getSource(currentSourceId!) as maplibregl.GeoJSONSource;
first = true;
});
}
// Update data only if source already exists
$effect(() => {
if (sourceObj && geojsonData) {
if (first) {
first = false;
} else {
sourceObj.setData(geojsonData);
}
}
});
onDestroy(() => {
if (sourceObj && $map) {
removeSource($map, currentSourceId!, sourceObj);
currentSourceId = undefined;
sourceObj = undefined;
}
});
</script>

<MapLibreGeoJSON id={currentSourceId} data={geojsonData} promoteId={promoteId}>
{@render children?.()}
</MapLibreGeoJSON>
62 changes: 23 additions & 39 deletions src/mapper/src/lib/components/map/main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,40 @@
import Legend from '$lib/components/map/legend.svelte';
import LayerSwitcher from '$lib/components/map/layer-switcher.svelte';
import Geolocation from '$lib/components/map/geolocation.svelte';
import FlatGeobuf from '$lib/components/map/flatgeobuf-layer.svelte';
import { getTaskStore } from '$store/tasks.svelte.ts';
// import { entityFeatcolStore, selectedEntityId } from '$store/entities';
type bboxType = [number, number, number, number];
interface Props {
projectOutlineCoords: Position[][],
entitiesUrl: string,
toggleTaskActionModal: boolean;
}
let {
projectOutlineCoords,
entitiesUrl,
toggleTaskActionModal = $bindable(),
}: Props = $props();
const taskStore = getTaskStore();
let map: maplibregl.Map | undefined = $state();
let loaded: boolean = $state(false);
let taskAreaClicked = $state(false);
let toggleGeolocationStatus = $state(false);
let taskAreaClicked: boolean = $state(false);
let toggleGeolocationStatus: boolean = $state(false);
// Fit the map bounds to the project area
$effect(() => {
if (map && projectOutlineCoords) {
const projectPolygon = polygon(projectOutlineCoords);
const projectBuffer = buffer(projectPolygon, 100, { units: 'meters' });
const projectBbox: [number, number, number, number] = bbox(projectBuffer) as [number, number, number, number];
map.fitBounds(projectBbox, { duration: 0 });
if (projectBuffer) {
const projectBbox: bboxType = bbox(projectBuffer) as bboxType;
map.fitBounds(projectBbox, { duration: 0 });
}
}
});
Expand Down Expand Up @@ -129,7 +136,7 @@
<Geolocation bind:map bind:toggleGeolocationStatus></Geolocation>
{/if}
<!-- The task area geojson -->
<GeoJSON id="states" data={taskStore.featcol} promoteId="TASKS">
<GeoJSON id="tasks" data={taskStore.featcol} promoteId="fid">
<FillLayer
hoverCursor="pointer"
paint={{
Expand Down Expand Up @@ -180,46 +187,23 @@
}}
/>
</GeoJSON>
<!-- The features / entities geojson
<GeoJSON id="states" data={$entityFeatcolStore} promoteId="ENTITIES">
<!-- The features / entities -->
<FlatGeobuf
id="entities"
url={entitiesUrl}
extent={taskStore.selectedTaskGeom}
extractGeomCols={true}
promoteId="id"
>
<FillLayer
hoverCursor="pointer"
paint={{
'fill-color': [
'match',
['get', 'status'],
'READY',
'#ffffff',
'OPENED_IN_ODK',
'#008099',
'SURVEY_SUBMITTED',
'#ade6ef',
'MARKED_BAD',
'#fceca4',
'#c5fbf5', // default color if no match is found
],
'fill-opacity': hoverStateFilter(0.1, 0),
}}
beforeLayerType="symbol"
manageHoverState
on:click={(e) => {
// taskAreaClicked = true;
// const clickedEntityId = e.detail.features?.[0]?.properties?.fid;
// entityStore.selectedEntityId = clickedEntityId;
// toggleTaskActionModal = true;
}}
/>
<LineLayer
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
paint={{
'line-color': ['case', ['==', ['get', 'fid'], $selectedEntityId], '#fa1100', '#0fffff'],
'line-width': 3,
'line-opacity': ['case', ['==', ['get', 'fid'], $selectedEntityId], 1, 0.35],
'fill-color': '#006600',
'fill-opacity': 0.5,
}}
beforeLayerType="symbol"
manageHoverState
/>
</GeoJSON> -->
</FlatGeobuf>
<div class="absolute right-3 bottom-3 sm:right-5 sm:bottom-5">
<LayerSwitcher />
<Legend />
Expand Down
Loading

0 comments on commit 6724b3f

Please sign in to comment.