import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { cubicOut } from "svelte/easing"; import type { TransitionConfig } from "svelte/transition"; import { get } from "svelte/store"; import { map } from "./stores"; import { base } from "$app/paths"; import { languages } from "$lib/languages"; import { locale } from "svelte-i18n"; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from "gpx"; import mapboxgl from "mapbox-gl"; import tilebelt 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)); } type FlyAndScaleParams = { y?: number; x?: number; start?: number; duration?: number; }; export const flyAndScale = ( node: Element, params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 50 } ): TransitionConfig => { const style = getComputedStyle(node); const transform = style.transform === "none" ? "" : style.transform; const scaleConversion = ( valueA: number, scaleA: [number, number], scaleB: [number, number] ) => { const [minA, maxA] = scaleA; const [minB, maxB] = scaleB; const percentage = (valueA - minA) / (maxA - minA); const valueB = percentage * (maxB - minB) + minB; return valueB; }; const styleToString = ( style: Record ): string => { return Object.keys(style).reduce((str, key) => { if (style[key] === undefined) return str; return str + `${key}:${style[key]};`; }, ""); }; return { duration: params.duration ?? 200, delay: 0, css: (t) => { const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); return styleToString({ transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, opacity: t }); }, easing: cubicOut }; }; 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) => tilebelt.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 = tilebelt.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; })); } let previousCursors: string[] = []; export function setCursor(cursor: string) { let m = get(map); if (m) { previousCursors.push(m.getCanvas().style.cursor); m.getCanvas().style.cursor = cursor; } } export function resetCursor() { let m = get(map); if (m) { m.getCanvas().style.cursor = previousCursors.pop() ?? ''; } } export function setPointerCursor() { setCursor('pointer'); } export function setGrabbingCursor() { setCursor('grabbing'); } export function setCrosshairCursor() { setCursor('crosshair'); } export const scissorsCursor = `url('data:image/svg+xml,') 12 12, auto`; export function setScissorsCursor() { setCursor(scissorsCursor); } 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 | null | undefined, path: string): string { let newPath = path.replace(base, ''); let languageInPath = newPath.split('/')[1]; if (!languages.hasOwnProperty(languageInPath)) { languageInPath = 'en'; } if (lang === null || lang === undefined) { lang = get(locale); if (lang === null || lang === undefined) { lang = 'en'; } } 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}`; } } }