Skip to content
Open
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
134 changes: 131 additions & 3 deletions frontend/src/lib/components/BaseMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
const MAP_HEIGHT_SMALL = '400px';
const MAP_HEIGHT_LARGE = 'max(100vh, 800px)';

// Minimum zoom level for elevation queries (contours available from zoom 9)
const MIN_ZOOM_FOR_ELEVATION_QUERY = 9;

export let mode: 'single' | 'multi';

// Common props
Expand All @@ -34,6 +37,12 @@
export let longitude: number | null = null;
export let editable: boolean = false;

export let onElevationLookup:
| ((data: {elevation: number | null; zoomTooLow: boolean}) => void)
| undefined = undefined;
export let onCountryLookup: ((data: {countryCode: string | null}) => void) | undefined =
undefined;

// Props only used for mode 'multi'
export let markers: NamedCoordinates[] = [];

Expand All @@ -59,6 +68,80 @@
}
}

/**
* Query elevation at a given point using terrain contours.
* Returns the elevation in meters, or null if not available.
*/
function queryElevation(
initializedMap: Map,
lngLat: {lng: number; lat: number},
): {
elevation: number | null;
zoomTooLow: boolean;
} {
const currentZoom = initializedMap.getZoom();
if (currentZoom < MIN_ZOOM_FOR_ELEVATION_QUERY) {
return {elevation: null, zoomTooLow: true};
}

const point = initializedMap.project([lngLat.lng, lngLat.lat]);
const features = initializedMap.queryRenderedFeatures(point, {
layers: ['terrain-contours-data'],
});

if (features.length === 0) {
return {elevation: null, zoomTooLow: false};
}

// Get the highest elevation from overlapping contour polygons
const elevations = features
.map((f) => f.properties?.ele as number | undefined)
.filter((ele): ele is number => ele !== undefined);

if (elevations.length === 0) {
return {elevation: null, zoomTooLow: false};
}

return {elevation: Math.max(...elevations), zoomTooLow: false};
}

/**
* Query country code at a given point.
* Returns the ISO 3166-1 alpha-2 country code, or null if not available.
* Country boundaries are available at all zoom levels.
*/
function queryCountryCode(
initializedMap: Map,
lngLat: {lng: number; lat: number},
): string | null {
const point = initializedMap.project([lngLat.lng, lngLat.lat]);
const features = initializedMap.queryRenderedFeatures(point, {
layers: ['countries-data'],
});

if (features.length === 0) {
return null;
}

// Get the country code from the first feature
return (features[0].properties?.iso_3166_1 as string | undefined) ?? null;
}

/**
* Perform location data lookup (elevation and country) and dispatch events.
*/
function performLocationDataLookup(initializedMap: Map, lngLat: {lng: number; lat: number}) {
if (!editable) {
return;
}

const elevationResult = queryElevation(initializedMap, lngLat);
const countryCode = queryCountryCode(initializedMap, lngLat);

onElevationLookup?.(elevationResult);
onCountryLookup?.({countryCode});
}

/**
* Toggle map height.
*/
Expand Down Expand Up @@ -93,15 +176,28 @@
longitude = Number(lngLat.lng.toFixed(5));
};

// Function to update marker position and coordinates
// Function to update marker position, coordinates, and location lookup data
const updateMarkerPosition = (lngLat: LngLatLike) => {
marker.setLngLat(lngLat);
ensureSingleMarkerVisible();
updateCoordinatesFromMarker();
// Lookup elevation and country code
const markerLngLat = marker.getLngLat();
performLocationDataLookup(initializedMap, {
lng: markerLngLat.lng,
lat: markerLngLat.lat,
});
};

// Update coordinates on marker drag
marker.on('dragend', updateCoordinatesFromMarker);
// Update coordinates and lookup data on marker drag
marker.on('dragend', () => {
updateCoordinatesFromMarker();
const markerLngLat = marker.getLngLat();
performLocationDataLookup(initializedMap, {
lng: markerLngLat.lng,
lat: markerLngLat.lat,
});
});

// Set up double click detection, update marker and coordinates on double click (desktop)
// or double tap (mobile)
Expand Down Expand Up @@ -230,6 +326,38 @@
break;
}

// Add invisible data layers for location lookups (elevation and country code).
// Only loaded when adding/editing a location.
if (editable) {
// Terrain contours for elevation lookup
initializedMap.addLayer({
'id': 'terrain-contours-data',
'type': 'fill',
'source': {
type: 'vector',
url: `https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2.json?access_token=${MAPBOX_ACCESS_TOKEN}`,
},
'source-layer': 'contour',
'paint': {
'fill-opacity': 0,
},
});

// Country boundaries for country code lookup
initializedMap.addLayer({
'id': 'countries-data',
'type': 'fill',
'source': {
type: 'vector',
url: `https://api.mapbox.com/v4/mapbox.country-boundaries-v1.json?access_token=${MAPBOX_ACCESS_TOKEN}`,
},
'source-layer': 'country_boundaries',
'paint': {
'fill-opacity': 0,
},
});
}

// Map markers and labels
addMapMarkersAndLabels(initializedMap);
});
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/lib/components/SingleMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@
export let editable: boolean = false;
export let center: LngLatLike = DEFAULT_MAP_CENTER;
export let zoom: number = 6;

// Callback props for location data lookups (forwarded to BaseMap)
export let onElevationLookup:
| ((data: {elevation: number | null; zoomTooLow: boolean}) => void)
| undefined = undefined;
export let onCountryLookup: ((data: {countryCode: string | null}) => void) | undefined =
undefined;
</script>

<BaseMap mode="single" bind:latitude bind:longitude {editable} {center} {zoom} />
<BaseMap
mode="single"
bind:latitude
bind:longitude
{editable}
{center}
{zoom}
{onElevationLookup}
{onCountryLookup}
/>
43 changes: 43 additions & 0 deletions frontend/src/routes/locations/LocationForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@
let latitude: number | null = location?.coordinates?.lat ?? null;
let longitude: number | null = location?.coordinates?.lon ?? null;

// Hint for elevation auto-fill (shown when zoom is too low)
let showElevationHint = false;

// Handle elevation lookup from map
function handleElevationLookup(data: {elevation: number | null; zoomTooLow: boolean}) {
const {elevation: newElevation, zoomTooLow} = data;
if (newElevation !== null) {
elevation = newElevation;
showElevationHint = false;
} else {
// Clear outdated value when zoomed out
elevation = null;
showElevationHint = zoomTooLow;
}
}

// Handle country code lookup from map
function handleCountryLookup(data: {countryCode: string | null}) {
const {countryCode: newCountryCode} = data;
if (newCountryCode !== null) {
countryCode = newCountryCode;
}
}

// Input transformations
$: if (countryCode.length > 0) {
countryCode = countryCode.toLocaleUpperCase();
Expand Down Expand Up @@ -270,6 +294,12 @@
{$i18n.t('common.error', {message: fieldErrors.elevation})}
</div>
{/if}
{#if showElevationHint}
<div class="field-hint">
<i class="fa-solid fa-circle-info"></i>
{$i18n.t('location.hint--zoom-in-elevation')}
</div>
{/if}

<label class="label" for="lat">
{$i18n.t('location.title--latitude')} (WGS 84)
Expand Down Expand Up @@ -336,6 +366,8 @@
editable={true}
center={location?.coordinates}
zoom={location?.coordinates !== undefined ? 13 : undefined}
onElevationLookup={handleElevationLookup}
onCountryLookup={handleCountryLookup}
/>
</div>

Expand Down Expand Up @@ -368,4 +400,15 @@
margin-top: -12px;
margin-bottom: 12px;
}

.field-hint {
color: #3273dc;
font-size: 0.8em;
margin-top: -12px;
margin-bottom: 12px;
}

.field-hint i {
margin-right: 4px;
}
</style>
1 change: 1 addition & 0 deletions frontend/src/translations/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
"error--longitude-empty": "Wenn der Breitengrad definiert ist, darf der Längengrad nicht leer sein",
"error--name-empty": "Name darf nicht leer sein",
"error--update-error": "Der Standort konnte aufgrund eines Serverfehlers nicht aktualisiert werden: {message}",
"hint--zoom-in-elevation": "Zoome in die Karte, um die Höhe automatisch auszufüllen.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"prose--add-success": "Standort «{name}» erfolgreich hinzugefügt",
"prose--hint-double-click": "Hinweis: Ein Doppelklick oder Doppeltippen auf der Karte aktualisiert die Koordinaten.",
"prose--update-success": "Standort «{name}» erfolgreich aktualisiert",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
"error--longitude-empty": "If latitude is set, longitude must not be empty",
"error--name-empty": "Name must not be empty",
"error--update-error": "The location could not be updated due to an error on the server: {message}",
"hint--zoom-in-elevation": "Zoom in on the map to auto-fill the elevation.",
"prose--add-success": "Location “{name}” successfully added",
"prose--hint-double-click": "Note: Double-click or double-tap on the map to update the location coordinates.",
"prose--update-success": "Location “{name}” successfully updated",
Expand Down