import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { base } from '$app/paths'; import { languages } from '$lib/languages'; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx'; import mapboxgl from 'mapbox-gl'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import PNGReader from 'png.js'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; export type WithElementRef = T & { ref?: U | null; }; export function getClosestLinePoint( points: TrackPoint[], point: TrackPoint | Coordinates, details: any = {} ): TrackPoint { let closest = points[0]; let closestDist = Number.MAX_VALUE; for (let i = 0; i < points.length - 1; i++) { let dist = crossarcDistance(points[i], points[i + 1], point); if (dist < closestDist) { closestDist = dist; if (distance(points[i], point) <= distance(points[i + 1], point)) { closest = points[i]; details['before'] = true; details['index'] = i; } else { closest = points[i + 1]; details['before'] = false; details['index'] = i + 1; } } } details['distance'] = closestDist; return closest; } export function getElevation( points: (TrackPoint | Waypoint | Coordinates)[], ELEVATION_ZOOM: number = 13, tileSize = 512 ): Promise { let coordinates = points.map((point) => point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point ); let bbox = new mapboxgl.LngLatBounds(); coordinates.forEach((coord) => bbox.extend(coord)); let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM)); let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) => tile.split(',').map((x) => parseInt(x)) ); let pngs = new Map(); let promises = uniqueTiles.map((tile) => fetch( `https://api.mapbox.com/v4/mapbox.mapbox-terrain-dem-v1/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}@2x.pngraw?access_token=${PUBLIC_MAPBOX_TOKEN}`, { cache: 'force-cache' } ) .then((response) => response.arrayBuffer()) .then( (buffer) => new Promise((resolve) => { let png = new PNGReader(new Uint8Array(buffer)); png.parse((err, png) => { if (err) { resolve(false); // Also resolve so that Promise.all doesn't fail } else { pngs.set(tile.join(','), png); resolve(true); } }); }) ) ); return Promise.all(promises).then(() => coordinates.map((coord, index) => { let tile = tiles[index]; let png = pngs.get(tile.join(',')); if (!png) { return 0; } let tf = pointToTileFraction(coord.lon, coord.lat, ELEVATION_ZOOM); let x = tileSize * (tf[0] - tile[0]); let y = tileSize * (tf[1] - tile[1]); let _x = Math.floor(x); let _y = Math.floor(y); let dx = x - _x; let dy = y - _y; const p00 = png.getPixel(_x, _y); const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1)); const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y); const p11 = png.getPixel( _x + (_x + 1 == tileSize ? 0 : 1), _y + (_y + 1 == tileSize ? 0 : 1) ); let ele00 = -10000 + (p00[0] * 256 * 256 + p00[1] * 256 + p00[2]) * 0.1; let ele01 = -10000 + (p01[0] * 256 * 256 + p01[1] * 256 + p01[2]) * 0.1; let ele10 = -10000 + (p10[0] * 256 * 256 + p10[1] * 256 + p10[2]) * 0.1; let ele11 = -10000 + (p11[0] * 256 * 256 + p11[1] * 256 + p11[2]) * 0.1; return ( ele00 * (1 - dx) * (1 - dy) + ele01 * (1 - dx) * dy + ele10 * dx * (1 - dy) + ele11 * dx * dy ); }) ); } export function isMac() { return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; } export function isSafari() { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); } export function getURLForLanguage(lang: string, path: string): string { let newPath = path.replace(base, ''); let languageInPath = newPath.split('/')[1]; if (!languages.hasOwnProperty(languageInPath)) { languageInPath = 'en'; } if (newPath === '/' && lang !== 'en') { newPath = ''; } if (languageInPath === 'en') { if (lang === 'en') { return `${base}${newPath}`; } else { return `${base}/${lang}${newPath}`; } } else { if (lang === 'en') { newPath = newPath.replace(`/${languageInPath}`, ''); return newPath === '' ? `${base}/` : `${base}${newPath}`; } else { newPath = newPath.replace(`/${languageInPath}`, `/${lang}`); return `${base}${newPath}`; } } }