Skip to content

Commit 98ba9cf

Browse files
committed
Add countrycode and elevation layer,
and update the corresponding fields in the form based on marker location
1 parent 78057d4 commit 98ba9cf

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

frontend/src/lib/components/BaseMap.svelte

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import {onMount, tick} from 'svelte';
44
55
import {unreachable} from '$lib/assert';
6-
import {MapDoubleClickDetector} from '$lib/map-helpers';
6+
import {
7+
MapDoubleClickDetector,
8+
queryCountryAtPoint,
9+
queryElevationAtPoint,
10+
} from '$lib/map-helpers';
711
import {reactive} from '$lib/svelte';
812
913
import {
@@ -33,6 +37,14 @@
3337
export let latitude: number | null = null;
3438
export let longitude: number | null = null;
3539
export let editable: boolean = false;
40+
export let onMarkerChange:
41+
| ((info: {
42+
lng: number;
43+
lat: number;
44+
countryCode: string | null;
45+
elevation: number | null;
46+
}) => void)
47+
| undefined = undefined;
3648
3749
// Props only used for mode 'multi'
3850
export let markers: NamedCoordinates[] = [];
@@ -92,6 +104,19 @@
92104
const lngLat = marker.getLngLat();
93105
latitude = Number(lngLat.lat.toFixed(5));
94106
longitude = Number(lngLat.lng.toFixed(5));
107+
108+
// Query country and elevation at new position and call marker change callback
109+
if (map && onMarkerChange) {
110+
const elevation = queryElevationAtPoint(map, lngLat.lng, lngLat.lat);
111+
const countryCode = queryCountryAtPoint(map, lngLat.lng, lngLat.lat);
112+
113+
onMarkerChange({
114+
lng: lngLat.lng,
115+
lat: lngLat.lat,
116+
countryCode,
117+
elevation,
118+
});
119+
}
95120
};
96121
97122
// Function to update marker position and coordinates
@@ -215,12 +240,15 @@
215240
'mapbox-light-base-layer',
216241
'swisstopo-layer',
217242
'swissimage-layer',
243+
'countries-layer',
218244
];
219245
const sourcesToRemove = [
220246
'mapbox-raster',
221247
'mapbox-light-base',
222248
'swisstopo-source',
223249
'swissimage-source',
250+
'countries-source',
251+
'terrain-rgb-source',
224252
];
225253
226254
layersToRemove.forEach((layerId) => {
@@ -315,6 +343,50 @@
315343
break;
316344
}
317345
346+
// Always add the countries source for country code detection (but hidden)
347+
if (onMarkerChange && !initializedMap.getSource('countries-source')) {
348+
initializedMap.addSource('countries-source', {
349+
type: 'vector',
350+
tiles: [
351+
`https://api.mapbox.com/v4/mapbox.country-boundaries-v1/{z}/{x}/{y}.vector.pbf?access_token=${MAPBOX_ACCESS_TOKEN}`,
352+
],
353+
minzoom: 0,
354+
maxzoom: 8,
355+
});
356+
357+
initializedMap.addLayer({
358+
'id': 'countries-layer',
359+
'type': 'fill',
360+
'source': 'countries-source',
361+
'source-layer': 'country_boundaries',
362+
'paint': {
363+
'fill-opacity': 0, // Make it invisible
364+
},
365+
'filter': [
366+
'all',
367+
['==', ['get', 'disputed'], 'false'],
368+
['any', ['==', 'all', ['get', 'worldview']], ['in', 'US', ['get', 'worldview']]],
369+
],
370+
});
371+
}
372+
373+
if (onMarkerChange && !initializedMap.getSource('terrain-rgb-source')) {
374+
initializedMap.addSource('terrain-rgb-source', {
375+
type: 'raster-dem',
376+
tiles: [
377+
`https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${MAPBOX_ACCESS_TOKEN}`,
378+
],
379+
tileSize: 256,
380+
maxzoom: 15,
381+
encoding: 'mapbox',
382+
});
383+
384+
initializedMap.setTerrain({
385+
source: 'terrain-rgb-source',
386+
exaggeration: 1,
387+
});
388+
}
389+
318390
prevMapType = newMapType;
319391
} catch (error) {
320392
console.error('Error updating map type:', error);
@@ -336,6 +408,20 @@
336408
mapMarker.setLngLat(pos);
337409
ensureSingleMarkerVisible();
338410
map?.flyTo({center: pos});
411+
412+
// Query country and elevation at new position
413+
if (map && onMarkerChange) {
414+
// Query both synchronously
415+
const elevation = queryElevationAtPoint(map, pos.lng, pos.lat);
416+
const countryCode = queryCountryAtPoint(map, pos.lng, pos.lat);
417+
418+
onMarkerChange({
419+
lng: pos.lng,
420+
lat: pos.lat,
421+
countryCode,
422+
elevation,
423+
});
424+
}
339425
}
340426
}, [latitude, longitude]);
341427

frontend/src/lib/components/SingleMap.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
export let editable: boolean = false;
1111
export let center: LngLatLike = DEFAULT_MAP_CENTER;
1212
export let zoom: number = 6;
13+
export let onMarkerChange:
14+
| ((info: {
15+
lng: number;
16+
lat: number;
17+
countryCode: string | null;
18+
elevation: number | null;
19+
}) => void)
20+
| undefined = undefined;
1321
</script>
1422

15-
<BaseMap mode="single" bind:latitude bind:longitude {editable} {center} {zoom} />
23+
<BaseMap mode="single" bind:latitude bind:longitude {editable} {center} {zoom} {onMarkerChange} />

frontend/src/lib/map-helpers.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
import type {LngLatLike, Map, MapMouseEvent} from 'maplibre-gl';
22

3+
/**
4+
* Query the country at a specific coordinate and return the result.
5+
*
6+
* @param map The MapLibre GL map instance
7+
* @param lng Longitude coordinate
8+
* @param lat Latitude coordinate
9+
* @returns The detected country code or null if not found
10+
*/
11+
export function queryCountryAtPoint(map: Map, lng: number, lat: number): string | null {
12+
// Convert coordinates to screen point for querying
13+
const point = map.project([lng, lat]);
14+
15+
// Query the countries layer at the point
16+
const features = map.queryRenderedFeatures(point, {
17+
layers: ['countries-layer'],
18+
});
19+
20+
if (features.length > 0) {
21+
const countryCode = features[0].properties?.iso_3166_1;
22+
if (countryCode && typeof countryCode === 'string') {
23+
return countryCode;
24+
}
25+
}
26+
return null;
27+
}
28+
29+
/**
30+
* Query the terrain elevation at a specific coordinate using MapLibre's built-in queryTerrainElevation.
31+
*
32+
* @param map The MapLibre GL map instance
33+
* @param lng Longitude coordinate
34+
* @param lat Latitude coordinate
35+
* @returns The elevation in meters or null if not available
36+
*/
37+
export function queryElevationAtPoint(map: Map, lng: number, lat: number): number | null {
38+
try {
39+
// Check if terrain source exists
40+
if (!map.getSource('terrain-rgb-source')) {
41+
return null;
42+
}
43+
44+
const elevation = map.queryTerrainElevation([lng, lat]);
45+
46+
if (elevation !== null && elevation !== undefined) {
47+
return Math.round(elevation);
48+
} else {
49+
return null;
50+
}
51+
} catch (error) {
52+
return null;
53+
}
54+
}
55+
356
export interface DoubleClickDetectorOptions {
457
/** Max delay for a double click/tap to be detected. */
558
maxDoubleTapDelayMs: number;

frontend/src/routes/locations/LocationForm.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@
3131
let longitudeInput: HTMLInputElement | null;
3232
let latitudeInput: HTMLInputElement | null;
3333
34+
// Marker change callback (includes country code and elevation detection)
35+
function handleMarkerChange(info: {
36+
lng: number;
37+
lat: number;
38+
countryCode: string | null;
39+
elevation: number | null;
40+
}) {
41+
// Auto-fill if changes detected
42+
if (info.countryCode && info.countryCode !== countryCode) {
43+
countryCode = info.countryCode;
44+
}
45+
if (info.elevation !== null && info.elevation !== elevation) {
46+
elevation = info.elevation;
47+
}
48+
}
49+
3450
// Validation
3551
const fields = ['name', 'countryCode', 'elevation', 'latitude', 'longitude'] as const;
3652
let fieldErrors: Record<(typeof fields)[number], string | undefined> = {
@@ -336,6 +352,7 @@
336352
editable={true}
337353
center={location?.coordinates}
338354
zoom={location?.coordinates !== undefined ? 13 : undefined}
355+
onMarkerChange={handleMarkerChange}
339356
/>
340357
</div>
341358

0 commit comments

Comments
 (0)