2026-04-07 22:01:58 +02:00
|
|
|
import { clsx, type ClassValue } from 'clsx';
|
2025-02-02 11:17:22 +01:00
|
|
|
import { twMerge } from 'tailwind-merge';
|
2026-10-31 18:08:22 +01:00
|
|
|
import { resolve } from '$app/paths';
|
|
|
|
|
import { languages } from '$lib/i18n/languages';
|
|
|
|
|
import type { Coordinates } from '$lib/geo/coordinates';
|
|
|
|
|
import { LngLatBounds, type Map as MapLibreMap } from 'maplibre-gl';
|
2025-06-08 16:32:41 +02:00
|
|
|
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
2024-04-08 17:12:39 +02:00
|
|
|
|
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
2024-06-06 11:58:50 +02:00
|
|
|
return twMerge(clsx(inputs));
|
2024-04-08 17:12:39 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-08 16:32:41 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
|
|
|
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
2026-04-07 22:01:58 +02:00
|
|
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
2024-06-06 11:58:50 +02:00
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
export function getElevation(
|
2026-10-31 18:08:22 +01:00
|
|
|
points: Coordinates[],
|
2026-05-27 18:52:15 +02:00
|
|
|
ELEVATION_ZOOM: number = 12,
|
2025-02-02 11:17:22 +01:00
|
|
|
tileSize = 512
|
|
|
|
|
): Promise<number[]> {
|
2026-10-31 18:08:22 +01:00
|
|
|
const coordinates = points;
|
|
|
|
|
const bbox = new LngLatBounds();
|
2024-09-04 19:11:56 +02:00
|
|
|
coordinates.forEach((coord) => bbox.extend(coord));
|
2024-07-19 13:17:23 +02:00
|
|
|
|
2026-10-31 18:08:22 +01:00
|
|
|
const tiles = coordinates.map((coord) => pointToTile(coord.lng, coord.lat, ELEVATION_ZOOM));
|
|
|
|
|
const uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
|
2025-02-02 11:17:22 +01:00
|
|
|
tile.split(',').map((x) => parseInt(x))
|
|
|
|
|
);
|
2026-10-31 18:08:22 +01:00
|
|
|
const images = new Map<string, ImageData>();
|
2026-01-30 21:01:24 +01:00
|
|
|
|
|
|
|
|
const getPixelFromImageData = (imageData: ImageData, x: number, y: number): number[] => {
|
|
|
|
|
const index = (y * imageData.width + x) * 4;
|
|
|
|
|
return [imageData.data[index], imageData.data[index + 1], imageData.data[index + 2]];
|
|
|
|
|
};
|
2024-07-19 13:18:38 +02:00
|
|
|
|
2026-10-31 18:08:22 +01:00
|
|
|
const promises = uniqueTiles.map((tile) =>
|
2026-05-27 18:52:15 +02:00
|
|
|
fetch(`https://tiles.gpx.studio/mapterhorn/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}.webp`, {
|
|
|
|
|
cache: 'force-cache',
|
|
|
|
|
})
|
2026-01-30 21:01:24 +01:00
|
|
|
.then((response) => response.blob())
|
2025-02-02 11:17:22 +01:00
|
|
|
.then(
|
2026-01-30 21:01:24 +01:00
|
|
|
(blob) =>
|
|
|
|
|
new Promise<void>((resolve) => {
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const img = new Image();
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
canvas.width = img.width;
|
|
|
|
|
canvas.height = img.height;
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
if (ctx) {
|
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
|
|
|
|
images.set(tile.join(','), imageData);
|
2025-02-02 11:17:22 +01:00
|
|
|
}
|
2026-01-30 21:01:24 +01:00
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
|
|
|
|
img.onerror = () => {
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
|
|
|
|
img.src = url;
|
2025-02-02 11:17:22 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return Promise.all(promises).then(() =>
|
|
|
|
|
coordinates.map((coord, index) => {
|
2026-10-31 18:08:22 +01:00
|
|
|
const tile = tiles[index];
|
|
|
|
|
const imageData = images.get(tile.join(','));
|
2025-02-02 11:17:22 +01:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
if (!imageData) {
|
2025-02-02 11:17:22 +01:00
|
|
|
return 0;
|
2024-09-04 19:11:56 +02:00
|
|
|
}
|
2024-07-19 13:18:38 +02:00
|
|
|
|
2026-10-31 18:08:22 +01:00
|
|
|
const tf = pointToTileFraction(coord.lng, coord.lat, ELEVATION_ZOOM);
|
|
|
|
|
const x = tileSize * (tf[0] - tile[0]);
|
|
|
|
|
const y = tileSize * (tf[1] - tile[1]);
|
|
|
|
|
const _x = Math.floor(x);
|
|
|
|
|
const _y = Math.floor(y);
|
|
|
|
|
const dx = x - _x;
|
|
|
|
|
const dy = y - _y;
|
2024-09-05 09:41:14 +02:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
const p00 = getPixelFromImageData(imageData, _x, _y);
|
|
|
|
|
const p01 = getPixelFromImageData(imageData, _x, _y + (_y + 1 == tileSize ? 0 : 1));
|
|
|
|
|
const p10 = getPixelFromImageData(imageData, _x + (_x + 1 == tileSize ? 0 : 1), _y);
|
|
|
|
|
const p11 = getPixelFromImageData(
|
|
|
|
|
imageData,
|
2025-02-02 11:17:22 +01:00
|
|
|
_x + (_x + 1 == tileSize ? 0 : 1),
|
|
|
|
|
_y + (_y + 1 == tileSize ? 0 : 1)
|
|
|
|
|
);
|
2024-09-05 09:41:14 +02:00
|
|
|
|
2026-10-31 18:08:22 +01:00
|
|
|
const ele00 = -32768 + p00[0] * 256 + p00[1] + p00[2] / 256;
|
|
|
|
|
const ele01 = -32768 + p01[0] * 256 + p01[1] + p01[2] / 256;
|
|
|
|
|
const ele10 = -32768 + p10[0] * 256 + p10[1] + p10[2] / 256;
|
|
|
|
|
const ele11 = -32768 + p11[0] * 256 + p11[1] + p11[2] / 256;
|
2024-07-19 13:18:38 +02:00
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
return (
|
|
|
|
|
ele00 * (1 - dx) * (1 - dy) +
|
|
|
|
|
ele01 * (1 - dx) * dy +
|
|
|
|
|
ele10 * dx * (1 - dy) +
|
|
|
|
|
ele11 * dx * dy
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
);
|
2024-07-19 13:18:38 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-24 19:06:39 +02:00
|
|
|
export function loadSVGIcon(map: MapLibreMap, id: string, svg: string, size: number = 100) {
|
2026-02-01 17:18:17 +01:00
|
|
|
if (!map.hasImage(id)) {
|
2026-10-31 18:08:22 +01:00
|
|
|
const icon = new Image(size, size);
|
2026-02-01 17:18:17 +01:00
|
|
|
icon.onload = () => {
|
|
|
|
|
if (!map.hasImage(id)) {
|
|
|
|
|
map.addImage(id, icon);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 11:13:55 +02:00
|
|
|
export function isMac() {
|
|
|
|
|
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSafari() {
|
|
|
|
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 18:08:22 +01:00
|
|
|
/**
|
|
|
|
|
* The path of a page in another language, without the base path (pass it to `resolve`). `path` is
|
|
|
|
|
* the path of the page, with or without the base path.
|
|
|
|
|
*/
|
2025-06-08 13:49:39 +02:00
|
|
|
export function getURLForLanguage(lang: string, path: string): string {
|
2026-10-31 18:08:22 +01:00
|
|
|
const base = resolve('/').slice(0, -1);
|
|
|
|
|
let newPath =
|
|
|
|
|
base && (path === base || path.startsWith(`${base}/`))
|
|
|
|
|
? path.slice(base.length) || '/'
|
|
|
|
|
: path;
|
2025-01-03 18:23:48 +01:00
|
|
|
|
2024-07-08 23:22:37 +02:00
|
|
|
let languageInPath = newPath.split('/')[1];
|
2026-10-31 18:08:22 +01:00
|
|
|
if (!Object.hasOwn(languages, languageInPath)) {
|
2024-07-08 23:22:37 +02:00
|
|
|
languageInPath = 'en';
|
2024-07-08 19:13:31 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-04 12:48:58 +01:00
|
|
|
if (newPath === '/' && lang !== 'en') {
|
|
|
|
|
newPath = '';
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 23:22:37 +02:00
|
|
|
if (languageInPath === 'en') {
|
|
|
|
|
if (lang === 'en') {
|
2026-10-31 18:08:22 +01:00
|
|
|
return newPath;
|
2024-07-08 23:22:37 +02:00
|
|
|
} else {
|
2026-10-31 18:08:22 +01:00
|
|
|
return `/${lang}${newPath}`;
|
2024-07-08 23:22:37 +02:00
|
|
|
}
|
2024-07-08 19:13:31 +02:00
|
|
|
} else {
|
2024-07-08 23:22:37 +02:00
|
|
|
if (lang === 'en') {
|
|
|
|
|
newPath = newPath.replace(`/${languageInPath}`, '');
|
2026-10-31 18:08:22 +01:00
|
|
|
return newPath === '' ? '/' : newPath;
|
2024-07-08 23:22:37 +02:00
|
|
|
} else {
|
2026-10-31 18:08:22 +01:00
|
|
|
return newPath.replace(`/${languageInPath}`, `/${lang}`);
|
2024-07-08 23:22:37 +02:00
|
|
|
}
|
2024-07-08 19:13:31 +02:00
|
|
|
}
|
2024-10-08 15:49:14 +02:00
|
|
|
}
|