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'; import type { DateFormatter } from '@internationalized/date'; 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 (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}`; } } } function getDateFormatter(locale: string) { return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'medium', }); } export let df: DateFormatter = getDateFormatter('en'); locale.subscribe((l) => { df = getDateFormatter(l ?? 'en'); });