prettier config + format all, closes #175

This commit is contained in:
vcoppe 2025-02-02 11:17:22 +01:00
parent 01cfd448f0
commit 0b457f9a1e
157 changed files with 17194 additions and 29365 deletions

16
.prettierrc Normal file
View File

@ -0,0 +1,16 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"overrides": [
{
"files": "**/*.svelte",
"options": {
"plugins": ["prettier-plugin-svelte"],
"parser": "svelte"
}
}
]
}

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
]
}

13
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
}
}

1
gpx/.prettierignore Normal file
View File

@ -0,0 +1 @@
package-lock.json

1573
gpx/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,14 @@
"devDependencies": { "devDependencies": {
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.14",
"@types/node": "^20.16.10", "@types/node": "^20.16.10",
"@typescript-eslint/parser": "^8.22.0",
"prettier": "^3.4.2",
"typescript": "^5.6.2" "typescript": "^5.6.2"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postinstall": "npm run build" "postinstall": "npm run build",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,3 @@ export * from './gpx';
export { Coordinates, LineStyleExtension, WaypointType } from './types'; export { Coordinates, LineStyleExtension, WaypointType } from './types';
export { parseGPX, buildGPX } from './io'; export { parseGPX, buildGPX } from './io';
export * from './simplify'; export * from './simplify';

View File

@ -1,32 +1,40 @@
import { XMLParser, XMLBuilder } from "fast-xml-parser"; import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { GPXFileType } from "./types"; import { GPXFileType } from './types';
import { GPXFile } from "./gpx"; import { GPXFile } from './gpx';
const attributesWithNamespace = { const attributesWithNamespace = {
'RoutePointExtension': 'gpxx:RoutePointExtension', RoutePointExtension: 'gpxx:RoutePointExtension',
'rpt': 'gpxx:rpt', rpt: 'gpxx:rpt',
'TrackPointExtension': 'gpxtpx:TrackPointExtension', TrackPointExtension: 'gpxtpx:TrackPointExtension',
'PowerExtension': 'gpxpx:PowerExtension', PowerExtension: 'gpxpx:PowerExtension',
'atemp': 'gpxtpx:atemp', atemp: 'gpxtpx:atemp',
'hr': 'gpxtpx:hr', hr: 'gpxtpx:hr',
'cad': 'gpxtpx:cad', cad: 'gpxtpx:cad',
'Extensions': 'gpxtpx:Extensions', Extensions: 'gpxtpx:Extensions',
'PowerInWatts': 'gpxpx:PowerInWatts', PowerInWatts: 'gpxpx:PowerInWatts',
'power': 'gpxpx:PowerExtension', power: 'gpxpx:PowerExtension',
'line': 'gpx_style:line', line: 'gpx_style:line',
'color': 'gpx_style:color', color: 'gpx_style:color',
'opacity': 'gpx_style:opacity', opacity: 'gpx_style:opacity',
'width': 'gpx_style:width', width: 'gpx_style:width',
}; };
export function parseGPX(gpxData: string): GPXFile { export function parseGPX(gpxData: string): GPXFile {
const parser = new XMLParser({ const parser = new XMLParser({
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
removeNSPrefix: true, removeNSPrefix: true,
isArray(name: string) { isArray(name: string) {
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt'; return (
name === 'trk' ||
name === 'trkseg' ||
name === 'trkpt' ||
name === 'wpt' ||
name === 'rte' ||
name === 'rtept' ||
name === 'gpxx:rpt'
);
}, },
attributeValueProcessor(attrName, attrValue, jPath) { attributeValueProcessor(attrName, attrValue, jPath) {
if (attrName === 'lat' || attrName === 'lon') { if (attrName === 'lat' || attrName === 'lon') {
@ -51,8 +59,14 @@ export function parseGPX(gpxData: string): GPXFile {
return new Date(tagValue); return new Date(tagValue);
} }
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || if (
tagName === 'gpx_style:opacity' || tagName === 'gpx_style:width') { tagName === 'gpxtpx:atemp' ||
tagName === 'gpxtpx:hr' ||
tagName === 'gpxtpx:cad' ||
tagName === 'gpxpx:PowerInWatts' ||
tagName === 'gpx_style:opacity' ||
tagName === 'gpx_style:width'
) {
return parseFloat(tagValue); return parseFloat(tagValue);
} }
@ -60,7 +74,7 @@ export function parseGPX(gpxData: string): GPXFile {
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag // Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
// Note that this only targets the transformed <power> tag, since it must be a leaf node // Note that this only targets the transformed <power> tag, since it must be a leaf node
return { return {
'gpxpx:PowerInWatts': parseFloat(tagValue) 'gpxpx:PowerInWatts': parseFloat(tagValue),
}; };
} }
} }
@ -72,7 +86,7 @@ export function parseGPX(gpxData: string): GPXFile {
const parsed: GPXFileType = parser.parse(gpxData).gpx; const parsed: GPXFileType = parser.parse(gpxData).gpx;
// @ts-ignore // @ts-ignore
if (parsed.metadata === "") { if (parsed.metadata === '') {
parsed.metadata = {}; parsed.metadata = {};
} }
@ -85,7 +99,7 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
const builder = new XMLBuilder({ const builder = new XMLBuilder({
format: true, format: true,
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
suppressEmptyNode: true, suppressEmptyNode: true,
tagValueProcessor: (tagName: string, tagValue: unknown): string => { tagValueProcessor: (tagName: string, tagValue: unknown): string => {
@ -96,13 +110,13 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
}, },
}); });
if (!gpx.attributes) if (!gpx.attributes) gpx.attributes = {};
gpx.attributes = {};
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio'; gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
gpx.attributes['version'] = '1.1'; gpx.attributes['version'] = '1.1';
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1'; gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'; gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
gpx.attributes['xsi:schemaLocation'] = 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd'; gpx.attributes['xsi:schemaLocation'] =
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1'; gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3'; gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1'; gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
@ -113,19 +127,24 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
} }
return builder.build({ return builder.build({
"?xml": { '?xml': {
attributes: { attributes: {
version: "1.0", version: '1.0',
encoding: "UTF-8", encoding: 'UTF-8',
}
}, },
gpx: removeEmptyElements(gpx) },
gpx: removeEmptyElements(gpx),
}); });
} }
function removeEmptyElements(obj: GPXFileType): GPXFileType { function removeEmptyElements(obj: GPXFileType): GPXFileType {
for (const key in obj) { for (const key in obj) {
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) { if (
obj[key] === null ||
obj[key] === undefined ||
obj[key] === '' ||
(Array.isArray(obj[key]) && obj[key].length === 0)
) {
delete obj[key]; delete obj[key];
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) { } else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
removeEmptyElements(obj[key]); removeEmptyElements(obj[key]);

View File

@ -1,33 +1,48 @@
import { TrackPoint } from "./gpx"; import { TrackPoint } from './gpx';
import { Coordinates } from "./types"; import { Coordinates } from './types';
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number }; export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] { export function ramerDouglasPeucker(
points: TrackPoint[],
epsilon: number = 50,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
): SimplifiedTrackPoint[] {
if (points.length == 0) { if (points.length == 0) {
return []; return [];
} else if (points.length == 1) { } else if (points.length == 1) {
return [{ return [
point: points[0] {
}]; point: points[0],
},
];
} }
let simplified = [{ let simplified = [
point: points[0] {
}]; point: points[0],
},
];
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified); ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
simplified.push({ simplified.push({
point: points[points.length - 1] point: points[points.length - 1],
}); });
return simplified; return simplified;
} }
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) { function ramerDouglasPeuckerRecursive(
points: TrackPoint[],
epsilon: number,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
start: number,
end: number,
simplified: SimplifiedTrackPoint[]
) {
let largest = { let largest = {
index: 0, index: 0,
distance: 0 distance: 0,
}; };
for (let i = start + 1; i < end; i++) { for (let i = start + 1; i < end; i++) {
@ -45,8 +60,16 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, mea
} }
} }
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number { export function crossarcDistance(
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3); point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): number {
return crossarc(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
} }
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number { function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
} }
// Is relative bearing obtuse? // Is relative bearing obtuse?
if (diff > (Math.PI / 2)) { if (diff > Math.PI / 2) {
return dis13; return dis13;
} }
@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
// Is p4 beyond the arc? // Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2); let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) { if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3); return distance(lat2, lon2, lat3, lon3);
} else { } else {
@ -93,18 +117,32 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
function distance(latA: number, lonA: number, latB: number, lonB: number): number { function distance(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the distance between two lat / lon points. // Finds the distance between two lat / lon points.
return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius; return (
Math.acos(
Math.sin(latA) * Math.sin(latB) +
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
) * earthRadius
);
} }
function bearing(latA: number, lonA: number, latB: number, lonB: number): number { function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the bearing from one lat / lon point to another. // Finds the bearing from one lat / lon point to another.
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB), return Math.atan2(
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); Math.sin(lonB - lonA) * Math.cos(latB),
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
);
} }
export function projectedPoint(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): Coordinates { export function projectedPoint(
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3); point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): Coordinates {
return projected(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
} }
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates { function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
@ -132,7 +170,7 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
} }
// Is relative bearing obtuse? // Is relative bearing obtuse?
if (diff > (Math.PI / 2)) { if (diff > Math.PI / 2) {
return coord1; return coord1;
} }
@ -141,14 +179,22 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
// Is p4 beyond the arc? // Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2); let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius; let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) { if (dis14 > dis12) {
return coord2; return coord2;
} else { } else {
// Determine the closest point (p4) on the great circle // Determine the closest point (p4) on the great circle
const f = dis14 / earthRadius; const f = dis14 / earthRadius;
const lat4 = Math.asin(Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)); const lat4 = Math.asin(
const lon4 = lon1 + Math.atan2(Math.sin(bear12) * Math.sin(f) * Math.cos(lat1), Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)); Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
);
const lon4 =
lon1 +
Math.atan2(
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
);
return { lat: lat4 / rad, lon: lon4 / rad }; return { lat: lat4 / rad, lon: lon4 / rad };
} }

View File

@ -93,11 +93,11 @@ export type TrackPointExtension = {
'gpxtpx:hr'?: number; 'gpxtpx:hr'?: number;
'gpxtpx:cad'?: number; 'gpxtpx:cad'?: number;
'gpxtpx:Extensions'?: Record<string, string>; 'gpxtpx:Extensions'?: Record<string, string>;
} };
export type PowerExtension = { export type PowerExtension = {
'gpxpx:PowerInWatts'?: number; 'gpxpx:PowerInWatts'?: number;
} };
export type Author = { export type Author = {
name?: string; name?: string;
@ -114,12 +114,12 @@ export type RouteType = {
type?: string; type?: string;
extensions?: TrackExtensions; extensions?: TrackExtensions;
rtept: WaypointType[]; rtept: WaypointType[];
} };
export type RoutePointExtension = { export type RoutePointExtension = {
'gpxx:rpt'?: GPXXRoutePoint[]; 'gpxx:rpt'?: GPXXRoutePoint[];
} };
export type GPXXRoutePoint = { export type GPXXRoutePoint = {
attributes: Coordinates; attributes: Coordinates;
} };

View File

@ -4,9 +4,7 @@
"target": "ES2015", "target": "ES2015",
"declaration": true, "declaration": true,
"outDir": "./dist", "outDir": "./dist",
"moduleResolution": "node", "moduleResolution": "node"
}, },
"include": [ "include": ["src"]
"src"
],
} }

View File

@ -5,27 +5,27 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended', 'plugin:svelte/recommended',
'prettier' 'prettier',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020, ecmaVersion: 2020,
extraFileExtensions: ['.svelte'] extraFileExtensions: ['.svelte'],
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
}, },
overrides: [ overrides: [
{ {
files: ['*.svelte'], files: ['*.svelte'],
parser: 'svelte-eslint-parser', parser: 'svelte-eslint-parser',
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser',
} },
} },
] ],
}; };

View File

@ -2,3 +2,5 @@
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
yarn.lock yarn.lock
src/lib/components/ui
*.mdx

View File

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -1,15 +1,13 @@
<!doctype html> <!doctype html>
<html> <html>
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -72,7 +72,7 @@
--link: 80 190 255; --link: 80 190 255;
--ring: hsl(212.7,26.8%,83.9); --ring: hsl(212.7, 26.8%, 83.9);
} }
} }

View File

@ -46,7 +46,8 @@ export async function handle({ event, resolve }) {
} }
const response = await resolve(event, { const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag), transformPageChunk: ({ html }) =>
html.replace('<html>', htmlTag).replace('<head>', headTag),
}); });
return response; return response;

View File

@ -1,28 +1,28 @@
export const surfaceColors: { [key: string]: string } = { export const surfaceColors: { [key: string]: string } = {
"missing": "#d1d1d1", missing: '#d1d1d1',
"paved": "#8c8c8c", paved: '#8c8c8c',
"unpaved": "#6b443a", unpaved: '#6b443a',
"asphalt": "#8c8c8c", asphalt: '#8c8c8c',
"concrete": "#8c8c8c", concrete: '#8c8c8c',
"cobblestone": "#ffd991", cobblestone: '#ffd991',
"paving_stones": "#8c8c8c", paving_stones: '#8c8c8c',
"sett": "#ffd991", sett: '#ffd991',
"metal": "#8c8c8c", metal: '#8c8c8c',
"wood": "#6b443a", wood: '#6b443a',
"compacted": "#ffffa8", compacted: '#ffffa8',
"fine_gravel": "#ffffa8", fine_gravel: '#ffffa8',
"gravel": "#ffffa8", gravel: '#ffffa8',
"pebblestone": "#ffffa8", pebblestone: '#ffffa8',
"rock": "#ffd991", rock: '#ffd991',
"dirt": "#ffffa8", dirt: '#ffffa8',
"ground": "#6b443a", ground: '#6b443a',
"earth": "#6b443a", earth: '#6b443a',
"mud": "#6b443a", mud: '#6b443a',
"sand": "#ffffc4", sand: '#ffffc4',
"grass": "#61b55c", grass: '#61b55c',
"grass_paver": "#61b55c", grass_paver: '#61b55c',
"clay": "#6b443a", clay: '#6b443a',
"stone": "#ffd991", stone: '#ffd991',
}; };
export function getSurfaceColor(surface: string): string { export function getSurfaceColor(surface: string): string {
@ -30,66 +30,72 @@ export function getSurfaceColor(surface: string): string {
} }
export const highwayColors: { [key: string]: string } = { export const highwayColors: { [key: string]: string } = {
"missing": "#d1d1d1", missing: '#d1d1d1',
"motorway": "#ff4d33", motorway: '#ff4d33',
"motorway_link": "#ff4d33", motorway_link: '#ff4d33',
"trunk": "#ff5e4d", trunk: '#ff5e4d',
"trunk_link": "#ff947f", trunk_link: '#ff947f',
"primary": "#ff6e5c", primary: '#ff6e5c',
"primary_link": "#ff6e5c", primary_link: '#ff6e5c',
"secondary": "#ff8d7b", secondary: '#ff8d7b',
"secondary_link": "#ff8d7b", secondary_link: '#ff8d7b',
"tertiary": "#ffd75f", tertiary: '#ffd75f',
"tertiary_link": "#ffd75f", tertiary_link: '#ffd75f',
"unclassified": "#f1f2a5", unclassified: '#f1f2a5',
"road": "#f1f2a5", road: '#f1f2a5',
"residential": "#73b2ff", residential: '#73b2ff',
"living_street": "#73b2ff", living_street: '#73b2ff',
"service": "#9c9cd9", service: '#9c9cd9',
"track": "#a8e381", track: '#a8e381',
"footway": "#a8e381", footway: '#a8e381',
"path": "#a8e381", path: '#a8e381',
"pedestrian": "#a8e381", pedestrian: '#a8e381',
"cycleway": "#9de2ff", cycleway: '#9de2ff',
"construction": "#e09a4a", construction: '#e09a4a',
"bridleway": "#946f43", bridleway: '#946f43',
"raceway": "#ff0000", raceway: '#ff0000',
"rest_area": "#9c9cd9", rest_area: '#9c9cd9',
"services": "#9c9cd9", services: '#9c9cd9',
"corridor": "#474747", corridor: '#474747',
"elevator": "#474747", elevator: '#474747',
"steps": "#474747", steps: '#474747',
"bus_stop": "#8545a3", bus_stop: '#8545a3',
"busway": "#8545a3", busway: '#8545a3',
"via_ferrata": "#474747" via_ferrata: '#474747',
}; };
export const sacScaleColors: { [key: string]: string } = { export const sacScaleColors: { [key: string]: string } = {
"hiking": "#007700", hiking: '#007700',
"mountain_hiking": "#1843ad", mountain_hiking: '#1843ad',
"demanding_mountain_hiking": "#ffff00", demanding_mountain_hiking: '#ffff00',
"alpine_hiking": "#ff9233", alpine_hiking: '#ff9233',
"demanding_alpine_hiking": "#ff0000", demanding_alpine_hiking: '#ff0000',
"difficult_alpine_hiking": "#000000", difficult_alpine_hiking: '#000000',
}; };
export const mtbScaleColors: { [key: string]: string } = { export const mtbScaleColors: { [key: string]: string } = {
"0-": "#007700", '0-': '#007700',
"0": "#007700", '0': '#007700',
"0+": "#007700", '0+': '#007700',
"1-": "#1843ad", '1-': '#1843ad',
"1": "#1843ad", '1': '#1843ad',
"1+": "#1843ad", '1+': '#1843ad',
"2-": "#ffff00", '2-': '#ffff00',
"2": "#ffff00", '2': '#ffff00',
"2+": "#ffff00", '2+': '#ffff00',
"3": "#ff0000", '3': '#ff0000',
"4": "#00ff00", '4': '#00ff00',
"5": "#000000", '5': '#000000',
"6": "#b105eb", '6': '#b105eb',
}; };
function createPattern(backgroundColor: string, sacScaleColor: string | undefined, mtbScaleColor: string | undefined, size: number = 16, lineWidth: number = 4) { function createPattern(
backgroundColor: string,
sacScaleColor: string | undefined,
mtbScaleColor: string | undefined,
size: number = 16,
lineWidth: number = 4
) {
let canvas = document.createElement('canvas'); let canvas = document.createElement('canvas');
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
@ -104,11 +110,11 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
if (sacScaleColor) { if (sacScaleColor) {
ctx.strokeStyle = sacScaleColor; ctx.strokeStyle = sacScaleColor;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(halfSize - halfLineWidth, - halfLineWidth); ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth); ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
ctx.stroke(); ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(- halfLineWidth, halfSize - halfLineWidth); ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth); ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
ctx.stroke(); ctx.stroke();
} }
@ -119,8 +125,8 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth); ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
ctx.stroke(); ctx.stroke();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(- halfLineWidth, halfSize + halfLineWidth); ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, - halfLineWidth); ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
ctx.stroke(); ctx.stroke();
} }
} }
@ -128,12 +134,16 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
} }
const patterns: Record<string, string | CanvasPattern> = {}; const patterns: Record<string, string | CanvasPattern> = {};
export function getHighwayColor(highway: string, sacScale: string | undefined, mtbScale: string | undefined) { export function getHighwayColor(
highway: string,
sacScale: string | undefined,
mtbScale: string | undefined
) {
let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing; let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined; let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined; let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
if (sacScale || mtbScale) { if (sacScale || mtbScale) {
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter(x => x).join('-')}`; let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter((x) => x).join('-')}`;
if (!patterns[patternId]) { if (!patterns[patternId]) {
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor); patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,67 @@
import { Landmark, Icon, Shell, Bike, Building, Tent, Car, Wrench, ShoppingBasket, Droplet, DoorOpen, Trees, Fuel, Home, Info, TreeDeciduous, CircleParking, Cross, Utensils, Construction, BrickWall, ShowerHead, Mountain, Phone, TrainFront, Bed, Binoculars, TriangleAlert, Anchor, Toilet } from "lucide-svelte"; import {
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg, Toilet as ToiletSvg } from "lucide-static"; Landmark,
import type { ComponentType } from "svelte"; Icon,
Shell,
Bike,
Building,
Tent,
Car,
Wrench,
ShoppingBasket,
Droplet,
DoorOpen,
Trees,
Fuel,
Home,
Info,
TreeDeciduous,
CircleParking,
Cross,
Utensils,
Construction,
BrickWall,
ShowerHead,
Mountain,
Phone,
TrainFront,
Bed,
Binoculars,
TriangleAlert,
Anchor,
Toilet,
} from 'lucide-svelte';
import {
Landmark as LandmarkSvg,
Shell as ShellSvg,
Bike as BikeSvg,
Building as BuildingSvg,
Tent as TentSvg,
Car as CarSvg,
Wrench as WrenchSvg,
ShoppingBasket as ShoppingBasketSvg,
Droplet as DropletSvg,
DoorOpen as DoorOpenSvg,
Trees as TreesSvg,
Fuel as FuelSvg,
Home as HomeSvg,
Info as InfoSvg,
TreeDeciduous as TreeDeciduousSvg,
CircleParking as CircleParkingSvg,
Cross as CrossSvg,
Utensils as UtensilsSvg,
Construction as ConstructionSvg,
BrickWall as BrickWallSvg,
ShowerHead as ShowerHeadSvg,
Mountain as MountainSvg,
Phone as PhoneSvg,
TrainFront as TrainFrontSvg,
Bed as BedSvg,
Binoculars as BinocularsSvg,
TriangleAlert as TriangleAlertSvg,
Anchor as AnchorSvg,
Toilet as ToiletSvg,
} from 'lucide-static';
import type { ComponentType } from 'svelte';
export type Symbol = { export type Symbol = {
value: string; value: string;
@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg }, campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
car: { value: 'Car', icon: Car, iconSvg: CarSvg }, car: { value: 'Car', icon: Car, iconSvg: CarSvg },
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg }, car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg }, convenience_store: {
value: 'Convenience Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
crossing: { value: 'Crossing' }, crossing: { value: 'Crossing' },
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg }, department_store: {
value: 'Department Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg }, drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg }, exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg }, lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg }, lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg }, forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg }, gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg }, ground_transportation: {
value: 'Ground Transportation',
icon: TrainFront,
iconSvg: TrainFrontSvg,
},
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg }, hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
house: { value: 'House', icon: Home, iconSvg: HomeSvg }, house: { value: 'House', icon: Home, iconSvg: HomeSvg },
information: { value: 'Information', icon: Info, iconSvg: InfoSvg }, information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} else { } else {
return Object.keys(symbols).find(key => symbols[key].value === value); return Object.keys(symbols).find((key) => symbols[key].value === value);
} }
} }

View File

@ -13,14 +13,14 @@
indexName: 'gpx', indexName: 'gpx',
container: '#docsearch', container: '#docsearch',
searchParameters: { searchParameters: {
facetFilters: ['lang:' + ($locale ?? 'en')] facetFilters: ['lang:' + ($locale ?? 'en')],
}, },
placeholder: $_('docs.search.search'), placeholder: $_('docs.search.search'),
disableUserPersonalization: true, disableUserPersonalization: true,
translations: { translations: {
button: { button: {
buttonText: $_('docs.search.search'), buttonText: $_('docs.search.search'),
buttonAriaLabel: $_('docs.search.search') buttonAriaLabel: $_('docs.search.search'),
}, },
modal: { modal: {
searchBox: { searchBox: {
@ -28,19 +28,19 @@
resetButtonAriaLabel: $_('docs.search.clear'), resetButtonAriaLabel: $_('docs.search.clear'),
cancelButtonText: $_('docs.search.cancel'), cancelButtonText: $_('docs.search.cancel'),
cancelButtonAriaLabel: $_('docs.search.cancel'), cancelButtonAriaLabel: $_('docs.search.cancel'),
searchInputLabel: $_('docs.search.search') searchInputLabel: $_('docs.search.search'),
}, },
footer: { footer: {
selectText: $_('docs.search.to_select'), selectText: $_('docs.search.to_select'),
navigateText: $_('docs.search.to_navigate'), navigateText: $_('docs.search.to_navigate'),
closeText: $_('docs.search.to_close') closeText: $_('docs.search.to_close'),
}, },
noResultsScreen: { noResultsScreen: {
noResultsText: $_('docs.search.no_results'), noResultsText: $_('docs.search.no_results'),
suggestedQueryText: $_('docs.search.no_results_suggestion') suggestedQueryText: $_('docs.search.no_results_suggestion'),
} },
} },
} },
}); });
} }

View File

@ -9,9 +9,9 @@
item: new TrackPoint({ item: new TrackPoint({
attributes: { attributes: {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng lon: e.lngLat.lng,
} },
}) }),
}); });
}); });
} }

View File

@ -17,7 +17,7 @@
Circle, Circle,
Check, Check,
ChartNoAxesColumn, ChartNoAxesColumn,
Construction Construction,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors'; import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
@ -33,7 +33,7 @@
getHeartRateWithUnits, getHeartRateWithUnits,
getPowerWithUnits, getPowerWithUnits,
getTemperatureWithUnits, getTemperatureWithUnits,
getVelocityWithUnits getVelocityWithUnits,
} from '$lib/units'; } from '$lib/units';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import type { GPXStatistics } from 'gpx'; import type { GPXStatistics } from 'gpx';
@ -72,37 +72,37 @@
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`; return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
}, },
align: 'inner', align: 'inner',
maxRotation: 0 maxRotation: 0,
} },
}, },
y: { y: {
type: 'linear', type: 'linear',
ticks: { ticks: {
callback: function (value: number) { callback: function (value: number) {
return getElevationWithUnits(value, false); return getElevationWithUnits(value, false);
} },
} },
} },
}, },
datasets: { datasets: {
line: { line: {
pointRadius: 0, pointRadius: 0,
tension: 0.4, tension: 0.4,
borderWidth: 2, borderWidth: 2,
cubicInterpolationMode: 'monotone' cubicInterpolationMode: 'monotone',
} },
}, },
interaction: { interaction: {
mode: 'nearest', mode: 'nearest',
axis: 'x', axis: 'x',
intersect: false intersect: false,
}, },
plugins: { plugins: {
legend: { legend: {
display: false display: false,
}, },
decimation: { decimation: {
enabled: true enabled: true,
}, },
tooltip: { tooltip: {
enabled: () => !dragging && !panning, enabled: () => !dragging && !panning,
@ -141,16 +141,20 @@
let slope = { let slope = {
at: point.slope.at.toFixed(1), at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1), segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length) length: getDistanceWithUnits(point.slope.length),
}; };
let surface = point.extensions.surface ? point.extensions.surface : 'unknown'; let surface = point.extensions.surface
let highway = point.extensions.highway ? point.extensions.highway : 'unknown'; ? point.extensions.surface
: 'unknown';
let highway = point.extensions.highway
? point.extensions.highway
: 'unknown';
let sacScale = point.extensions.sac_scale; let sacScale = point.extensions.sac_scale;
let mtbScale = point.extensions.mtb_scale; let mtbScale = point.extensions.mtb_scale;
let labels = [ let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`, ` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}` ` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
]; ];
if (elevationFill === 'surface') { if (elevationFill === 'surface') {
@ -162,7 +166,9 @@
if (elevationFill === 'highway') { if (elevationFill === 'highway') {
labels.push( labels.push(
` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${ ` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${
sacScale ? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})` : '' sacScale
? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})`
: ''
}` }`
); );
if (mtbScale) { if (mtbScale) {
@ -175,8 +181,8 @@
} }
return labels; return labels;
} },
} },
}, },
zoom: { zoom: {
pan: { pan: {
@ -190,18 +196,19 @@
}, },
onPanComplete: function () { onPanComplete: function () {
panning = false; panning = false;
} },
}, },
zoom: { zoom: {
wheel: { wheel: {
enabled: true enabled: true,
}, },
mode: 'x', mode: 'x',
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) { onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
if ( if (
event.deltaY < 0 && event.deltaY < 0 &&
Math.abs( Math.abs(
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange - chart.getInitialScaleBounds().x.max /
chart.options.plugins.zoom.limits.x.minRange -
chart.getZoomLevel() chart.getZoomLevel()
) < 0.01 ) < 0.01
) { ) {
@ -210,21 +217,21 @@
} }
$slicedGPXStatistics = undefined; $slicedGPXStatistics = undefined;
} },
}, },
limits: { limits: {
x: { x: {
min: 'original', min: 'original',
max: 'original', max: 'original',
minRange: 1 minRange: 1,
} },
} },
} },
}, },
stacked: false, stacked: false,
onResize: function () { onResize: function () {
updateOverlay(); updateOverlay();
} },
}; };
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power']; let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
@ -233,10 +240,10 @@
type: 'linear', type: 'linear',
position: 'right', position: 'right',
grid: { grid: {
display: false display: false,
}, },
reverse: () => id === 'speed' && $velocityUnits === 'pace', reverse: () => id === 'speed' && $velocityUnits === 'pace',
display: false display: false,
}; };
}); });
@ -246,7 +253,7 @@
chart = new Chart(canvas, { chart = new Chart(canvas, {
type: 'line', type: 'line',
data: { data: {
datasets: [] datasets: [],
}, },
options, options,
plugins: [ plugins: [
@ -259,16 +266,16 @@
marker.remove(); marker.remove();
} }
} }
} },
} },
] ],
}); });
// Map marker to show on hover // Map marker to show on hover
let element = document.createElement('div'); let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white'; element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
marker = new mapboxgl.Marker({ marker = new mapboxgl.Marker({
element element,
}); });
let startIndex = 0; let startIndex = 0;
@ -278,7 +285,7 @@
evt, evt,
'x', 'x',
{ {
intersect: false intersect: false,
}, },
true true
); );
@ -321,9 +328,12 @@
startIndex = endIndex; startIndex = endIndex;
} else if (startIndex !== endIndex) { } else if (startIndex !== endIndex) {
$slicedGPXStatistics = [ $slicedGPXStatistics = [
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)), $gpxStatistics.slice(
Math.min(startIndex, endIndex), Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex) Math.max(startIndex, endIndex)
),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex),
]; ];
} }
} }
@ -357,76 +367,76 @@
slope: { slope: {
at: data.local.slope.at[index], at: data.local.slope.at[index],
segment: data.local.slope.segment[index], segment: data.local.slope.segment[index],
length: data.local.slope.length[index] length: data.local.slope.length[index],
}, },
extensions: point.getExtensions(), extensions: point.getExtensions(),
coordinates: point.getCoordinates(), coordinates: point.getCoordinates(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
fill: 'start', fill: 'start',
order: 1 order: 1,
}; };
chart.data.datasets[1] = { chart.data.datasets[1] = {
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]), y: getConvertedVelocity(data.local.speed[index]),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: 'yspeed', yAxisID: 'yspeed',
hidden: true hidden: true,
}; };
chart.data.datasets[2] = { chart.data.datasets[2] = {
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(), y: point.getHeartRate(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: 'yhr', yAxisID: 'yhr',
hidden: true hidden: true,
}; };
chart.data.datasets[3] = { chart.data.datasets[3] = {
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(), y: point.getCadence(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: 'ycad', yAxisID: 'ycad',
hidden: true hidden: true,
}; };
chart.data.datasets[4] = { chart.data.datasets[4] = {
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()), y: getConvertedTemperature(point.getTemperature()),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: 'yatemp', yAxisID: 'yatemp',
hidden: true hidden: true,
}; };
chart.data.datasets[5] = { chart.data.datasets[5] = {
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(), y: point.getPower(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: 'ypower', yAxisID: 'ypower',
hidden: true hidden: true,
}; };
chart.options.scales.x['min'] = 0; chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total); chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
@ -453,15 +463,15 @@
$: if (chart) { $: if (chart) {
if (elevationFill === 'slope') { if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback backgroundColor: slopeFillCallback,
}; };
} else if (elevationFill === 'surface') { } else if (elevationFill === 'surface') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback backgroundColor: surfaceFillCallback,
}; };
} else if (elevationFill === 'highway') { } else if (elevationFill === 'highway') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: highwayFillCallback backgroundColor: highwayFillCallback,
}; };
} else { } else {
chart.data.datasets[0]['segment'] = {}; chart.data.datasets[0]['segment'] = {};
@ -553,7 +563,11 @@
<ChartNoAxesColumn size="18" /> <ChartNoAxesColumn size="18" />
</ButtonWithTooltip> </ButtonWithTooltip>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="w-fit p-0 flex flex-col divide-y" side="top" sideOffset={-32}> <Popover.Content
class="w-fit p-0 flex flex-col divide-y"
side="top"
sideOffset={-32}
>
<ToggleGroup.Root <ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1" class="flex flex-col items-start gap-0 p-1"
type="single" type="single"
@ -613,7 +627,9 @@
{/if} {/if}
</div> </div>
<Zap size="15" class="mr-1" /> <Zap size="15" class="mr-1" />
{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} {$velocityUnits === 'speed'
? $_('quantities.speed')
: $_('quantities.pace')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"

View File

@ -10,7 +10,7 @@
exportSelectedFiles, exportSelectedFiles,
ExportState, ExportState,
exportState, exportState,
gpxStatistics gpxStatistics,
} from '$lib/stores'; } from '$lib/stores';
import { fileObservers } from '$lib/db'; import { fileObservers } from '$lib/db';
import { import {
@ -20,7 +20,7 @@
HeartPulse, HeartPulse,
Orbit, Orbit,
Thermometer, Thermometer,
SquareActivity SquareActivity,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { selection } from './file-list/Selection'; import { selection } from './file-list/Selection';
@ -35,7 +35,7 @@
cad: true, cad: true,
atemp: true, atemp: true,
power: true, power: true,
extensions: true extensions: true,
}; };
let hide: Record<string, boolean> = { let hide: Record<string, boolean> = {
time: false, time: false,
@ -43,7 +43,7 @@
cad: false, cad: false,
atemp: false, atemp: false,
power: false, power: false,
extensions: false extensions: false,
}; };
$: if ($exportState !== ExportState.NONE) { $: if ($exportState !== ExportState.NONE) {
@ -121,7 +121,9 @@
</Button> </Button>
</div> </div>
<div <div
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some((v) => !v) class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some(
(v) => !v
)
? '' ? ''
: 'hidden'}" : 'hidden'}"
> >
@ -144,7 +146,9 @@
{$_('quantities.time')} {$_('quantities.time')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"> <div
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
>
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} /> <Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
<Label for="export-extensions" class="flex flex-row items-center gap-1"> <Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" /> <Earth size="16" />

View File

@ -53,13 +53,17 @@
{#if panelSize > 120 || orientation === 'horizontal'} {#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip <Tooltip
class={orientation === 'horizontal' ? 'hidden xs:block' : ''} class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_( label="{$velocityUnits === 'speed'
'quantities.moving' ? $_('quantities.speed')
)} / {$_('quantities.total')})" : $_('quantities.pace')} ({$_('quantities.moving')} / {$_('quantities.total')})"
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Zap size="16" class="mr-1" /> <Zap size="16" class="mr-1" />
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} /> <WithUnits
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
@ -68,7 +72,9 @@
{#if panelSize > 160 || orientation === 'horizontal'} {#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip <Tooltip
class={orientation === 'horizontal' ? 'hidden md:block' : ''} class={orientation === 'horizontal' ? 'hidden md:block' : ''}
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})" label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_(
'quantities.total'
)})"
> >
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">
<Timer size="16" class="mr-1" /> <Timer size="16" class="mr-1" />

View File

@ -8,13 +8,13 @@
let selected = { let selected = {
value: '', value: '',
label: '' label: '',
}; };
$: if ($locale) { $: if ($locale) {
selected = { selected = {
value: $locale, value: $locale,
label: languages[$locale] label: languages[$locale],
}; };
} }
</script> </script>

View File

@ -26,13 +26,13 @@
let fitBoundsOptions: mapboxgl.FitBoundsOptions = { let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15, maxZoom: 15,
linear: true, linear: true,
easing: () => 1 easing: () => 1,
}; };
const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } = const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
settings; settings;
let scaleControl = new mapboxgl.ScaleControl({ let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits unit: $distanceUnits,
}); });
onMount(() => { onMount(() => {
@ -70,12 +70,12 @@
sources: {}, sources: {},
layers: [], layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf', glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}` sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`,
} },
}, },
{ {
id: 'basemap', id: 'basemap',
url: '' url: '',
}, },
{ {
id: 'overlays', id: 'overlays',
@ -83,10 +83,10 @@
data: { data: {
version: 8, version: 8,
sources: {}, sources: {},
layers: [] layers: [],
} },
} },
] ],
}, },
projection: 'globe', projection: 'globe',
zoom: 0, zoom: 0,
@ -94,7 +94,7 @@
language, language,
attributionControl: false, attributionControl: false,
logoPosition: 'bottom-right', logoPosition: 'bottom-right',
boxZoom: false boxZoom: false,
}); });
newMap.on('load', () => { newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded $map = newMap; // only set the store after the map has loaded
@ -104,13 +104,13 @@
newMap.addControl( newMap.addControl(
new mapboxgl.AttributionControl({ new mapboxgl.AttributionControl({
compact: true compact: true,
}) })
); );
newMap.addControl( newMap.addControl(
new mapboxgl.NavigationControl({ new mapboxgl.NavigationControl({
visualizePitch: true visualizePitch: true,
}) })
); );
@ -134,12 +134,12 @@
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: [result.lon, result.lat] coordinates: [result.lon, result.lat],
}, },
place_name: result.display_name place_name: result.display_name,
}; };
}); });
}) }),
}); });
let onKeyDown = geocoder._onKeyDown; let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => { geocoder._onKeyDown = (e: KeyboardEvent) => {
@ -157,11 +157,11 @@
newMap.addControl( newMap.addControl(
new mapboxgl.GeolocateControl({ new mapboxgl.GeolocateControl({
positionOptions: { positionOptions: {
enableHighAccuracy: true enableHighAccuracy: true,
}, },
fitBoundsOptions, fitBoundsOptions,
trackUserLocation: true, trackUserLocation: true,
showUserHeading: true showUserHeading: true,
}) })
); );
} }
@ -173,25 +173,25 @@
type: 'raster-dem', type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1', url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512, tileSize: 512,
maxzoom: 14 maxzoom: 14,
}); });
if (newMap.getPitch() > 0) { if (newMap.getPitch() > 0) {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: 1 exaggeration: 1,
}); });
} }
newMap.setFog({ newMap.setFog({
color: 'rgb(186, 210, 235)', color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)', 'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1, 'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)' 'space-color': 'rgb(156, 240, 255)',
}); });
newMap.on('pitch', () => { newMap.on('pitch', () => {
if (newMap.getPitch() > 0) { if (newMap.getPitch() > 0) {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: 1 exaggeration: 1,
}); });
} else { } else {
newMap.setTerrain(null); newMap.setTerrain(null);
@ -215,7 +215,8 @@
<div {...$$restProps}> <div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div> <div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
<div <div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported && !embeddedApp class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported &&
!embeddedApp
? 'hidden' ? 'hidden'
: ''} {embeddedApp ? 'z-30' : ''}" : ''} {embeddedApp ? 'z-30' : ''}"
> >

View File

@ -1,8 +1,8 @@
import { TrackPoint, Waypoint } from "gpx"; import { TrackPoint, Waypoint } from 'gpx';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { tick } from "svelte"; import { tick } from 'svelte';
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from 'svelte/store';
import MapPopupComponent from "./MapPopup.svelte"; import MapPopupComponent from './MapPopup.svelte';
export type PopupItem<T = Waypoint | TrackPoint | any> = { export type PopupItem<T = Waypoint | TrackPoint | any> = {
item: T; item: T;
@ -23,16 +23,15 @@ export class MapPopup {
let component = new MapPopupComponent({ let component = new MapPopupComponent({
target: document.body, target: document.body,
props: { props: {
item: this.item item: this.item,
} },
}); });
tick().then(() => this.popup.setDOMContent(component.container)); tick().then(() => this.popup.setDOMContent(component.container));
} }
setItem(item: PopupItem | null) { setItem(item: PopupItem | null) {
if (item) if (item) item.hide = () => this.hide();
item.hide = () => this.hide();
this.item.set(item); this.item.set(item);
if (item === null) { if (item === null) {
this.hide(); this.hide();
@ -76,6 +75,8 @@ export class MapPopup {
if (i === null) { if (i === null) {
return new mapboxgl.LngLat(0, 0); return new mapboxgl.LngLat(0, 0);
} }
return (i.item instanceof Waypoint || i.item instanceof TrackPoint) ? i.item.getCoordinates() : new mapboxgl.LngLat(i.item.lon, i.item.lat); return i.item instanceof Waypoint || i.item instanceof TrackPoint
? i.item.getCoordinates()
: new mapboxgl.LngLat(i.item.lon, i.item.lat);
} }
} }

View File

@ -42,7 +42,7 @@
FileX, FileX,
BookOpenText, BookOpenText,
ChartArea, ChartArea,
Maximize Maximize,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { import {
@ -56,7 +56,7 @@
editStyle, editStyle,
exportState, exportState,
ExportState, ExportState,
centerMapOnSelection centerMapOnSelection,
} from '$lib/stores'; } from '$lib/stores';
import { import {
copied, copied,
@ -64,7 +64,7 @@
cutSelection, cutSelection,
pasteSelection, pasteSelection,
selectAll, selectAll,
selection selection,
} from '$lib/components/file-list/Selection'; } from '$lib/components/file-list/Selection';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db'; import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
@ -91,7 +91,7 @@
distanceMarkers, distanceMarkers,
directionMarkers, directionMarkers,
streetViewSource, streetViewSource,
routing routing,
} = settings; } = settings;
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo); let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
@ -151,18 +151,27 @@
<Shortcut key="O" ctrl={true} /> <Shortcut key="O" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.duplicateSelection} disabled={$selection.size == 0}> <Menubar.Item
on:click={dbUtils.duplicateSelection}
disabled={$selection.size == 0}
>
<Copy size="16" class="mr-1" /> <Copy size="16" class="mr-1" />
{$_('menu.duplicate')} {$_('menu.duplicate')}
<Shortcut key="D" ctrl={true} /> <Shortcut key="D" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelectedFiles} disabled={$selection.size == 0}> <Menubar.Item
on:click={dbUtils.deleteSelectedFiles}
disabled={$selection.size == 0}
>
<FileX size="16" class="mr-1" /> <FileX size="16" class="mr-1" />
{$_('menu.close')} {$_('menu.close')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item on:click={dbUtils.deleteAllFiles} disabled={$fileObservers.size == 0}> <Menubar.Item
on:click={dbUtils.deleteAllFiles}
disabled={$fileObservers.size == 0}
>
<FileX size="16" class="mr-1" /> <FileX size="16" class="mr-1" />
{$_('menu.close_all')} {$_('menu.close_all')}
<Shortcut key="⌫" ctrl={true} shift={true} /> <Shortcut key="⌫" ctrl={true} shift={true} />
@ -207,7 +216,11 @@
disabled={$selection.size !== 1 || disabled={$selection.size !== 1 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editMetadata = true)} on:click={() => ($editMetadata = true)}
> >
<Info size="16" class="mr-1" /> <Info size="16" class="mr-1" />
@ -218,7 +231,11 @@
disabled={$selection.size === 0 || disabled={$selection.size === 0 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editStyle = true)} on:click={() => ($editStyle = true)}
> >
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" class="mr-1" />
@ -247,13 +264,16 @@
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)} {#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())} on:click={() =>
dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
disabled={$selection.size !== 1} disabled={$selection.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" class="mr-1" />
{$_('menu.new_track')} {$_('menu.new_track')}
</Menubar.Item> </Menubar.Item>
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)} {:else if $selection
.getSelected()
.some((item) => item instanceof ListTrackItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
on:click={() => { on:click={() => {
@ -300,7 +320,9 @@
disabled={$copied === undefined || disabled={$copied === undefined ||
$copied.length === 0 || $copied.length === 0 ||
($selection.size > 0 && ($selection.size > 0 &&
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))} !allowedPastes[$copied[0].level].includes(
$selection.getSelected().pop()?.level
))}
on:click={pasteSelection} on:click={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" class="mr-1" />
@ -309,7 +331,10 @@
</Menubar.Item> </Menubar.Item>
{/if} {/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}> <Menubar.Item
on:click={dbUtils.deleteSelection}
disabled={$selection.size == 0}
>
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{$_('menu.delete')} {$_('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
@ -334,17 +359,25 @@
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={switchBasemaps}> <Menubar.Item inset on:click={switchBasemaps}>
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut key="F1" /> <Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut
key="F1"
/>
</Menubar.Item> </Menubar.Item>
<Menubar.Item inset on:click={toggleOverlays}> <Menubar.Item inset on:click={toggleOverlays}>
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut key="F2" /> <Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut
key="F2"
/>
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.CheckboxItem bind:checked={$distanceMarkers}> <Menubar.CheckboxItem bind:checked={$distanceMarkers}>
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut key="F3" /> <Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut
key="F3"
/>
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$directionMarkers}> <Menubar.CheckboxItem bind:checked={$directionMarkers}>
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut key="F4" /> <Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut
key="F4"
/>
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={toggle3D}> <Menubar.Item inset on:click={toggle3D}>
@ -368,9 +401,15 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}> <Menubar.RadioGroup bind:value={$distanceUnits}>
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem> <Menubar.RadioItem value="metric"
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem> >{$_('menu.metric')}</Menubar.RadioItem
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem> >
<Menubar.RadioItem value="imperial"
>{$_('menu.imperial')}</Menubar.RadioItem
>
<Menubar.RadioItem value="nautical"
>{$_('menu.nautical')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@ -380,8 +419,12 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}> <Menubar.RadioGroup bind:value={$velocityUnits}>
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem> <Menubar.RadioItem value="speed"
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem> >{$_('quantities.speed')}</Menubar.RadioItem
>
<Menubar.RadioItem value="pace"
>{$_('quantities.pace')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@ -391,8 +434,12 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}> <Menubar.RadioGroup bind:value={$temperatureUnits}>
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem> <Menubar.RadioItem value="celsius"
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem> >{$_('menu.celsius')}</Menubar.RadioItem
>
<Menubar.RadioItem value="fahrenheit"
>{$_('menu.fahrenheit')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@ -428,8 +475,11 @@
setMode(value); setMode(value);
}} }}
> >
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem> <Menubar.RadioItem value="light"
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem> >{$_('menu.light')}</Menubar.RadioItem
>
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@ -441,8 +491,12 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$streetViewSource}> <Menubar.RadioGroup bind:value={$streetViewSource}>
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem> <Menubar.RadioItem value="mapillary"
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem> >{$_('menu.mapillary')}</Menubar.RadioItem
>
<Menubar.RadioItem value="google"
>{$_('menu.google')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>

View File

@ -12,7 +12,8 @@
const handleMouseMove = (event: PointerEvent) => { const handleMouseMove = (event: PointerEvent) => {
const newAfter = const newAfter =
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY); startAfter +
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
if (newAfter >= minAfter && newAfter <= maxAfter) { if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter; after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) { } else if (newAfter < minAfter && after !== minAfter) {

View File

@ -8,7 +8,7 @@
getDistanceUnits, getDistanceUnits,
getElevationUnits, getElevationUnits,
getVelocityUnits, getVelocityUnits,
secondsToHHMMSS secondsToHHMMSS,
} from '$lib/units'; } from '$lib/units';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';

View File

@ -18,7 +18,11 @@
class="w-full max-w-3xl" class="w-full max-w-3xl"
/> />
{:else if src === 'tools/split'} {:else if src === 'tools/split'}
<enhanced:img src="/src/lib/assets/img/docs/tools/split.png" {alt} class="w-full max-w-3xl" /> <enhanced:img
src="/src/lib/assets/img/docs/tools/split.png"
{alt}
class="w-full max-w-3xl"
/>
{/if} {/if}
</div> </div>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p> <p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>

View File

@ -1,39 +1,64 @@
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte"; import {
import type { ComponentType } from "svelte"; File,
FilePen,
View,
type Icon,
Settings,
Pencil,
MapPin,
Scissors,
CalendarClock,
Group,
Ungroup,
Filter,
SquareDashedMousePointer,
MountainSnow,
} from 'lucide-svelte';
import type { ComponentType } from 'svelte';
export const guides: Record<string, string[]> = { export const guides: Record<string, string[]> = {
'getting-started': [], 'getting-started': [],
menu: ['file', 'edit', 'view', 'settings'], menu: ['file', 'edit', 'view', 'settings'],
'files-and-stats': [], 'files-and-stats': [],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'], toolbar: [
'routing',
'poi',
'scissors',
'time',
'merge',
'extract',
'elevation',
'minify',
'clean',
],
'map-controls': [], 'map-controls': [],
'gpx': [], gpx: [],
'integration': [], integration: [],
'faq': [], faq: [],
}; };
export const guideIcons: Record<string, string | ComponentType<Icon>> = { export const guideIcons: Record<string, string | ComponentType<Icon>> = {
"getting-started": "🚀", 'getting-started': '🚀',
"menu": "📂 ⚙️", menu: '📂 ⚙️',
"file": File, file: File,
"edit": FilePen, edit: FilePen,
"view": View, view: View,
"settings": Settings, settings: Settings,
"files-and-stats": "🗂 📈", 'files-and-stats': '🗂 📈',
"toolbar": "🧰", toolbar: '🧰',
"routing": Pencil, routing: Pencil,
"poi": MapPin, poi: MapPin,
"scissors": Scissors, scissors: Scissors,
"time": CalendarClock, time: CalendarClock,
"merge": Group, merge: Group,
"extract": Ungroup, extract: Ungroup,
"elevation": MountainSnow, elevation: MountainSnow,
"minify": Filter, minify: Filter,
"clean": SquareDashedMousePointer, clean: SquareDashedMousePointer,
"map-controls": "🗺", 'map-controls': '🗺',
"gpx": "💾", gpx: '💾',
"integration": "{ 👩‍💻 }", integration: '{ 👩‍💻 }',
"faq": "🔮", faq: '🔮',
}; };
export function getPreviousGuide(currentGuide: string): string | undefined { export function getPreviousGuide(currentGuide: string): string | undefined {

View File

@ -12,7 +12,7 @@
embedding, embedding,
loadFile, loadFile,
map, map,
updateGPXData updateGPXData,
} from '$lib/stores'; } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db'; import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
@ -23,7 +23,7 @@
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions, getFilesFromEmbeddingOptions,
type EmbeddingOptions type EmbeddingOptions,
} from './Embedding'; } from './Embedding';
import { mode, setMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@ -37,7 +37,7 @@
temperatureUnits, temperatureUnits,
fileOrder, fileOrder,
distanceMarkers, distanceMarkers,
directionMarkers directionMarkers,
} = settings; } = settings;
export let useHash = true; export let useHash = true;
@ -50,7 +50,7 @@
distanceUnits: 'metric', distanceUnits: 'metric',
velocityUnits: 'speed', velocityUnits: 'speed',
temperatureUnits: 'celsius', temperatureUnits: 'celsius',
theme: 'system' theme: 'system',
}; };
function applyOptions() { function applyOptions() {
@ -74,12 +74,12 @@
let bounds = { let bounds = {
southWest: { southWest: {
lat: 90, lat: 90,
lon: 180 lon: 180,
}, },
northEast: { northEast: {
lat: -90, lat: -90,
lon: -180 lon: -180,
} },
}; };
fileObservers.update(($fileObservers) => { fileObservers.update(($fileObservers) => {
@ -96,12 +96,13 @@
id, id,
readable({ readable({
file, file,
statistics statistics,
}) })
); );
ids.push(id); ids.push(id);
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds; let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global
.bounds;
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat); bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon); bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
@ -130,12 +131,12 @@
bounds.southWest.lon, bounds.southWest.lon,
bounds.southWest.lat, bounds.southWest.lat,
bounds.northEast.lon, bounds.northEast.lon,
bounds.northEast.lat bounds.northEast.lat,
], ],
{ {
padding: 80, padding: 80,
linear: true, linear: true,
easing: () => 1 easing: () => 1,
} }
); );
} }
@ -143,7 +144,10 @@
} }
}); });
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) { if (
options.basemap !== $currentBasemap &&
allowedEmbeddingBasemaps.includes(options.basemap)
) {
$currentBasemap = options.basemap; $currentBasemap = options.basemap;
} }
@ -257,7 +261,7 @@
options.elevation.hr ? 'hr' : null, options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null, options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null, options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null options.elevation.power ? 'power' : null,
].filter((dataset) => dataset !== null)} ].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill} elevationFill={options.elevation.fill}
showControls={options.elevation.controls} showControls={options.elevation.controls}

View File

@ -39,14 +39,14 @@ export const defaultEmbeddingOptions = {
hr: false, hr: false,
cad: false, cad: false,
temp: false, temp: false,
power: false power: false,
}, },
distanceMarkers: false, distanceMarkers: false,
directionMarkers: false, directionMarkers: false,
distanceUnits: 'metric', distanceUnits: 'metric',
velocityUnits: 'speed', velocityUnits: 'speed',
temperatureUnits: 'celsius', temperatureUnits: 'celsius',
theme: 'system' theme: 'system',
}; };
export function getDefaultEmbeddingOptions(): EmbeddingOptions { export function getDefaultEmbeddingOptions(): EmbeddingOptions {
@ -59,7 +59,11 @@ export function getMergedEmbeddingOptions(
): EmbeddingOptions { ): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions)); const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) { for (const key in options) {
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) { if (
typeof options[key] === 'object' &&
options[key] !== null &&
!Array.isArray(options[key])
) {
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]); mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else { } else {
mergedOptions[key] = options[key]; mergedOptions[key] = options[key];
@ -79,7 +83,10 @@ export function getCleanedEmbeddingOptions(
cleanedOptions[key] !== null && cleanedOptions[key] !== null &&
!Array.isArray(cleanedOptions[key]) !Array.isArray(cleanedOptions[key])
) { ) {
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]); cleanedOptions[key] = getCleanedEmbeddingOptions(
cleanedOptions[key],
defaultOptions[key]
);
if (Object.keys(cleanedOptions[key]).length === 0) { if (Object.keys(cleanedOptions[key]).length === 0) {
delete cleanedOptions[key]; delete cleanedOptions[key];
} }
@ -141,7 +148,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
} }
if (options.has('slope')) { if (options.has('slope')) {
newOptions.elevation = { newOptions.elevation = {
fill: 'slope' fill: 'slope',
}; };
} }
return newOptions; return newOptions;

View File

@ -13,13 +13,13 @@
SquareActivity, SquareActivity,
Coins, Coins,
Milestone, Milestone,
Video Video,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
getCleanedEmbeddingOptions, getCleanedEmbeddingOptions,
getDefaultEmbeddingOptions getDefaultEmbeddingOptions,
} from './Embedding'; } from './Embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte'; import Embedding from './Embedding.svelte';
@ -30,7 +30,7 @@
let options = getDefaultEmbeddingOptions(); let options = getDefaultEmbeddingOptions();
options.token = 'YOUR_MAPBOX_TOKEN'; options.token = 'YOUR_MAPBOX_TOKEN';
options.files = [ options.files = [
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx' 'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx',
]; ];
let files = options.files[0]; let files = options.files[0];
@ -130,7 +130,11 @@
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1"> <div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
<Label class="flex flex-row items-center gap-2"> <Label class="flex flex-row items-center gap-2">
{$_('embedding.height')} {$_('embedding.height')}
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" /> <Input
type="number"
bind:value={options.elevation.height}
class="h-8 w-20"
/>
</Label> </Label>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<span class="shrink-0"> <span class="shrink-0">
@ -142,7 +146,11 @@
let value = selected?.value; let value = selected?.value;
if (value === 'none') { if (value === 'none') {
options.elevation.fill = undefined; options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface' || value === 'highway') { } else if (
value === 'slope' ||
value === 'surface' ||
value === 'highway'
) {
options.elevation.fill = value; options.elevation.fill = value;
} }
}} }}
@ -152,8 +160,10 @@
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item> <Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item> <Select.Item value="surface">{$_('quantities.surface')}</Select.Item
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item> >
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item
>
<Select.Item value="none">{$_('embedding.none')}</Select.Item> <Select.Item value="none">{$_('embedding.none')}</Select.Item>
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@ -318,7 +328,8 @@
<Label> <Label>
{$_('embedding.code')} {$_('embedding.code')}
</Label> </Label>
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all"> <pre
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<code class="language-html"> <code class="language-html">
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`} {`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
</code> </code>

View File

@ -1,8 +1,8 @@
import { dbUtils, getFile } from "$lib/db"; import { dbUtils, getFile } from '$lib/db';
import { freeze } from "immer"; import { freeze } from 'immer';
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx"; import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
import { selection } from "./Selection"; import { selection } from './Selection';
import { newGPXFile } from "$lib/stores"; import { newGPXFile } from '$lib/stores';
export enum ListLevel { export enum ListLevel {
ROOT, ROOT,
@ -10,7 +10,7 @@ export enum ListLevel {
TRACK, TRACK,
SEGMENT, SEGMENT,
WAYPOINTS, WAYPOINTS,
WAYPOINT WAYPOINT,
} }
export const allowedMoves: Record<ListLevel, ListLevel[]> = { export const allowedMoves: Record<ListLevel, ListLevel[]> = {
@ -19,7 +19,7 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK], [ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT], [ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS], [ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT] [ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
}; };
export const allowedPastes: Record<ListLevel, ListLevel[]> = { export const allowedPastes: Record<ListLevel, ListLevel[]> = {
@ -28,7 +28,7 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK], [ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT], [ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT], [ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT] [ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
}; };
export abstract class ListItem { export abstract class ListItem {
@ -322,7 +322,13 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
} }
} }
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) { export function moveItems(
fromParent: ListItem,
toParent: ListItem,
fromItems: ListItem[],
toItems: ListItem[],
remove: boolean = true
) {
if (fromItems.length === 0) { if (fromItems.length === 0) {
return; return;
} }
@ -338,11 +344,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
context.push(file.clone()); context.push(file.clone());
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
context.push(file.trk[item.getTrackIndex()].clone()); context.push(file.trk[item.getTrackIndex()].clone());
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) { } else if (
item instanceof ListTrackSegmentItem &&
item.getTrackIndex() < file.trk.length &&
item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length
) {
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone()); context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
context.push(file.wpt.map((wpt) => wpt.clone())); context.push(file.wpt.map((wpt) => wpt.clone()));
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) { } else if (
item instanceof ListWaypointItem &&
item.getWaypointIndex() < file.wpt.length
) {
context.push(file.wpt[item.getWaypointIndex()].clone()); context.push(file.wpt[item.getWaypointIndex()].clone());
} }
} }
@ -359,7 +372,12 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []); file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []); file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex(),
[]
);
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []); file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
@ -371,25 +389,43 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
toItems.forEach((item, i) => { toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) { if (context[i] instanceof Track) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]); file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
context[i],
]);
} else if (context[i] instanceof TrackSegment) { } else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({ file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
trkseg: [context[i]] new Track({
})]); trkseg: [context[i]],
}),
]);
} }
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) { } else if (
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]); item instanceof ListTrackSegmentItem &&
context[i] instanceof TrackSegment
) {
file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex() - 1,
[context[i]]
);
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) { if (
Array.isArray(context[i]) &&
context[i].length > 0 &&
context[i][0] instanceof Waypoint
) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]); file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) { } else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]); file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
} }
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) { } else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]); file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [
context[i],
]);
} }
}); });
} },
]; ];
if (fromParent instanceof ListRootItem) { if (fromParent instanceof ListRootItem) {
@ -400,7 +436,10 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
callbacks.splice(0, 1); callbacks.splice(0, 1);
} }
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => { dbUtils.applyEachToFilesAndGlobal(
files,
callbacks,
(files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => { toItems.forEach((item, i) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) { if (context[i] instanceof GPXFile) {
@ -421,14 +460,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
} else if (context[i] instanceof TrackSegment) { } else if (context[i] instanceof TrackSegment) {
let newFile = newGPXFile(); let newFile = newGPXFile();
newFile._data.id = item.getFileId(); newFile._data.id = item.getFileId();
newFile.replaceTracks(0, 0, [new Track({ newFile.replaceTracks(0, 0, [
trkseg: [context[i]] new Track({
})]); trkseg: [context[i]],
}),
]);
files.set(item.getFileId(), freeze(newFile)); files.set(item.getFileId(), freeze(newFile));
} }
} }
}); });
}, context); },
context
);
selection.update(($selection) => { selection.update(($selection) => {
$selection.clear(); $selection.clear();

View File

@ -5,7 +5,7 @@
TrackSegment, TrackSegment,
Waypoint, Waypoint,
type AnyGPXTreeElement, type AnyGPXTreeElement,
type GPXTreeElement type GPXTreeElement,
} from 'gpx'; } from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index'; import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { settings, type GPXFileWithStatistics } from '$lib/db'; import { settings, type GPXFileWithStatistics } from '$lib/db';
@ -19,7 +19,7 @@
ListWaypointItem, ListWaypointItem,
ListWaypointsItem, ListWaypointsItem,
type ListItem, type ListItem,
type ListTrackItem type ListTrackItem,
} from './FileList'; } from './FileList';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { selection } from './Selection'; import { selection } from './Selection';
@ -43,7 +43,8 @@
: node instanceof TrackSegment : node instanceof TrackSegment
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}` ? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint : node instanceof Waypoint
? (node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`) ? (node.name ??
`${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
: node instanceof GPXFile && item instanceof ListWaypointsItem : node instanceof GPXFile && item instanceof ListWaypointsItem
? $_('gpx.waypoints') ? $_('gpx.waypoints')
: ''; : '';

View File

@ -19,7 +19,7 @@
ListWaypointsItem, ListWaypointsItem,
allowedMoves, allowedMoves,
moveItems, moveItems,
type ListItem type ListItem,
} from './FileList'; } from './FileList';
import { selection } from './Selection'; import { selection } from './Selection';
import { isMac } from '$lib/utils'; import { isMac } from '$lib/utils';
@ -113,7 +113,7 @@
Sortable.utils.select(element); Sortable.utils.select(element);
element.scrollIntoView({ element.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest' block: 'nearest',
}); });
} else { } else {
Sortable.utils.deselect(element); Sortable.utils.deselect(element);
@ -155,7 +155,7 @@
group: { group: {
name: sortableLevel, name: sortableLevel,
pull: allowedMoves[sortableLevel], pull: allowedMoves[sortableLevel],
put: true put: true,
}, },
direction: orientation, direction: orientation,
forceAutoScrollFallback: true, forceAutoScrollFallback: true,
@ -233,16 +233,16 @@
moveItems(fromItem, toItem, fromItems, toItems); moveItems(fromItem, toItem, fromItems, toItems);
} }
} },
}); });
Object.defineProperty(sortable, '_item', { Object.defineProperty(sortable, '_item', {
value: item, value: item,
writable: true writable: true,
}); });
Object.defineProperty(sortable, '_waypointRoot', { Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot, value: waypointRoot,
writable: true writable: true,
}); });
} }

View File

@ -18,7 +18,7 @@
Maximize, Maximize,
Scissors, Scissors,
FileStack, FileStack,
FileX FileX,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { import {
ListFileItem, ListFileItem,
@ -26,7 +26,7 @@
ListTrackItem, ListTrackItem,
ListWaypointItem, ListWaypointItem,
allowedPastes, allowedPastes,
type ListItem type ListItem,
} from './FileList'; } from './FileList';
import { import {
copied, copied,
@ -36,7 +36,7 @@
pasteSelection, pasteSelection,
selectAll, selectAll,
selectItem, selectItem,
selection selection,
} from './Selection'; } from './Selection';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -47,7 +47,7 @@
embedding, embedding,
centerMapOnSelection, centerMapOnSelection,
gpxLayers, gpxLayers,
map map,
} from '$lib/stores'; } from '$lib/stores';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx'; import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
@ -177,7 +177,10 @@
if (layer && file) { if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
waypointPopup?.setItem({ item: waypoint, fileId: item.getFileId() }); waypointPopup?.setItem({
item: waypoint,
fileId: item.getFileId(),
});
} }
} }
} }
@ -195,19 +198,29 @@
<Waypoints size="16" class="mr-1 shrink-0" /> <Waypoints size="16" class="mr-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT} {:else if item.level === ListLevel.WAYPOINT}
{#if symbolKey && symbols[symbolKey].icon} {#if symbolKey && symbols[symbolKey].icon}
<svelte:component this={symbols[symbolKey].icon} size="16" class="mr-1 shrink-0" /> <svelte:component
this={symbols[symbolKey].icon}
size="16"
class="mr-1 shrink-0"
/>
{:else} {:else}
<MapPin size="16" class="mr-1 shrink-0" /> <MapPin size="16" class="mr-1 shrink-0" />
{/if} {/if}
{/if} {/if}
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}"> <span
class="grow select-none truncate {orientation === 'vertical'
? 'last:mr-2'
: ''}"
>
{label} {label}
</span> </span>
{#if hidden} {#if hidden}
<EyeOff <EyeOff
size="12" size="12"
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level === class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT ? 'mr-2'
: ''} {item.level === ListLevel.SEGMENT ||
item.level === ListLevel.WAYPOINT
? 'mr-3' ? 'mr-3'
: ''}" : ''}"
/> />

View File

@ -17,15 +17,15 @@
let name: string = let name: string =
node instanceof GPXFile node instanceof GPXFile
? node.metadata.name ?? '' ? (node.metadata.name ?? '')
: node instanceof Track : node instanceof Track
? node.name ?? '' ? (node.name ?? '')
: ''; : '';
let description: string = let description: string =
node instanceof GPXFile node instanceof GPXFile
? node.metadata.desc ?? '' ? (node.metadata.desc ?? '')
: node instanceof Track : node instanceof Track
? node.desc ?? '' ? (node.desc ?? '')
: ''; : '';
$: if (!open) { $: if (!open) {

View File

@ -1,12 +1,23 @@
import { get, writable } from "svelte/store"; import { get, writable } from 'svelte/store';
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList"; import {
import { fileObservers, getFile, getFileIds, settings } from "$lib/db"; ListFileItem,
ListItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
export class SelectionTreeType { export class SelectionTreeType {
item: ListItem; item: ListItem;
selected: boolean; selected: boolean;
children: { children: {
[key: string | number]: SelectionTreeType [key: string | number]: SelectionTreeType;
}; };
size: number = 0; size: number = 0;
@ -67,7 +78,11 @@ export class SelectionTreeType {
} }
hasAnyParent(item: ListItem, self: boolean = true): boolean { hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) { if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected; return this.selected;
} }
let id = item.getIdAtLevel(this.item.level); let id = item.getIdAtLevel(this.item.level);
@ -80,7 +95,11 @@ export class SelectionTreeType {
} }
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean { hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) { if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected; return this.selected;
} }
let id = item.getIdAtLevel(this.item.level); let id = item.getIdAtLevel(this.item.level);
@ -131,7 +150,7 @@ export class SelectionTreeType {
delete this.children[id]; delete this.children[id];
} }
} }
}; }
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem())); export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
@ -181,7 +200,10 @@ export function selectAll() {
let file = getFile(item.getFileId()); let file = getFile(item.getFileId());
if (file) { if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => { file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true); $selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true
);
}); });
} }
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
@ -205,14 +227,24 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
return selected; return selected;
} }
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) { export function applyToOrderedItemsFromFile(
selectedItems: ListItem[],
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
get(settings.fileOrder).forEach((fileId) => { get(settings.fileOrder).forEach((fileId) => {
let level: ListLevel | undefined = undefined; let level: ListLevel | undefined = undefined;
let items: ListItem[] = []; let items: ListItem[] = [];
selectedItems.forEach((item) => { selectedItems.forEach((item) => {
if (item.getFileId() === fileId) { if (item.getFileId() === fileId) {
level = item.level; level = item.level;
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) { if (
item instanceof ListFileItem ||
item instanceof ListTrackItem ||
item instanceof ListTrackSegmentItem ||
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem
) {
items.push(item); items.push(item);
} }
} }
@ -225,7 +257,10 @@ export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback:
}); });
} }
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) { export function applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse); applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
} }
@ -270,7 +305,11 @@ export function pasteSelection() {
let startIndex: number | undefined = undefined; let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) { if (fromItems[0].level === toParent.level) {
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) { if (
toParent instanceof ListTrackItem ||
toParent instanceof ListTrackSegmentItem ||
toParent instanceof ListWaypointItem
) {
startIndex = toParent.getId() + 1; startIndex = toParent.getId() + 1;
} }
toParent = toParent.getParent(); toParent = toParent.getParent();
@ -288,20 +327,41 @@ export function pasteSelection() {
fromItems.forEach((item, index) => { fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) { if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index)); toItems.push(
new ListTrackItem(
toParent.getFileId(),
(startIndex ?? toFile.trk.length) + index
)
);
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId())); toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index)); toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
} }
} else if (toParent instanceof ListTrackItem) { } else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex(); let toTrackIndex = toParent.getTrackIndex();
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index)); toItems.push(
new ListTrackSegmentItem(
toParent.getFileId(),
toTrackIndex,
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
)
);
} }
} else if (toParent instanceof ListWaypointsItem) { } else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index)); toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
} }
} }
}); });

View File

@ -60,10 +60,16 @@
let track = file.trk[item.getTrackIndex()]; let track = file.trk[item.getTrackIndex()];
let style = track.getStyle(); let style = track.getStyle();
if (style) { if (style) {
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) { if (
style['gpx_style:color'] &&
!colors.includes(style['gpx_style:color'])
) {
colors.push(style['gpx_style:color']); colors.push(style['gpx_style:color']);
} }
if (style['gpx_style:opacity'] && !opacity.includes(style['gpx_style:opacity'])) { if (
style['gpx_style:opacity'] &&
!opacity.includes(style['gpx_style:opacity'])
) {
opacity.push(style['gpx_style:opacity']); opacity.push(style['gpx_style:opacity']);
} }
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) { if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {

View File

@ -1,11 +1,17 @@
import { settings } from '$lib/db';
import { settings } from "$lib/db"; import { gpxStatistics } from '$lib/stores';
import { gpxStatistics } from "$lib/stores"; import { get } from 'svelte/store';
import { get } from "svelte/store";
const { distanceMarkers, distanceUnits } = settings; const { distanceMarkers, distanceUnits } = settings;
const stops = [[100, 0], [50, 7], [25, 8, 10], [10, 10], [5, 11], [1, 13]]; const stops = [
[100, 0],
[50, 7],
[25, 8, 10],
[10, 10],
[5, 11],
[1, 13],
];
export class DistanceMarkers { export class DistanceMarkers {
map: mapboxgl.Map; map: mapboxgl.Map;
@ -30,7 +36,7 @@ export class DistanceMarkers {
} else { } else {
this.map.addSource('distance-markers', { this.map.addSource('distance-markers', {
type: 'geojson', type: 'geojson',
data: this.getDistanceMarkersGeoJSON() data: this.getDistanceMarkersGeoJSON(),
}); });
} }
stops.forEach(([d, minzoom, maxzoom]) => { stops.forEach(([d, minzoom, maxzoom]) => {
@ -39,7 +45,14 @@ export class DistanceMarkers {
id: `distance-markers-${d}`, id: `distance-markers-${d}`,
type: 'symbol', type: 'symbol',
source: 'distance-markers', source: 'distance-markers',
filter: d === 5 ? ['any', ['==', ['get', 'level'], 5], ['==', ['get', 'level'], 25]] : ['==', ['get', 'level'], d], filter:
d === 5
? [
'any',
['==', ['get', 'level'], 5],
['==', ['get', 'level'], 25],
]
: ['==', ['get', 'level'], d],
minzoom: minzoom, minzoom: minzoom,
maxzoom: maxzoom ?? 24, maxzoom: maxzoom ?? 24,
layout: { layout: {
@ -51,7 +64,7 @@ export class DistanceMarkers {
'text-color': 'black', 'text-color': 'black',
'text-halo-width': 2, 'text-halo-width': 2,
'text-halo-color': 'white', 'text-halo-color': 'white',
} },
}); });
} else { } else {
this.map.moveLayer(`distance-markers-${d}`); this.map.moveLayer(`distance-markers-${d}`);
@ -64,13 +77,14 @@ export class DistanceMarkers {
} }
}); });
} }
} catch (e) { // No reliable way to check if the map is ready to add sources and layers } catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return; return;
} }
} }
remove() { remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
} }
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection { getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
@ -79,20 +93,28 @@ export class DistanceMarkers {
let features = []; let features = [];
let currentTargetDistance = 1; let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.total.length; i++) { for (let i = 0; i < statistics.local.distance.total.length; i++) {
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) { if (
statistics.local.distance.total[i] >=
currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)
) {
let distance = currentTargetDistance.toFixed(0); let distance = currentTargetDistance.toFixed(0);
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [0, 0]; let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
0, 0,
];
features.push({ features.push({
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()] coordinates: [
statistics.local.points[i].getLongitude(),
statistics.local.points[i].getLatitude(),
],
}, },
properties: { properties: {
distance, distance,
level, level,
minzoom, minzoom,
} },
} as GeoJSON.Feature); } as GeoJSON.Feature);
currentTargetDistance += 1; currentTargetDistance += 1;
} }
@ -100,7 +122,7 @@ export class DistanceMarkers {
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',
features features,
}; };
} }
} }

View File

@ -1,14 +1,28 @@
import { currentTool, map, Tool } from "$lib/stores"; import { currentTool, map, Tool } from '$lib/stores';
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db"; import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
import { get, type Readable } from "svelte/store"; import { get, type Readable } from 'svelte/store';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { waypointPopup, deleteWaypoint, trackpointPopup } from "./GPXLayerPopup"; import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection"; import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList"; import {
import { getClosestLinePoint, getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils"; ListTrackSegmentItem,
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte"; ListWaypointItem,
import { MapPin, Square } from "lucide-static"; ListWaypointsItem,
import { getSymbolKey, symbols } from "$lib/assets/symbols"; ListTrackItem,
ListFileItem,
ListRootItem,
} from '$lib/components/file-list/FileList';
import {
getClosestLinePoint,
getElevation,
resetCursor,
setGrabbingCursor,
setPointerCursor,
setScissorsCursor,
} from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/Waypoint.svelte';
import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
const colors = [ const colors = [
'#ff0000', '#ff0000',
@ -21,7 +35,7 @@ const colors = [
'#288228', '#288228',
'#9933ff', '#9933ff',
'#50f0be', '#50f0be',
'#8c645a' '#8c645a',
]; ];
const colorCount: { [key: string]: number } = {}; const colorCount: { [key: string]: number } = {};
@ -56,12 +70,12 @@ class KeyDown {
if (e.key === this.key) { if (e.key === this.key) {
this.down = true; this.down = true;
} }
} };
onKeyUp = (e: KeyboardEvent) => { onKeyUp = (e: KeyboardEvent) => {
if (e.key === this.key) { if (e.key === this.key) {
this.down = false; this.down = false;
} }
} };
isDown() { isDown() {
return this.down; return this.down;
} }
@ -70,22 +84,26 @@ class KeyDown {
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) { function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined; let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square ${Square.replace('width="24"', 'width="12"')
.replace('width="24"', 'width="12"')
.replace('height="24"', 'height="12"') .replace('height="24"', 'height="12"')
.replace('stroke="currentColor"', 'stroke="SteelBlue"') .replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"') .replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)} .replace('fill="none"', `fill="${layerColor}"`)}
${MapPin ${MapPin.replace('width="24"', '')
.replace('width="24"', '')
.replace('height="24"', '') .replace('height="24"', '')
.replace('stroke="currentColor"', '') .replace('stroke="currentColor"', '')
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`) .replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)} .replace(
${symbolSvg?.replace('width="24"', 'width="10"') 'circle',
`circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`
)}
${
symbolSvg
?.replace('width="24"', 'width="10"')
.replace('height="24"', 'height="10"') .replace('height="24"', 'height="10"')
.replace('stroke="currentColor"', 'stroke="white"') .replace('stroke="currentColor"', 'stroke="white"')
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''} .replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''
}
</svg>`; </svg>`;
} }
@ -108,13 +126,18 @@ export class GPXLayer {
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this); layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) { constructor(
map: mapboxgl.Map,
fileId: string,
file: Readable<GPXFileWithStatistics | undefined>
) {
this.map = map; this.map = map;
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
this.layerColor = getColor(); this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded)); this.unsubscribe.push(file.subscribe(this.updateBinded));
this.unsubscribe.push(selection.subscribe($selection => { this.unsubscribe.push(
selection.subscribe(($selection) => {
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId)); let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
if (this.selected || newSelected) { if (this.selected || newSelected) {
this.selected = newSelected; this.selected = newSelected;
@ -123,17 +146,20 @@ export class GPXLayer {
if (newSelected) { if (newSelected) {
this.moveToFront(); this.moveToFront();
} }
})); })
);
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(currentTool.subscribe(tool => { this.unsubscribe.push(
currentTool.subscribe((tool) => {
if (tool === Tool.WAYPOINT && !this.draggable) { if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true; this.draggable = true;
this.markers.forEach(marker => marker.setDraggable(true)); this.markers.forEach((marker) => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) { } else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false; this.draggable = false;
this.markers.forEach(marker => marker.setDraggable(false)); this.markers.forEach((marker) => marker.setDraggable(false));
} }
})); })
);
this.draggable = get(currentTool) === Tool.WAYPOINT; this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.import.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
@ -149,7 +175,11 @@ export class GPXLayer {
return; return;
} }
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) { if (
file._data.style &&
file._data.style.color &&
this.layerColor !== `#${file._data.style.color}`
) {
decrementColor(this.layerColor); decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`; this.layerColor = `#${file._data.style.color}`;
} }
@ -161,7 +191,7 @@ export class GPXLayer {
} else { } else {
this.map.addSource(this.fileId, { this.map.addSource(this.fileId, {
type: 'geojson', type: 'geojson',
data: this.getGeoJSON() data: this.getGeoJSON(),
}); });
} }
@ -172,13 +202,13 @@ export class GPXLayer {
source: this.fileId, source: this.fileId,
layout: { layout: {
'line-join': 'round', 'line-join': 'round',
'line-cap': 'round' 'line-cap': 'round',
}, },
paint: { paint: {
'line-color': ['get', 'color'], 'line-color': ['get', 'color'],
'line-width': ['get', 'width'], 'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'] 'line-opacity': ['get', 'opacity'],
} },
}); });
this.map.on('click', this.fileId, this.layerOnClickBinded); this.map.on('click', this.fileId, this.layerOnClickBinded);
@ -190,7 +220,8 @@ export class GPXLayer {
if (get(directionMarkers)) { if (get(directionMarkers)) {
if (!this.map.getLayer(this.fileId + '-direction')) { if (!this.map.getLayer(this.fileId + '-direction')) {
this.map.addLayer({ this.map.addLayer(
{
id: this.fileId + '-direction', id: this.fileId + '-direction',
type: 'symbol', type: 'symbol',
source: this.fileId, source: this.fileId,
@ -208,9 +239,11 @@ export class GPXLayer {
'text-color': 'white', 'text-color': 'white',
'text-opacity': 0.7, 'text-opacity': 0.7,
'text-halo-width': 0.2, 'text-halo-width': 0.2,
'text-halo-color': 'white' 'text-halo-color': 'white',
} },
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined); },
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
);
} }
} else { } else {
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
@ -225,23 +258,53 @@ export class GPXLayer {
} }
}); });
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false }); this.map.setFilter(
this.fileId,
[
'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false }); this.map.setFilter(
this.fileId + '-direction',
[
'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
} }
} catch (e) { // No reliable way to check if the map is ready to add sources and layers } catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return; return;
} }
let markerIndex = 0; let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) { if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => { // Update markers file.wpt.forEach((waypoint) => {
// Update markers
let symbolKey = getSymbolKey(waypoint.sym); let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) { if (markerIndex < this.markers.length) {
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor); this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
symbolKey,
this.layerColor
);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates()); this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true }); Object.defineProperty(this.markers[markerIndex], '_waypoint', {
value: waypoint,
writable: true,
});
} else { } else {
let element = document.createElement('div'); let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl'); element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
@ -249,7 +312,7 @@ export class GPXLayer {
let marker = new mapboxgl.Marker({ let marker = new mapboxgl.Marker({
draggable: this.draggable, draggable: this.draggable,
element, element,
anchor: 'bottom' anchor: 'bottom',
}).setLngLat(waypoint.getCoordinates()); }).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true }); Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0; let dragEndTimestamp = 0;
@ -272,10 +335,20 @@ export class GPXLayer {
} }
if (get(treeFileView)) { if (get(treeFileView)) {
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) { if (
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); (e.ctrlKey || e.metaKey) &&
get(selection).hasAnyChildren(
new ListWaypointsItem(this.fileId),
false
)
) {
addSelectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} else { } else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); selectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} }
} else if (get(currentTool) === Tool.WAYPOINT) { } else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]); selectedWaypoint.set([marker._waypoint, this.fileId]);
@ -298,12 +371,12 @@ export class GPXLayer {
let wpt = file.wpt[marker._waypoint._data.index]; let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({ wpt.setCoordinates({
lat: latLng.lat, lat: latLng.lat,
lon: latLng.lng lon: latLng.lng,
}); });
wpt.ele = ele[0]; wpt.ele = ele[0];
}); });
}); });
dragEndTimestamp = Date.now() dragEndTimestamp = Date.now();
}); });
this.markers.push(marker); this.markers.push(marker);
} }
@ -311,7 +384,8 @@ export class GPXLayer {
}); });
} }
while (markerIndex < this.markers.length) { // Remove extra markers while (markerIndex < this.markers.length) {
// Remove extra markers
this.markers.pop()?.remove(); this.markers.pop()?.remove();
} }
@ -364,7 +438,10 @@ export class GPXLayer {
this.map.moveLayer(this.fileId); this.map.moveLayer(this.fileId);
} }
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined); this.map.moveLayer(
this.fileId + '-direction',
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
);
} }
} }
@ -372,7 +449,12 @@ export class GPXLayer {
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
get(currentTool) === Tool.SCISSORS &&
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
setScissorsCursor(); setScissorsCursor();
} else { } else {
setPointerCursor(); setPointerCursor();
@ -390,22 +472,36 @@ export class GPXLayer {
const file = get(this.file)?.file; const file = get(this.file)?.file;
if (file) { if (file) {
const closest = getClosestLinePoint(file.trk[trackIndex].trkseg[segmentIndex].trkpt, { lat: e.lngLat.lat, lon: e.lngLat.lng }); const closest = getClosestLinePoint(
file.trk[trackIndex].trkseg[segmentIndex].trkpt,
{ lat: e.lngLat.lat, lon: e.lngLat.lng }
);
trackpointPopup?.setItem({ item: closest, fileId: this.fileId }); trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
} }
} }
} }
layerOnClick(e: any) { layerOnClick(e: any) {
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) { if (
get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
) {
return; return;
} }
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng }); get(currentTool) === Tool.SCISSORS &&
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
dbUtils.split(this.fileId, trackIndex, segmentIndex, {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
});
return; return;
} }
@ -415,8 +511,12 @@ export class GPXLayer {
} }
let item = undefined; let item = undefined;
if (get(treeFileView) && file.getSegments().length > 1) { // Select inner item if (get(treeFileView) && file.getSegments().length > 1) {
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex); // Select inner item
item =
file.children[trackIndex].children.length > 1
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
: new ListTrackItem(this.fileId, trackIndex);
} else { } else {
item = new ListFileItem(this.fileId); item = new ListFileItem(this.fileId);
} }
@ -439,13 +539,14 @@ export class GPXLayer {
if (!file) { if (!file) {
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: [],
}; };
} }
let data = file.toGeoJSON(); let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0; let trackIndex = 0,
segmentIndex = 0;
for (let feature of data.features) { for (let feature of data.features) {
if (!feature.properties) { if (!feature.properties) {
feature.properties = {}; feature.properties = {};
@ -459,7 +560,12 @@ export class GPXLayer {
if (!feature.properties.width) { if (!feature.properties.width) {
feature.properties.width = get(defaultWidth); feature.properties.width = get(defaultWidth);
} }
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) { if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) ||
get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)
) {
feature.properties.width = feature.properties.width + 2; feature.properties.width = feature.properties.width + 2;
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1); feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
} }

View File

@ -1,5 +1,5 @@
import { dbUtils } from "$lib/db"; import { dbUtils } from '$lib/db';
import { MapPopup } from "$lib/components/MapPopup"; import { MapPopup } from '$lib/components/MapPopup';
export let waypointPopup: MapPopup | null = null; export let waypointPopup: MapPopup | null = null;
export let trackpointPopup: MapPopup | null = null; export let trackpointPopup: MapPopup | null = null;
@ -11,14 +11,14 @@ export function createPopups(map: mapboxgl.Map) {
focusAfterOpen: false, focusAfterOpen: false,
maxWidth: undefined, maxWidth: undefined,
offset: { offset: {
'top': [0, 0], top: [0, 0],
'top-left': [0, 0], 'top-left': [0, 0],
'top-right': [0, 0], 'top-right': [0, 0],
'bottom': [0, -30], bottom: [0, -30],
'bottom-left': [0, -30], 'bottom-left': [0, -30],
'bottom-right': [0, -30], 'bottom-right': [0, -30],
'left': [10, -15], left: [10, -15],
'right': [-10, -15], right: [-10, -15],
}, },
}); });
trackpointPopup = new MapPopup(map, { trackpointPopup = new MapPopup(map, {

View File

@ -1,6 +1,6 @@
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores"; import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export class StartEndMarkers { export class StartEndMarkers {
map: mapboxgl.Map; map: mapboxgl.Map;
@ -16,7 +16,8 @@ export class StartEndMarkers {
let endElement = document.createElement('div'); let endElement = document.createElement('div');
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`; startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
endElement.className = `h-4 w-4 rounded-full border-2 border-white`; endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
endElement.style.background = 'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round'; endElement.style.background =
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
this.start = new mapboxgl.Marker({ element: startElement }); this.start = new mapboxgl.Marker({ element: startElement });
this.end = new mapboxgl.Marker({ element: endElement }); this.end = new mapboxgl.Marker({ element: endElement });
@ -31,7 +32,11 @@ export class StartEndMarkers {
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics); let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) { if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map); this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
this.end.setLngLat(statistics.local.points[statistics.local.points.length - 1].getCoordinates()).addTo(this.map); this.end
.setLngLat(
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
)
.addTo(this.map);
} else { } else {
this.start.remove(); this.start.remove();
this.end.remove(); this.end.remove();
@ -39,7 +44,7 @@ export class StartEndMarkers {
} }
remove() { remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this.start.remove(); this.start.remove();
this.end.remove(); this.end.remove();

View File

@ -25,8 +25,8 @@
allowedTags: ['a', 'br', 'img'], allowedTags: ['a', 'br', 'img'],
allowedAttributes: { allowedAttributes: {
a: ['href', 'target'], a: ['href', 'target'],
img: ['src'] img: ['src'],
} },
}).trim(); }).trim();
} }
</script> </script>
@ -61,7 +61,9 @@
</span> </span>
<Dot size="16" /> <Dot size="16" />
{/if} {/if}
{waypoint.item.getLatitude().toFixed(6)}&deg; {waypoint.item.getLongitude().toFixed(6)}&deg; {waypoint.item.getLatitude().toFixed(6)}&deg; {waypoint.item
.getLongitude()
.toFixed(6)}&deg;
{#if waypoint.item.ele !== undefined} {#if waypoint.item.ele !== undefined}
<Dot size="16" /> <Dot size="16" />
<WithUnits value={waypoint.item.ele} type="elevation" /> <WithUnits value={waypoint.item.ele} type="elevation" />

View File

@ -15,7 +15,7 @@
Trash2, Trash2,
Move, Move,
Map, Map,
Layers2 Layers2,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
@ -34,7 +34,7 @@
currentOverlays, currentOverlays,
previousOverlays, previousOverlays,
customBasemapOrder, customBasemapOrder,
customOverlayOrder customOverlayOrder,
} = settings; } = settings;
let name: string = ''; let name: string = '';
@ -68,7 +68,7 @@
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} },
}); });
overlaySortable = Sortable.create(overlayContainer, { overlaySortable = Sortable.create(overlayContainer, {
onSort: (e) => { onSort: (e) => {
@ -77,7 +77,7 @@
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} },
}); });
basemapSortable.sort($customBasemapOrder); basemapSortable.sort($customBasemapOrder);
@ -118,7 +118,7 @@
maxZoom: maxZoom, maxZoom: maxZoom,
layerType: layerType, layerType: layerType,
resourceType: resourceType, resourceType: resourceType,
value: '' value: '',
}; };
if (resourceType === 'vector') { if (resourceType === 'vector') {
@ -131,16 +131,16 @@
type: 'raster', type: 'raster',
tiles: layer.tileUrls, tiles: layer.tileUrls,
tileSize: is512 ? 512 : 256, tileSize: is512 ? 512 : 256,
maxzoom: maxZoom maxzoom: maxZoom,
} },
}, },
layers: [ layers: [
{ {
id: layerId, id: layerId,
type: 'raster', type: 'raster',
source: layerId source: layerId,
} },
] ],
}; };
} }
$customLayers[layerId] = layer; $customLayers[layerId] = layer;
@ -230,7 +230,10 @@
layerId layerId
); );
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) { if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom'); $selectedBasemapTree.basemaps = tryDeleteLayer(
$selectedBasemapTree.basemaps,
'custom'
);
} }
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId); $customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
} else { } else {
@ -247,7 +250,10 @@
layerId layerId
); );
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) { if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom'); $selectedOverlayTree.overlays = tryDeleteLayer(
$selectedOverlayTree.overlays,
'custom'
);
} }
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId); $customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
@ -367,7 +373,8 @@
/> />
{#if tileUrls.length > 1} {#if tileUrls.length > 1}
<Button <Button
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))} on:click={() =>
(tileUrls = tileUrls.filter((_, index) => index !== i))}
variant="outline" variant="outline"
class="p-1 h-8" class="p-1 h-8"
> >
@ -387,7 +394,14 @@
{/each} {/each}
{#if resourceType === 'raster'} {#if resourceType === 'raster'}
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label> <Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" /> <Input
type="number"
bind:value={maxZoom}
id="maxZoom"
min={0}
max={22}
class="h-8"
/>
{/if} {/if}
<Label>{$_('layers.custom_layers.layer_type')}</Label> <Label>{$_('layers.custom_layers.layer_type')}</Label>
<RadioGroup.Root bind:value={layerType} class="flex flex-row"> <RadioGroup.Root bind:value={layerType} class="flex flex-row">

View File

@ -26,7 +26,7 @@
selectedOverlayTree, selectedOverlayTree,
selectedOverpassTree, selectedOverpassTree,
customLayers, customLayers,
opacities opacities,
} = settings; } = settings;
function setStyle() { function setStyle() {
@ -41,7 +41,7 @@
$map.addImport( $map.addImport(
{ {
id: 'basemap', id: 'basemap',
data: basemap data: basemap,
}, },
'overlays' 'overlays'
); );
@ -70,12 +70,12 @@
layer.paint['raster-opacity'] = $opacities[id]; layer.paint['raster-opacity'] = $opacities[id];
} }
return layer; return layer;
}) }),
}; };
} }
$map.addImport({ $map.addImport({
id, id,
data: overlay data: overlay,
}); });
} }
} catch (e) { } catch (e) {

View File

@ -14,7 +14,7 @@
defaultBasemap, defaultBasemap,
overlays, overlays,
overlayTree, overlayTree,
overpassTree overpassTree,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils'; import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
@ -31,7 +31,7 @@
currentBasemap, currentBasemap,
currentOverlays, currentOverlays,
customLayers, customLayers,
opacities opacities,
} = settings; } = settings;
export let open: boolean; export let open: boolean;
@ -137,7 +137,9 @@
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto"> <Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(overlays) as id} {#each Object.keys(overlays) as id}
{#if isSelected($selectedOverlayTree, id)} {#if isSelected($selectedOverlayTree, id)}
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item> <Select.Item value={id}
>{$_(`layers.label.${id}`)}</Select.Item
>
{/if} {/if}
{/each} {/each}
{#each Object.entries($customLayers) as [id, layer]} {#each Object.entries($customLayers) as [id, layer]}
@ -159,7 +161,13 @@
disabled={$selectedOverlay === undefined} disabled={$selectedOverlay === undefined}
onValueChange={(value) => { onValueChange={(value) => {
if ($selectedOverlay) { if ($selectedOverlay) {
if ($map && isSelected($currentOverlays, $selectedOverlay.value)) { if (
$map &&
isSelected(
$currentOverlays,
$selectedOverlay.value
)
) {
try { try {
$map.removeImport($selectedOverlay.value); $map.removeImport($selectedOverlay.value);
} catch (e) { } catch (e) {

View File

@ -49,7 +49,13 @@
aria-label={$_(`layers.label.${id}`)} aria-label={$_(`layers.label.${id}`)}
/> />
{:else} {:else}
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} /> <input
id="{name}-{id}"
type="radio"
{name}
value={id}
bind:group={selected}
/>
{/if} {/if}
<Label for="{name}-{id}" class="flex flex-row items-center gap-1"> <Label for="{name}-{id}" class="flex flex-row items-center gap-1">
{#if $customLayers.hasOwnProperty(id)} {#if $customLayers.hasOwnProperty(id)}
@ -64,7 +70,13 @@
<CollapsibleTreeNode {id}> <CollapsibleTreeNode {id}>
<span slot="trigger">{$_(`layers.label.${id}`)}</span> <span slot="trigger">{$_(`layers.label.${id}`)}</span>
<div slot="content"> <div slot="content">
<svelte:self node={node[id]} {name} bind:selected {multiple} bind:checked={checked[id]} /> <svelte:self
node={node[id]}
{name}
bind:selected
{multiple}
bind:checked={checked[id]}
/>
</div> </div>
</CollapsibleTreeNode> </CollapsibleTreeNode>
{/if} {/if}

View File

@ -1,14 +1,12 @@
import SphericalMercator from "@mapbox/sphericalmercator"; import SphericalMercator from '@mapbox/sphericalmercator';
import { getLayers } from "./utils"; import { getLayers } from './utils';
import { get, writable } from "svelte/store"; import { get, writable } from 'svelte/store';
import { liveQuery } from "dexie"; import { liveQuery } from 'dexie';
import { db, settings } from "$lib/db"; import { db, settings } from '$lib/db';
import { overpassQueryData } from "$lib/assets/layers"; import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from "$lib/components/MapPopup"; import { MapPopup } from '$lib/components/MapPopup';
const { const { currentOverpassQueries } = settings;
currentOverpassQueries
} = settings;
const mercator = new SphericalMercator({ const mercator = new SphericalMercator({
size: 256, size: 256,
@ -29,7 +27,7 @@ export class OverpassLayer {
popup: MapPopup; popup: MapPopup;
currentQueries: Set<string> = new Set(); currentQueries: Set<string> = new Set();
nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map(); nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
unsubscribes: (() => void)[] = []; unsubscribes: (() => void)[] = [];
queryIfNeededBinded = this.queryIfNeeded.bind(this); queryIfNeededBinded = this.queryIfNeeded.bind(this);
@ -50,10 +48,12 @@ export class OverpassLayer {
this.map.on('moveend', this.queryIfNeededBinded); this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.import.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded)); this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(currentOverpassQueries.subscribe(() => { this.unsubscribes.push(
currentOverpassQueries.subscribe(() => {
this.updateBinded(); this.updateBinded();
this.queryIfNeededBinded(); this.queryIfNeededBinded();
})); })
);
this.update(); this.update();
} }
@ -126,8 +126,8 @@ export class OverpassLayer {
this.popup.setItem({ this.popup.setItem({
item: { item: {
...e.features[0].properties, ...e.features[0].properties,
sym: overpassQueryData[e.features[0].properties.query].symbol ?? '' sym: overpassQueryData[e.features[0].properties.query].symbol ?? '',
} },
}); });
} }
@ -146,8 +146,19 @@ export class OverpassLayer {
continue; continue;
} }
db.overpasstiles.where('[x+y]').equals([x, y]).toArray().then((querytiles) => { db.overpasstiles
let missingQueries = queries.filter((query) => !querytiles.some((querytile) => querytile.query === query && time - querytile.time < this.expirationTime)); .where('[x+y]')
.equals([x, y])
.toArray()
.then((querytiles) => {
let missingQueries = queries.filter(
(query) =>
!querytiles.some(
(querytile) =>
querytile.query === query &&
time - querytile.time < this.expirationTime
)
);
if (missingQueries.length > 0) { if (missingQueries.length > 0) {
this.queryTile(x, y, missingQueries); this.queryTile(x, y, missingQueries);
} }
@ -165,13 +176,16 @@ export class OverpassLayer {
const bounds = mercator.bbox(x, y, this.queryZoom); const bounds = mercator.bbox(x, y, this.queryZoom);
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`) fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
.then((response) => { .then(
(response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
this.currentQueries.delete(`${x},${y}`); this.currentQueries.delete(`${x},${y}`);
return Promise.reject(); return Promise.reject();
}, () => (this.currentQueries.delete(`${x},${y}`))) },
() => this.currentQueries.delete(`${x},${y}`)
)
.then((data) => this.storeOverpassData(x, y, queries, data)) .then((data) => this.storeOverpassData(x, y, queries, data))
.catch(() => this.currentQueries.delete(`${x},${y}`)); .catch(() => this.currentQueries.delete(`${x},${y}`));
} }
@ -179,7 +193,7 @@ export class OverpassLayer {
storeOverpassData(x: number, y: number, queries: string[], data: any) { storeOverpassData(x: number, y: number, queries: string[], data: any) {
let time = Date.now(); let time = Date.now();
let queryTiles = queries.map((query) => ({ x, y, query, time })); let queryTiles = queries.map((query) => ({ x, y, query, time }));
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = []; let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
if (data.elements === undefined) { if (data.elements === undefined) {
return; return;
@ -195,7 +209,9 @@ export class OverpassLayer {
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: element.center ? [element.center.lon, element.center.lat] : [element.lon, element.lat], coordinates: element.center
? [element.center.lon, element.center.lat]
: [element.lon, element.lat],
}, },
properties: { properties: {
id: element.id, id: element.id,
@ -203,9 +219,9 @@ export class OverpassLayer {
lon: element.center ? element.center.lon : element.lon, lon: element.center ? element.center.lon : element.lon,
query: query, query: query,
icon: `overpass-${query}`, icon: `overpass-${query}`,
tags: element.tags tags: element.tags,
},
}, },
}
}); });
} }
} }
@ -228,11 +244,13 @@ export class OverpassLayer {
if (!this.map.hasImage(`overpass-${query}`)) { if (!this.map.hasImage(`overpass-${query}`)) {
this.map.addImage(`overpass-${query}`, icon); this.map.addImage(`overpass-${query}`, icon);
} }
} };
// Lucide icons are SVG files with a 24x24 viewBox // Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle // Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src = 'data:image/svg+xml,' + encodeURIComponent(` icon.src =
'data:image/svg+xml,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" /> <circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
<g transform="translate(8 8)"> <g transform="translate(8 8)">
@ -264,9 +282,14 @@ function getQuery(query: string) {
function getQueryItem(tags: Record<string, string | boolean | string[]>) { function getQueryItem(tags: Record<string, string | boolean | string[]>) {
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value)); let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
if (arrayEntry !== undefined) { if (arrayEntry !== undefined) {
return arrayEntry[1].map((val) => `nwr${Object.entries(tags) return arrayEntry[1]
.map(
(val) =>
`nwr${Object.entries(tags)
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`) .map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
.join('')};`).join(''); .join('')};`
)
.join('');
} else { } else {
return `nwr${Object.entries(tags) return `nwr${Object.entries(tags)
.map(([tag, value]) => `[${tag}=${value}]`) .map(([tag, value]) => `[${tag}=${value}]`)
@ -283,8 +306,9 @@ function belongsToQuery(element: any, query: string) {
} }
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) { function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
return Object.entries(tags) return Object.entries(tags).every(([tag, value]) =>
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value); Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
);
} }
function getCurrentQueries() { function getCurrentQueries() {
@ -293,5 +317,7 @@ function getCurrentQueries() {
return []; return [];
} }
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query); return Object.entries(getLayers(currentQueries))
.filter(([_, selected]) => selected)
.map(([query, _]) => query);
} }

View File

@ -79,12 +79,12 @@
dbUtils.addOrUpdateWaypoint({ dbUtils.addOrUpdateWaypoint({
attributes: { attributes: {
lat: poi.item.lat, lat: poi.item.lat,
lon: poi.item.lon lon: poi.item.lon,
}, },
name: name, name: name,
desc: desc, desc: desc,
cmt: desc, cmt: desc,
sym: poi.item.sym sym: poi.item.sym,
}); });
}} }}
> >

View File

@ -1,9 +1,10 @@
import type { LayerTreeType } from "$lib/assets/layers"; import type { LayerTreeType } from '$lib/assets/layers';
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
export function anySelectedLayer(node: LayerTreeType) { export function anySelectedLayer(node: LayerTreeType) {
return Object.keys(node).find((id) => { return (
if (typeof node[id] == "boolean") { Object.keys(node).find((id) => {
if (typeof node[id] == 'boolean') {
if (node[id]) { if (node[id]) {
return true; return true;
} }
@ -13,12 +14,16 @@ export function anySelectedLayer(node: LayerTreeType) {
} }
} }
return false; return false;
}) !== undefined; }) !== undefined
);
} }
export function getLayers(node: LayerTreeType, layers: { [key: string]: boolean } = {}): { [key: string]: boolean } { export function getLayers(
node: LayerTreeType,
layers: { [key: string]: boolean } = {}
): { [key: string]: boolean } {
Object.keys(node).forEach((id) => { Object.keys(node).forEach((id) => {
if (typeof node[id] == "boolean") { if (typeof node[id] == 'boolean') {
layers[id] = node[id]; layers[id] = node[id];
} else { } else {
getLayers(node[id], layers); getLayers(node[id], layers);
@ -32,7 +37,7 @@ export function isSelected(node: LayerTreeType, id: string) {
if (key === id) { if (key === id) {
return node[key]; return node[key];
} }
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) { if (typeof node[key] !== 'boolean' && isSelected(node[key], id)) {
return true; return true;
} }
return false; return false;
@ -43,7 +48,7 @@ export function toggle(node: LayerTreeType, id: string) {
Object.keys(node).forEach((key) => { Object.keys(node).forEach((key) => {
if (key === id) { if (key === id) {
node[key] = !node[key]; node[key] = !node[key];
} else if (typeof node[key] !== "boolean") { } else if (typeof node[key] !== 'boolean') {
toggle(node[key], id); toggle(node[key], id);
} }
}); });

View File

@ -1,5 +1,5 @@
import { resetCursor, setCrosshairCursor } from "$lib/utils"; import { resetCursor, setCrosshairCursor } from '$lib/utils';
import type mapboxgl from "mapbox-gl"; import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect { export class GoogleRedirect {
map: mapboxgl.Map; map: mapboxgl.Map;

View File

@ -1,12 +1,14 @@
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from "mapbox-gl"; import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { resetCursor, setPointerCursor } from "$lib/utils"; import { resetCursor, setPointerCursor } from '$lib/utils';
import type { Writable } from "svelte/store"; import type { Writable } from 'svelte/store';
const mapillarySource: VectorSourceSpecification = { const mapillarySource: VectorSourceSpecification = {
type: 'vector', type: 'vector',
tiles: ['https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011'], tiles: [
'https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011',
],
minzoom: 6, minzoom: 6,
maxzoom: 14, maxzoom: 14,
}; };
@ -70,7 +72,7 @@ export class MapillaryLayer {
this.marker = new mapboxgl.Marker({ this.marker = new mapboxgl.Marker({
rotationAlignment: 'map', rotationAlignment: 'map',
element element,
}); });
this.viewer.on('position', async () => { this.viewer.on('position', async () => {

View File

@ -10,7 +10,7 @@
MapPin, MapPin,
Filter, Filter,
Scissors, Scissors,
MountainSnow MountainSnow,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';

View File

@ -24,7 +24,7 @@
onMount(() => { onMount(() => {
popup = new mapboxgl.Popup({ popup = new mapboxgl.Popup({
closeButton: false, closeButton: false,
maxWidth: undefined maxWidth: undefined,
}); });
popup.setDOMContent(popupElement); popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden'); popupElement.classList.remove('hidden');

View File

@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
enum CleanType { enum CleanType {
INSIDE = 'inside', INSIDE = 'inside',
OUTSIDE = 'outside' OUTSIDE = 'outside',
} }
</script> </script>
@ -41,10 +41,10 @@
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat], [rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat], [rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
[rectangleCoordinates[0].lng, rectangleCoordinates[1].lat], [rectangleCoordinates[0].lng, rectangleCoordinates[1].lat],
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat] [rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
] ],
] ],
} },
}; };
let source = $map.getSource('rectangle'); let source = $map.getSource('rectangle');
if (source) { if (source) {
@ -52,7 +52,7 @@
} else { } else {
$map.addSource('rectangle', { $map.addSource('rectangle', {
type: 'geojson', type: 'geojson',
data: data data: data,
}); });
} }
if (!$map.getLayer('rectangle')) { if (!$map.getLayer('rectangle')) {
@ -62,8 +62,8 @@
source: 'rectangle', source: 'rectangle',
paint: { paint: {
'fill-color': 'SteelBlue', 'fill-color': 'SteelBlue',
'fill-opacity': 0.5 'fill-opacity': 0.5,
} },
}); });
} }
} }
@ -161,12 +161,12 @@
[ [
{ {
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat), lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng) lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
}, },
{ {
lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat), lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng) lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
} },
], ],
cleanType === CleanType.INSIDE, cleanType === CleanType.INSIDE,
deleteTrackpoints, deleteTrackpoints,

View File

@ -7,7 +7,7 @@
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
ListWaypointsItem ListWaypointsItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';

View File

@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
enum MergeType { enum MergeType {
TRACES = 'traces', TRACES = 'traces',
CONTENTS = 'contents' CONTENTS = 'contents',
} }
</script> </script>

View File

@ -3,7 +3,11 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList'; import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { Filter } from 'lucide-svelte'; import { Filter } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
@ -35,7 +39,7 @@
let data: GeoJSON.FeatureCollection = { let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: [],
}; };
simplified.forEach(([item, maxPts, points], itemFullId) => { simplified.forEach(([item, maxPts, points], itemFullId) => {
@ -52,10 +56,10 @@
type: 'LineString', type: 'LineString',
coordinates: current.map((point) => [ coordinates: current.map((point) => [
point.point.getLongitude(), point.point.getLongitude(),
point.point.getLatitude() point.point.getLatitude(),
]) ]),
}, },
properties: {} properties: {},
}); });
}); });
@ -66,7 +70,7 @@
} else { } else {
$map.addSource('simplified', { $map.addSource('simplified', {
type: 'geojson', type: 'geojson',
data: data data: data,
}); });
} }
if (!$map.getLayer('simplified')) { if (!$map.getLayer('simplified')) {
@ -76,8 +80,8 @@
source: 'simplified', source: 'simplified',
paint: { paint: {
'line-color': 'white', 'line-color': 'white',
'line-width': 3 'line-width': 3,
} },
}); });
} else { } else {
$map.moveLayer('simplified'); $map.moveLayer('simplified');
@ -94,17 +98,23 @@
}); });
$fileObservers.forEach((fileStore, fileId) => { $fileObservers.forEach((fileStore, fileId) => {
if (!unsubscribes.has(fileId)) { if (!unsubscribes.has(fileId)) {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe( let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
([fs, sel]) => { fs,
sel,
]).subscribe(([fs, sel]) => {
if (fs) { if (fs) {
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => { fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex); let segmentItem = new ListTrackSegmentItem(
fileId,
trackIndex,
segmentIndex
);
if (sel.hasAnyParent(segmentItem)) { if (sel.hasAnyParent(segmentItem)) {
let statistics = fs.statistics.getStatisticsFor(segmentItem); let statistics = fs.statistics.getStatisticsFor(segmentItem);
simplified.set(segmentItem.getFullId(), [ simplified.set(segmentItem.getFullId(), [
segmentItem, segmentItem,
statistics.local.points.length, statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, minTolerance) ramerDouglasPeucker(statistics.local.points, minTolerance),
]); ]);
update(); update();
} else if (simplified.has(segmentItem.getFullId())) { } else if (simplified.has(segmentItem.getFullId())) {
@ -113,8 +123,7 @@
} }
}); });
} }
} });
);
unsubscribes.set(fileId, unsubscribe); unsubscribes.set(fileId, unsubscribe);
} }
}); });

View File

@ -11,7 +11,7 @@
distancePerHourToSecondsPerDistance, distancePerHourToSecondsPerDistance,
getConvertedVelocity, getConvertedVelocity,
milesToKilometers, milesToKilometers,
nauticalMilesToKilometers nauticalMilesToKilometers,
} from '$lib/units'; } from '$lib/units';
import { CalendarDate, type DateValue } from '@internationalized/date'; import { CalendarDate, type DateValue } from '@internationalized/date';
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte'; import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
@ -23,7 +23,7 @@
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem ListTrackSegmentItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
@ -305,7 +305,11 @@
class="grow whitespace-normal h-fit" class="grow whitespace-normal h-fit"
on:click={() => { on:click={() => {
let effectiveSpeed = getSpeed(); let effectiveSpeed = getSpeed();
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) { if (
startDate === undefined ||
startTime === undefined ||
effectiveSpeed === undefined
) {
return; return;
} }
@ -326,9 +330,16 @@
dbUtils.applyToFile(fileId, (file) => { dbUtils.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (artificial) { if (artificial) {
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime); file.createArtificialTimestamps(
getDate(startDate, startTime),
movingTime
);
} else { } else {
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio); file.changeTimestamps(
getDate(startDate, startTime),
effectiveSpeed,
ratio
);
} }
} else if (item instanceof ListTrackItem) { } else if (item instanceof ListTrackItem) {
if (artificial) { if (artificial) {

View File

@ -31,7 +31,7 @@
let selectedSymbol = { let selectedSymbol = {
value: '', value: '',
label: '' label: '',
}; };
const { treeFileView } = settings; const { treeFileView } = settings;
@ -74,12 +74,12 @@
if (symbolKey) { if (symbolKey) {
selectedSymbol = { selectedSymbol = {
value: symbol, value: symbol,
label: $_(`gpx.symbol.${symbolKey}`) label: $_(`gpx.symbol.${symbolKey}`),
}; };
} else { } else {
selectedSymbol = { selectedSymbol = {
value: symbol, value: symbol,
label: '' label: '',
}; };
} }
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6)); longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
@ -99,7 +99,7 @@
link = ''; link = '';
selectedSymbol = { selectedSymbol = {
value: '', value: '',
label: '' label: '',
}; };
longitude = 0; longitude = 0;
latitude = 0; latitude = 0;
@ -134,13 +134,13 @@
{ {
attributes: { attributes: {
lat: latitude, lat: latitude,
lon: longitude lon: longitude,
}, },
name: name.length > 0 ? name : undefined, name: name.length > 0 ? name : undefined,
desc: description.length > 0 ? description : undefined, desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined, cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined, link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined,
}, },
$selectedWaypoint $selectedWaypoint
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index) ? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
@ -195,7 +195,11 @@
/> />
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label> <Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
<Select.Root bind:selected={selectedSymbol}> <Select.Root bind:selected={selectedSymbol}>
<Select.Trigger id="symbol" class="w-full h-8" disabled={!canCreate && !$selectedWaypoint}> <Select.Trigger
id="symbol"
class="w-full h-8"
disabled={!canCreate && !$selectedWaypoint}
>
<Select.Value /> <Select.Value />
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll"> <Select.Content class="max-h-60 overflow-y-scroll">
@ -218,7 +222,12 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<Label for="link">{$_('toolbar.waypoint.link')}</Label> <Label for="link">{$_('toolbar.waypoint.link')}</Label>
<Input bind:value={link} id="link" class="h-8" disabled={!canCreate && !$selectedWaypoint} /> <Input
bind:value={link}
id="link"
class="h-8"
disabled={!canCreate && !$selectedWaypoint}
/>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div class="grow"> <div class="grow">
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label> <Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>

View File

@ -19,7 +19,7 @@
RouteOff, RouteOff,
Repeat, Repeat,
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight SquareArrowOutDownRight,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores'; import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
@ -37,7 +37,7 @@
ListRootItem, ListRootItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
type ListItem type ListItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -68,7 +68,10 @@
// add controls for new files // add controls for new files
$fileObservers.forEach((file, fileId) => { $fileObservers.forEach((file, fileId) => {
if (!routingControls.has(fileId)) { if (!routingControls.has(fileId)) {
routingControls.set(fileId, new RoutingControls($map, fileId, file, popup, popupElement)); routingControls.set(
fileId,
new RoutingControls($map, fileId, file, popup, popupElement)
);
} }
}); });
} }
@ -82,9 +85,9 @@
new TrackPoint({ new TrackPoint({
attributes: { attributes: {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng lon: e.lngLat.lng,
} },
}) }),
]); ]);
file._data.id = getFileIds(1)[0]; file._data.id = getFileIds(1)[0];
dbUtils.add(file); dbUtils.add(file);
@ -195,7 +198,8 @@
if (selected[0] instanceof ListFileItem) { if (selected[0] instanceof ListFileItem) {
return firstFile.trk[0]?.trkseg[0]?.trkpt[0]; return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
} else if (selected[0] instanceof ListTrackItem) { } else if (selected[0] instanceof ListTrackItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0]; return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]
?.trkpt[0];
} else if (selected[0] instanceof ListTrackSegmentItem) { } else if (selected[0] instanceof ListTrackSegmentItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[ return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
selected[0].getSegmentIndex() selected[0].getSegmentIndex()

View File

@ -1,9 +1,9 @@
import type { Coordinates } from "gpx"; import type { Coordinates } from 'gpx';
import { TrackPoint, distance } from "gpx"; import { TrackPoint, distance } from 'gpx';
import { derived, get, writable } from "svelte/store"; import { derived, get, writable } from 'svelte/store';
import { settings } from "$lib/db"; import { settings } from '$lib/db';
import { _, isLoading, locale } from "svelte-i18n"; import { _, isLoading, locale } from 'svelte-i18n';
import { getElevation } from "$lib/utils"; import { getElevation } from '$lib/utils';
const { routing, routingProfile, privateRoads } = settings; const { routing, routingProfile, privateRoads } = settings;
@ -15,22 +15,31 @@ export const brouterProfiles: { [key: string]: string } = {
foot: 'Hiking-Alpine-SAC6', foot: 'Hiking-Alpine-SAC6',
motorcycle: 'Car-FastEco', motorcycle: 'Car-FastEco',
water: 'river', water: 'river',
railway: 'rail' railway: 'rail',
}; };
export const routingProfileSelectItem = writable({ export const routingProfileSelectItem = writable({
value: '', value: '',
label: '' label: '',
}); });
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => { derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(
if (!i && profile !== '' && (profile !== get(routingProfileSelectItem).value || get(_)(`toolbar.routing.activities.${profile}`) !== get(routingProfileSelectItem).label) && l !== null) { ([profile, l, i]) => {
if (
!i &&
profile !== '' &&
(profile !== get(routingProfileSelectItem).value ||
get(_)(`toolbar.routing.activities.${profile}`) !==
get(routingProfileSelectItem).label) &&
l !== null
) {
routingProfileSelectItem.update((item) => { routingProfileSelectItem.update((item) => {
item.value = profile; item.value = profile;
item.label = get(_)(`toolbar.routing.activities.${profile}`); item.label = get(_)(`toolbar.routing.activities.${profile}`);
return item; return item;
}); });
} }
}); }
);
routingProfileSelectItem.subscribe((item) => { routingProfileSelectItem.subscribe((item) => {
if (item.value !== '' && item.value !== get(routingProfile)) { if (item.value !== '' && item.value !== get(routingProfile)) {
routingProfile.set(item.value); routingProfile.set(item.value);
@ -45,8 +54,12 @@ export function route(points: Coordinates[]): Promise<TrackPoint[]> {
} }
} }
async function getRoute(points: Coordinates[], brouterProfile: string, privateRoads: boolean): Promise<TrackPoint[]> { async function getRoute(
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`; points: Coordinates[],
brouterProfile: string,
privateRoads: boolean
): Promise<TrackPoint[]> {
let url = `https://routing.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
let response = await fetch(url); let response = await fetch(url);
@ -61,25 +74,29 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
let coordinates = geojson.features[0].geometry.coordinates; let coordinates = geojson.features[0].geometry.coordinates;
let messages = geojson.features[0].properties.messages; let messages = geojson.features[0].properties.messages;
const lngIdx = messages[0].indexOf("Longitude"); const lngIdx = messages[0].indexOf('Longitude');
const latIdx = messages[0].indexOf("Latitude"); const latIdx = messages[0].indexOf('Latitude');
const tagIdx = messages[0].indexOf("WayTags"); const tagIdx = messages[0].indexOf('WayTags');
let messageIdx = 1; let messageIdx = 1;
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {}; let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) { for (let i = 0; i < coordinates.length; i++) {
let coord = coordinates[i]; let coord = coordinates[i];
route.push(new TrackPoint({ route.push(
new TrackPoint({
attributes: { attributes: {
lat: coord[1], lat: coord[1],
lon: coord[0] lon: coord[0],
}, },
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0) ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0),
})); })
);
if (messageIdx < messages.length && if (
messageIdx < messages.length &&
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 && coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) { coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000
) {
messageIdx++; messageIdx++;
if (messageIdx == messages.length) tags = {}; if (messageIdx == messages.length) tags = {};
@ -93,10 +110,10 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
} }
function getTags(message: string): { [key: string]: string } { function getTags(message: string): { [key: string]: string } {
const fields = message.split(" "); const fields = message.split(' ');
let tags: { [key: string]: string } = {}; let tags: { [key: string]: string } = {};
for (let i = 0; i < fields.length; i++) { for (let i = 0; i < fields.length; i++) {
let [key, value] = fields[i].split("="); let [key, value] = fields[i].split('=');
key = key.replace(/:/g, '_'); key = key.replace(/:/g, '_');
tags[key] = value; tags[key] = value;
} }
@ -107,26 +124,31 @@ function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
let route: TrackPoint[] = []; let route: TrackPoint[] = [];
let step = 0.05; let step = 0.05;
for (let i = 0; i < points.length - 1; i++) { // Add intermediate points between each pair of points for (let i = 0; i < points.length - 1; i++) {
// Add intermediate points between each pair of points
let dist = distance(points[i], points[i + 1]) / 1000; let dist = distance(points[i], points[i + 1]) / 1000;
for (let d = 0; d < dist; d += step) { for (let d = 0; d < dist; d += step) {
let lat = points[i].lat + d / dist * (points[i + 1].lat - points[i].lat); let lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
let lon = points[i].lon + d / dist * (points[i + 1].lon - points[i].lon); let lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
route.push(new TrackPoint({ route.push(
new TrackPoint({
attributes: { attributes: {
lat: lat, lat: lat,
lon: lon lon: lon,
} },
})); })
);
} }
} }
route.push(new TrackPoint({ route.push(
new TrackPoint({
attributes: { attributes: {
lat: points[points.length - 1].lat, lat: points[points.length - 1].lat,
lon: points[points.length - 1].lon lon: points[points.length - 1].lon,
} },
})); })
);
return getElevation(route).then((elevations) => { return getElevation(route).then((elevations) => {
route.forEach((point, i) => { route.forEach((point, i) => {

View File

@ -1,14 +1,18 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from "gpx"; import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from "svelte/store"; import { get, writable, type Readable } from 'svelte/store';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { route } from "./Routing"; import { route } from './Routing';
import { toast } from "svelte-sonner"; import { toast } from 'svelte-sonner';
import { _ } from "svelte-i18n"; import { _ } from 'svelte-i18n';
import { dbUtils, settings, type GPXFileWithStatistics } from "$lib/db"; import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
import { getOrderedSelection, selection } from "$lib/components/file-list/Selection"; import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList"; import {
import { currentTool, streetViewEnabled, Tool } from "$lib/stores"; ListFileItem,
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils"; ListTrackItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import { currentTool, streetViewEnabled, Tool } from '$lib/stores';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
const { streetViewSource } = settings; const { streetViewSource } = settings;
export const canChangeStart = writable(false); export const canChangeStart = writable(false);
@ -28,15 +32,22 @@ export class RoutingControls {
popupElement: HTMLElement; popupElement: HTMLElement;
temporaryAnchor: AnchorWithMarker; temporaryAnchor: AnchorWithMarker;
lastDragEvent = 0; lastDragEvent = 0;
fileUnsubscribe: () => void = () => { }; fileUnsubscribe: () => void = () => {};
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
toggleAnchorsForZoomLevelAndBoundsBinded: () => void = this.toggleAnchorsForZoomLevelAndBounds.bind(this); toggleAnchorsForZoomLevelAndBoundsBinded: () => void =
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this); showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this); updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this); appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) { constructor(
map: mapboxgl.Map,
fileId: string,
file: Readable<GPXFileWithStatistics | undefined>,
popup: mapboxgl.Popup,
popupElement: HTMLElement
) {
this.map = map; this.map = map;
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
@ -46,8 +57,8 @@ export class RoutingControls {
let point = new TrackPoint({ let point = new TrackPoint({
attributes: { attributes: {
lat: 0, lat: 0,
lon: 0 lon: 0,
} },
}); });
this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0); this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0);
this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers
@ -65,7 +76,9 @@ export class RoutingControls {
return; return;
} }
let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, ['waypoints']); let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, [
'waypoints',
]);
if (selected) { if (selected) {
if (this.active) { if (this.active) {
this.updateControls(); this.updateControls();
@ -88,7 +101,8 @@ export class RoutingControls {
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this)); this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
} }
updateControls() { // Update the markers when the file changes updateControls() {
// Update the markers when the file changes
let file = get(this.file)?.file; let file = get(this.file)?.file;
if (!file) { if (!file) {
return; return;
@ -96,8 +110,13 @@ export class RoutingControls {
let anchorIndex = 0; let anchorIndex = 0;
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
for (let point of segment.trkpt) { // Update the existing anchors (could be improved by matching the existing anchors with the new ones?) get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
for (let point of segment.trkpt) {
// Update the existing anchors (could be improved by matching the existing anchors with the new ones?)
if (point._data.anchor) { if (point._data.anchor) {
if (anchorIndex < this.anchors.length) { if (anchorIndex < this.anchors.length) {
this.anchors[anchorIndex].point = point; this.anchors[anchorIndex].point = point;
@ -106,7 +125,9 @@ export class RoutingControls {
this.anchors[anchorIndex].segmentIndex = segmentIndex; this.anchors[anchorIndex].segmentIndex = segmentIndex;
this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates()); this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates());
} else { } else {
this.anchors.push(this.createAnchor(point, segment, trackIndex, segmentIndex)); this.anchors.push(
this.createAnchor(point, segment, trackIndex, segmentIndex)
);
} }
anchorIndex++; anchorIndex++;
} }
@ -114,7 +135,8 @@ export class RoutingControls {
} }
}); });
while (anchorIndex < this.anchors.length) { // Remove the extra anchors while (anchorIndex < this.anchors.length) {
// Remove the extra anchors
this.anchors.pop()?.marker.remove(); this.anchors.pop()?.marker.remove();
} }
@ -141,14 +163,19 @@ export class RoutingControls {
this.map = map; this.map = map;
} }
createAnchor(point: TrackPoint, segment: TrackSegment, trackIndex: number, segmentIndex: number): AnchorWithMarker { createAnchor(
point: TrackPoint,
segment: TrackSegment,
trackIndex: number,
segmentIndex: number
): AnchorWithMarker {
let element = document.createElement('div'); let element = document.createElement('div');
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`; element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
let marker = new mapboxgl.Marker({ let marker = new mapboxgl.Marker({
draggable: true, draggable: true,
className: 'z-10', className: 'z-10',
element element,
}).setLngLat(point.getCoordinates()); }).setLngLat(point.getCoordinates());
let anchor = { let anchor = {
@ -157,7 +184,7 @@ export class RoutingControls {
trackIndex, trackIndex,
segmentIndex, segmentIndex,
marker, marker,
inZoom: false inZoom: false,
}; };
marker.on('dragstart', (e) => { marker.on('dragstart', (e) => {
@ -185,7 +212,8 @@ export class RoutingControls {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (Date.now() - this.lastDragEvent < 100) { // Prevent click event during drag if (Date.now() - this.lastDragEvent < 100) {
// Prevent click event during drag
return; return;
} }
@ -204,7 +232,12 @@ export class RoutingControls {
return false; return false;
} }
let segment = anchor.segment; let segment = anchor.segment;
if (distance(segment.trkpt[0].getCoordinates(), segment.trkpt[segment.trkpt.length - 1].getCoordinates()) > 1000) { if (
distance(
segment.trkpt[0].getCoordinates(),
segment.trkpt[segment.trkpt.length - 1].getCoordinates()
) > 1000
) {
return false; return false;
} }
return true; return true;
@ -224,7 +257,8 @@ export class RoutingControls {
}; };
} }
toggleAnchorsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds toggleAnchorsForZoomLevelAndBounds() {
// Show markers only if they are in the current zoom level and bounds
this.shownAnchors.splice(0, this.shownAnchors.length); this.shownAnchors.splice(0, this.shownAnchors.length);
let center = this.map.getCenter(); let center = this.map.getCenter();
@ -245,7 +279,8 @@ export class RoutingControls {
} }
showTemporaryAnchor(e: any) { showTemporaryAnchor(e: any) {
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not not change the source point if it is already being dragged if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not not change the source point if it is already being dragged
return; return;
} }
@ -253,7 +288,15 @@ export class RoutingControls {
return; return;
} }
if (!get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, e.features[0].properties.trackIndex, e.features[0].properties.segmentIndex))) { if (
!get(selection).hasAnyParent(
new ListTrackSegmentItem(
this.fileId,
e.features[0].properties.trackIndex,
e.features[0].properties.segmentIndex
)
)
) {
return; return;
} }
@ -263,7 +306,7 @@ export class RoutingControls {
this.temporaryAnchor.point.setCoordinates({ this.temporaryAnchor.point.setCoordinates({
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng lon: e.lngLat.lng,
}); });
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map); this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
@ -271,12 +314,17 @@ export class RoutingControls {
} }
updateTemporaryAnchor(e: any) { updateTemporaryAnchor(e: any) {
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not hide if it is being dragged, and stop listening for mousemove if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not hide if it is being dragged, and stop listening for mousemove
this.map.off('mousemove', this.updateTemporaryAnchorBinded); this.map.off('mousemove', this.updateTemporaryAnchorBinded);
return; return;
} }
if (e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 || this.temporaryAnchorCloseToOtherAnchor(e)) { // Hide if too far from the layer if (
e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
this.temporaryAnchorCloseToOtherAnchor(e)
) {
// Hide if too far from the layer
this.temporaryAnchor.marker.remove(); this.temporaryAnchor.marker.remove();
this.map.off('mousemove', this.updateTemporaryAnchorBinded); this.map.off('mousemove', this.updateTemporaryAnchorBinded);
return; return;
@ -294,14 +342,16 @@ export class RoutingControls {
return false; return false;
} }
async moveAnchor(anchorWithMarker: AnchorWithMarker) { // Move the anchor and update the route from and to the neighbouring anchors async moveAnchor(anchorWithMarker: AnchorWithMarker) {
// Move the anchor and update the route from and to the neighbouring anchors
let coordinates = { let coordinates = {
lat: anchorWithMarker.marker.getLngLat().lat, lat: anchorWithMarker.marker.getLngLat().lat,
lon: anchorWithMarker.marker.getLngLat().lng lon: anchorWithMarker.marker.getLngLat().lng,
}; };
let anchor = anchorWithMarker as Anchor; let anchor = anchorWithMarker as Anchor;
if (anchorWithMarker === this.temporaryAnchor) { // Temporary anchor, need to find the closest point of the segment and create an anchor for it if (anchorWithMarker === this.temporaryAnchor) {
// Temporary anchor, need to find the closest point of the segment and create an anchor for it
this.temporaryAnchor.marker.remove(); this.temporaryAnchor.marker.remove();
anchor = this.getPermanentAnchor(); anchor = this.getPermanentAnchor();
} }
@ -326,7 +376,8 @@ export class RoutingControls {
let success = await this.routeBetweenAnchors(anchors, targetCoordinates); let success = await this.routeBetweenAnchors(anchors, targetCoordinates);
if (!success) { // Route failed, revert the anchor to the previous position if (!success) {
// Route failed, revert the anchor to the previous position
anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates()); anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates());
} }
} }
@ -338,16 +389,24 @@ export class RoutingControls {
let minDetails: any = { distance: Number.MAX_VALUE }; let minDetails: any = { distance: Number.MAX_VALUE };
let minAnchor = this.temporaryAnchor as Anchor; let minAnchor = this.temporaryAnchor as Anchor;
file?.forEachSegment((segment, trackIndex, segmentIndex) => { file?.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
let details: any = {}; let details: any = {};
let closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details); let closest = getClosestLinePoint(
segment.trkpt,
this.temporaryAnchor.point,
details
);
if (details.distance < minDetails.distance) { if (details.distance < minDetails.distance) {
minDetails = details; minDetails = details;
minAnchor = { minAnchor = {
point: closest, point: closest,
segment, segment,
trackIndex, trackIndex,
segmentIndex segmentIndex,
}; };
} }
} }
@ -374,41 +433,67 @@ export class RoutingControls {
point: this.temporaryAnchor.point, point: this.temporaryAnchor.point,
trackIndex: -1, trackIndex: -1,
segmentIndex: -1, segmentIndex: -1,
trkptIndex: -1 trkptIndex: -1,
}; };
file?.forEachSegment((segment, trackIndex, segmentIndex) => { file?.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
let details: any = {}; let details: any = {};
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details); getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
if (details.distance < minDetails.distance) { if (details.distance < minDetails.distance) {
minDetails = details; minDetails = details;
let before = details.before ? details.index : details.index - 1; let before = details.before ? details.index : details.index - 1;
let projectedPt = projectedPoint(segment.trkpt[before], segment.trkpt[before + 1], this.temporaryAnchor.point); let projectedPt = projectedPoint(
let ratio = distance(segment.trkpt[before], projectedPt) / distance(segment.trkpt[before], segment.trkpt[before + 1]); segment.trkpt[before],
segment.trkpt[before + 1],
this.temporaryAnchor.point
);
let ratio =
distance(segment.trkpt[before], projectedPt) /
distance(segment.trkpt[before], segment.trkpt[before + 1]);
let point = segment.trkpt[before].clone(); let point = segment.trkpt[before].clone();
point.setCoordinates(projectedPt); point.setCoordinates(projectedPt);
point.ele = (1 - ratio) * (segment.trkpt[before].ele ?? 0) + ratio * (segment.trkpt[before + 1].ele ?? 0); point.ele =
point.time = (segment.trkpt[before].time && segment.trkpt[before + 1].time) ? new Date((1 - ratio) * segment.trkpt[before].time.getTime() + ratio * segment.trkpt[before + 1].time.getTime()) : undefined; (1 - ratio) * (segment.trkpt[before].ele ?? 0) +
ratio * (segment.trkpt[before + 1].ele ?? 0);
point.time =
segment.trkpt[before].time && segment.trkpt[before + 1].time
? new Date(
(1 - ratio) * segment.trkpt[before].time.getTime() +
ratio * segment.trkpt[before + 1].time.getTime()
)
: undefined;
point._data = { point._data = {
anchor: true, anchor: true,
zoom: 0 zoom: 0,
}; };
minInfo = { minInfo = {
point, point,
trackIndex, trackIndex,
segmentIndex, segmentIndex,
trkptIndex: before + 1 trkptIndex: before + 1,
}; };
} }
} }
}); });
if (minInfo.trackIndex !== -1) { if (minInfo.trackIndex !== -1) {
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(minInfo.trackIndex, minInfo.segmentIndex, minInfo.trkptIndex, minInfo.trkptIndex - 1, [minInfo.point])); dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
minInfo.trackIndex,
minInfo.segmentIndex,
minInfo.trkptIndex,
minInfo.trkptIndex - 1,
[minInfo.point]
)
);
} }
} }
@ -416,22 +501,46 @@ export class RoutingControls {
return () => this.deleteAnchor(anchor); return () => this.deleteAnchor(anchor);
} }
async deleteAnchor(anchor: Anchor) { // Remove the anchor and route between the neighbouring anchors if they exist async deleteAnchor(anchor: Anchor) {
// Remove the anchor and route between the neighbouring anchors if they exist
this.popup.remove(); this.popup.remove();
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor); let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it if (previousAnchor === null && nextAnchor === null) {
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])); // Only one point, remove it
} else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor dbUtils.applyToFile(this.fileId, (file) =>
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, nextAnchor.point._data.index - 1, [])); file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
} else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor );
} else if (previousAnchor === null) {
// First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
0,
nextAnchor.point._data.index - 1,
[]
)
);
} else if (nextAnchor === null) {
// Last point, remove trackpoints from previousAnchor
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => {
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex); let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []); file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
previousAnchor.point._data.index + 1,
segment.trkpt.length - 1,
[]
);
}); });
} else { // Route between previousAnchor and nextAnchor } else {
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]); // Route between previousAnchor and nextAnchor
this.routeBetweenAnchors(
[previousAnchor, nextAnchor],
[previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]
);
} }
} }
@ -447,27 +556,43 @@ export class RoutingControls {
return; return;
} }
let speed = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)).global.speed.moving; let speed = fileWithStats.statistics.getStatisticsFor(
new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)
).global.speed.moving;
let segment = anchor.segment; let segment = anchor.segment;
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => {
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined); file.replaceTrackPoints(
file.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]); anchor.trackIndex,
anchor.segmentIndex,
segment.trkpt.length,
segment.trkpt.length - 1,
segment.trkpt.slice(0, anchor.point._data.index),
speed > 0 ? speed : undefined
);
file.crop(
anchor.point._data.index,
anchor.point._data.index + segment.trkpt.length - 1,
[anchor.trackIndex],
[anchor.segmentIndex]
);
}); });
} }
async appendAnchor(e: mapboxgl.MapMouseEvent) { // Add a new anchor to the end of the last segment async appendAnchor(e: mapboxgl.MapMouseEvent) {
// Add a new anchor to the end of the last segment
if (get(streetViewEnabled) && get(streetViewSource) === 'google') { if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
return; return;
} }
this.appendAnchorWithCoordinates({ this.appendAnchorWithCoordinates({
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng lon: e.lngLat.lng,
}); });
} }
async appendAnchorWithCoordinates(coordinates: Coordinates) { // Add a new anchor to the end of the last segment async appendAnchorWithCoordinates(coordinates: Coordinates) {
// Add a new anchor to the end of the last segment
let selected = getOrderedSelection(); let selected = getOrderedSelection();
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) { if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
return; return;
@ -477,7 +602,7 @@ export class RoutingControls {
let lastAnchor = this.anchors[this.anchors.length - 1]; let lastAnchor = this.anchors[this.anchors.length - 1];
let newPoint = new TrackPoint({ let newPoint = new TrackPoint({
attributes: coordinates attributes: coordinates,
}); });
newPoint._data.anchor = true; newPoint._data.anchor = true;
newPoint._data.zoom = 0; newPoint._data.zoom = 0;
@ -488,7 +613,10 @@ export class RoutingControls {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
trackIndex = item.getTrackIndex(); trackIndex = item.getTrackIndex();
} }
let segmentIndex = (file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0) ? file.trk[trackIndex].trkseg.length - 1 : 0; let segmentIndex =
file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0
? file.trk[trackIndex].trkseg.length - 1
: 0;
if (item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackSegmentItem) {
segmentIndex = item.getSegmentIndex(); segmentIndex = item.getSegmentIndex();
} }
@ -512,10 +640,13 @@ export class RoutingControls {
point: newPoint, point: newPoint,
segment: lastAnchor.segment, segment: lastAnchor.segment,
trackIndex: lastAnchor.trackIndex, trackIndex: lastAnchor.trackIndex,
segmentIndex: lastAnchor.segmentIndex segmentIndex: lastAnchor.segmentIndex,
}; };
await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]); await this.routeBetweenAnchors(
[lastAnchor, newAnchor],
[lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]
);
} }
getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] { getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] {
@ -525,11 +656,17 @@ export class RoutingControls {
for (let i = 0; i < this.anchors.length; i++) { for (let i = 0; i < this.anchors.length; i++) {
if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) { if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) {
if (this.anchors[i].point._data.index < anchor.point._data.index) { if (this.anchors[i].point._data.index < anchor.point._data.index) {
if (!previousAnchor || this.anchors[i].point._data.index > previousAnchor.point._data.index) { if (
!previousAnchor ||
this.anchors[i].point._data.index > previousAnchor.point._data.index
) {
previousAnchor = this.anchors[i]; previousAnchor = this.anchors[i];
} }
} else if (this.anchors[i].point._data.index > anchor.point._data.index) { } else if (this.anchors[i].point._data.index > anchor.point._data.index) {
if (!nextAnchor || this.anchors[i].point._data.index < nextAnchor.point._data.index) { if (
!nextAnchor ||
this.anchors[i].point._data.index < nextAnchor.point._data.index
) {
nextAnchor = this.anchors[i]; nextAnchor = this.anchors[i];
} }
} }
@ -539,7 +676,10 @@ export class RoutingControls {
return [previousAnchor, nextAnchor]; return [previousAnchor, nextAnchor];
} }
async routeBetweenAnchors(anchors: Anchor[], targetCoordinates: Coordinates[]): Promise<boolean> { async routeBetweenAnchors(
anchors: Anchor[],
targetCoordinates: Coordinates[]
): Promise<boolean> {
let segment = anchors[0].segment; let segment = anchors[0].segment;
let fileWithStats = get(this.file); let fileWithStats = get(this.file);
@ -547,10 +687,15 @@ export class RoutingControls {
return false; return false;
} }
if (anchors.length === 1) { // Only one anchor, update the point in the segment if (anchors.length === 1) {
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [new TrackPoint({ // Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
new TrackPoint({
attributes: targetCoordinates[0], attributes: targetCoordinates[0],
})])); }),
])
);
return true; return true;
} }
@ -559,23 +704,28 @@ export class RoutingControls {
response = await route(targetCoordinates); response = await route(targetCoordinates);
} catch (e: any) { } catch (e: any) {
if (e.message.includes('from-position not mapped in existing datafile')) { if (e.message.includes('from-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.from")); toast.error(get(_)('toolbar.routing.error.from'));
} else if (e.message.includes('via1-position not mapped in existing datafile')) { } else if (e.message.includes('via1-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.via")); toast.error(get(_)('toolbar.routing.error.via'));
} else if (e.message.includes('to-position not mapped in existing datafile')) { } else if (e.message.includes('to-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.to")); toast.error(get(_)('toolbar.routing.error.to'));
} else if (e.message.includes('Time-out')) { } else if (e.message.includes('Time-out')) {
toast.error(get(_)("toolbar.routing.error.timeout")); toast.error(get(_)('toolbar.routing.error.timeout'));
} else { } else {
toast.error(e.message); toast.error(e.message);
} }
return false; return false;
} }
if (anchors[0].point._data.index === 0) { // First anchor is the first point of the segment if (anchors[0].point._data.index === 0) {
// First anchor is the first point of the segment
anchors[0].point = response[0]; // replace the first anchor anchors[0].point = response[0]; // replace the first anchor
anchors[0].point._data.index = 0; anchors[0].point._data.index = 0;
} else if (anchors[0].point._data.index === segment.trkpt.length - 1 && distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1) { // First anchor is the last point of the segment, and the new point is close enough } else if (
anchors[0].point._data.index === segment.trkpt.length - 1 &&
distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1
) {
// First anchor is the last point of the segment, and the new point is close enough
anchors[0].point = response[0]; // replace the first anchor anchors[0].point = response[0]; // replace the first anchor
anchors[0].point._data.index = segment.trkpt.length - 1; anchors[0].point._data.index = segment.trkpt.length - 1;
} else { } else {
@ -583,7 +733,8 @@ export class RoutingControls {
response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it
} }
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) { // Last anchor is the last point of the segment if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) {
// Last anchor is the last point of the segment
anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor
anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1; anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1;
} else { } else {
@ -594,7 +745,7 @@ export class RoutingControls {
for (let i = 1; i < anchors.length - 1; i++) { for (let i = 1; i < anchors.length - 1; i++) {
// Find the closest point to the intermediate anchor // Find the closest point to the intermediate anchor
// and transfer the marker to that point // and transfer the marker to that point
anchors[i].point = getClosestLinePoint(response.slice(1, - 1), targetCoordinates[i]); anchors[i].point = getClosestLinePoint(response.slice(1, -1), targetCoordinates[i]);
} }
anchors.forEach((anchor) => { anchors.forEach((anchor) => {
@ -602,36 +753,64 @@ export class RoutingControls {
anchor.point._data.zoom = 0; // Make these anchors permanent anchor.point._data.zoom = 0; // Make these anchors permanent
}); });
let stats = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex)); let stats = fileWithStats.statistics.getStatisticsFor(
new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex)
);
let speed: number | undefined = undefined; let speed: number | undefined = undefined;
let startTime = anchors[0].point.time; let startTime = anchors[0].point.time;
if (stats.global.speed.moving > 0) { if (stats.global.speed.moving > 0) {
let replacingDistance = 0; let replacingDistance = 0;
for (let i = 1; i < response.length; i++) { for (let i = 1; i < response.length; i++) {
replacingDistance += distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000; replacingDistance +=
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
} }
let replacedDistance = stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - stats.local.distance.moving[anchors[0].point._data.index]; let replacedDistance =
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
stats.local.distance.moving[anchors[0].point._data.index];
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance; let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
let newTime = newDistance / stats.global.speed.moving * 3600; let newTime = (newDistance / stats.global.speed.moving) * 3600;
let remainingTime = stats.global.time.moving - (stats.local.time.moving[anchors[anchors.length - 1].point._data.index] - stats.local.time.moving[anchors[0].point._data.index]); let remainingTime =
stats.global.time.moving -
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
stats.local.time.moving[anchors[0].point._data.index]);
let replacingTime = newTime - remainingTime; let replacingTime = newTime - remainingTime;
if (replacingTime <= 0) { // Fallback to simple time difference if (replacingTime <= 0) {
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index]; // Fallback to simple time difference
replacingTime =
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
stats.local.time.total[anchors[0].point._data.index];
} }
speed = replacingDistance / replacingTime * 3600; speed = (replacingDistance / replacingTime) * 3600;
if (startTime === undefined) { // Replacing the first point if (startTime === undefined) {
// Replacing the first point
let endIndex = anchors[anchors.length - 1].point._data.index; let endIndex = anchors[anchors.length - 1].point._data.index;
startTime = new Date((segment.trkpt[endIndex].time?.getTime() ?? 0) - (replacingTime + stats.local.time.total[endIndex] - stats.local.time.moving[endIndex]) * 1000); startTime = new Date(
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
(replacingTime +
stats.local.time.total[endIndex] -
stats.local.time.moving[endIndex]) *
1000
);
} }
} }
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, anchors[0].point._data.index, anchors[anchors.length - 1].point._data.index, response, speed, startTime)); dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchors[0].trackIndex,
anchors[0].segmentIndex,
anchors[0].point._data.index,
anchors[anchors.length - 1].point._data.index,
response,
speed,
startTime
)
);
return true; return true;
} }

View File

@ -1,4 +1,4 @@
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx"; import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
@ -17,7 +17,8 @@ export function updateAnchorPoints(file: GPXFile) {
let segments = file.getSegments(); let segments = file.getSegments();
for (let segment of segments) { for (let segment of segments) {
if (!segment._data.anchors) { // New segment, compute anchor points for it if (!segment._data.anchors) {
// New segment, compute anchor points for it
computeAnchorPoints(segment); computeAnchorPoints(segment);
continue; continue;
} }
@ -42,4 +43,3 @@ function computeAnchorPoints(segment: TrackSegment) {
}); });
segment._data.anchors = true; segment._data.anchors = true;
} }

View File

@ -2,7 +2,7 @@
export enum SplitType { export enum SplitType {
FILES = 'files', FILES = 'files',
TRACKS = 'tracks', TRACKS = 'tracks',
SEGMENTS = 'segments' SEGMENTS = 'segments',
} }
</script> </script>
@ -50,7 +50,7 @@
$slicedGPXStatistics = [ $slicedGPXStatistics = [
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]), get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
sliderValues[0], sliderValues[0],
sliderValues[1] sliderValues[1],
]; ];
} else { } else {
$slicedGPXStatistics = undefined; $slicedGPXStatistics = undefined;
@ -93,7 +93,7 @@
const splitTypes = [ const splitTypes = [
{ value: SplitType.FILES, label: $_('gpx.files') }, { value: SplitType.FILES, label: $_('gpx.files') },
{ value: SplitType.TRACKS, label: $_('gpx.tracks') }, { value: SplitType.TRACKS, label: $_('gpx.tracks') },
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') } { value: SplitType.SEGMENTS, label: $_('gpx.segments') },
]; ];
let splitType = splitTypes.find((type) => type.value === $splitAs) ?? splitTypes[0]; let splitType = splitTypes.find((type) => type.value === $splitAs) ?? splitTypes[0];
@ -111,7 +111,12 @@
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<div class="p-2"> <div class="p-2">
<Slider bind:value={sliderValues} max={maxSliderValue} step={1} disabled={!validSelection} /> <Slider
bind:value={sliderValues}
max={maxSliderValue}
step={1}
disabled={!validSelection}
/>
</div> </div>
<Button <Button
variant="outline" variant="outline"

View File

@ -1,12 +1,15 @@
import { TrackPoint, TrackSegment } from "gpx"; import { TrackPoint, TrackSegment } from 'gpx';
import { get } from "svelte/store"; import { get } from 'svelte/store';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from "$lib/db"; import { dbUtils, getFile } from '$lib/db';
import { applyToOrderedSelectedItemsFromFile, selection } from "$lib/components/file-list/Selection"; import {
import { ListTrackSegmentItem } from "$lib/components/file-list/FileList"; applyToOrderedSelectedItemsFromFile,
import { currentTool, gpxStatistics, Tool } from "$lib/stores"; selection,
import { _ } from "svelte-i18n"; } from '$lib/components/file-list/Selection';
import { Scissors } from "lucide-static"; import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import { currentTool, gpxStatistics, Tool } from '$lib/stores';
import { _ } from 'svelte-i18n';
import { Scissors } from 'lucide-static';
export class SplitControls { export class SplitControls {
active: boolean = false; active: boolean = false;
@ -15,7 +18,8 @@ export class SplitControls {
shownControls: ControlWithMarker[] = []; shownControls: ControlWithMarker[] = [];
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
toggleControlsForZoomLevelAndBoundsBinded: () => void = this.toggleControlsForZoomLevelAndBounds.bind(this); toggleControlsForZoomLevelAndBoundsBinded: () => void =
this.toggleControlsForZoomLevelAndBounds.bind(this);
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
@ -48,15 +52,21 @@ export class SplitControls {
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded); this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
updateControls() { // Update the markers when the files change updateControls() {
// Update the markers when the files change
let controlIndex = 0; let controlIndex = 0;
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = getFile(fileId); let file = getFile(fileId);
if (file) { if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(fileId, trackIndex, segmentIndex))) { if (
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?) get(selection).hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
)
) {
for (let point of segment.trkpt.slice(1, -1)) {
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
if (point._data.anchor) { if (point._data.anchor) {
if (controlIndex < this.controls.length) { if (controlIndex < this.controls.length) {
this.controls[controlIndex].fileId = fileId; this.controls[controlIndex].fileId = fileId;
@ -64,20 +74,30 @@ export class SplitControls {
this.controls[controlIndex].segment = segment; this.controls[controlIndex].segment = segment;
this.controls[controlIndex].trackIndex = trackIndex; this.controls[controlIndex].trackIndex = trackIndex;
this.controls[controlIndex].segmentIndex = segmentIndex; this.controls[controlIndex].segmentIndex = segmentIndex;
this.controls[controlIndex].marker.setLngLat(point.getCoordinates()); this.controls[controlIndex].marker.setLngLat(
point.getCoordinates()
);
} else { } else {
this.controls.push(this.createControl(point, segment, fileId, trackIndex, segmentIndex)); this.controls.push(
this.createControl(
point,
segment,
fileId,
trackIndex,
segmentIndex
)
);
} }
controlIndex++; controlIndex++;
} }
} }
} }
}); });
} }
}, false); }, false);
while (controlIndex < this.controls.length) { // Remove the extra controls while (controlIndex < this.controls.length) {
// Remove the extra controls
this.controls.pop()?.marker.remove(); this.controls.pop()?.marker.remove();
} }
@ -94,7 +114,8 @@ export class SplitControls {
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded); this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
toggleControlsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds toggleControlsForZoomLevelAndBounds() {
// Show markers only if they are in the current zoom level and bounds
this.shownControls.splice(0, this.shownControls.length); this.shownControls.splice(0, this.shownControls.length);
let southWest = this.map.unproject([0, this.map.getCanvas().height]); let southWest = this.map.unproject([0, this.map.getCanvas().height]);
@ -113,15 +134,23 @@ export class SplitControls {
}); });
} }
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker { createControl(
point: TrackPoint,
segment: TrackSegment,
fileId: string,
trackIndex: number,
segmentIndex: number
): ControlWithMarker {
let element = document.createElement('div'); let element = document.createElement('div');
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`; element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "").replace('stroke="currentColor"', 'stroke="black"'); element.innerHTML = Scissors.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', 'stroke="black"');
let marker = new mapboxgl.Marker({ let marker = new mapboxgl.Marker({
draggable: true, draggable: true,
className: 'z-10', className: 'z-10',
element element,
}).setLngLat(point.getCoordinates()); }).setLngLat(point.getCoordinates());
let control = { let control = {
@ -131,12 +160,18 @@ export class SplitControls {
trackIndex, trackIndex,
segmentIndex, segmentIndex,
marker, marker,
inZoom: false inZoom: false,
}; };
marker.getElement().addEventListener('click', (e) => { marker.getElement().addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
dbUtils.split(control.fileId, control.trackIndex, control.segmentIndex, control.point.getCoordinates(), control.point._data.index); dbUtils.split(
control.fileId,
control.trackIndex,
control.segmentIndex,
control.point.getCoordinates(),
control.point._data.index
);
}); });
return control; return control;

View File

@ -1,11 +1,58 @@
import Dexie, { liveQuery } from 'dexie'; import Dexie, { liveQuery } from 'dexie';
import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance, type LineStyleExtension, type WaypointType } from 'gpx'; import {
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, freeze, produceWithPatches } from 'immer'; GPXFile,
GPXStatistics,
Track,
TrackSegment,
Waypoint,
TrackPoint,
type Coordinates,
distance,
type LineStyleExtension,
type WaypointType,
} from 'gpx';
import {
enableMapSet,
enablePatches,
applyPatches,
type Patch,
type WritableDraft,
freeze,
produceWithPatches,
} from 'immer';
import { writable, get, derived, type Readable, type Writable } from 'svelte/store'; import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
import { gpxStatistics, initTargetMapBounds, map, splitAs, updateAllHidden, updateTargetMapBounds } from './stores'; import {
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays, type CustomLayer, defaultOpacities, defaultOverpassQueries, defaultOverpassTree } from './assets/layers'; gpxStatistics,
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection'; initTargetMapBounds,
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList'; map,
splitAs,
updateAllHidden,
updateTargetMapBounds,
} from './stores';
import {
defaultBasemap,
defaultBasemapTree,
defaultOverlayTree,
defaultOverlays,
type CustomLayer,
defaultOpacities,
defaultOverpassQueries,
defaultOverpassTree,
} from './assets/layers';
import {
applyToOrderedItemsFromFile,
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListItem,
ListTrackItem,
ListLevel,
ListTrackSegmentItem,
ListWaypointItem,
ListRootItem,
} from '$lib/components/file-list/FileList';
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify'; import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import { getClosestLinePoint, getElevation } from '$lib/utils'; import { getClosestLinePoint, getElevation } from '$lib/utils';
@ -15,17 +62,22 @@ enableMapSet();
enablePatches(); enablePatches();
class Database extends Dexie { class Database extends Dexie {
fileids!: Dexie.Table<string, string>; fileids!: Dexie.Table<string, string>;
files!: Dexie.Table<GPXFile, string>; files!: Dexie.Table<GPXFile, string>;
patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[], index: number }, number>; patches!: Dexie.Table<{ patch: Patch[]; inversePatch: Patch[]; index: number }, number>;
settings!: Dexie.Table<any, string>; settings!: Dexie.Table<any, string>;
overpasstiles!: Dexie.Table<{ query: string, x: number, y: number, time: number }, [string, number, number]>; overpasstiles!: Dexie.Table<
overpassdata!: Dexie.Table<{ query: string, id: number, poi: GeoJSON.Feature }, [string, number]>; { query: string; x: number; y: number; time: number },
[string, number, number]
>;
overpassdata!: Dexie.Table<
{ query: string; id: number; poi: GeoJSON.Feature },
[string, number]
>;
constructor() { constructor() {
super("Database", { super('Database', {
cache: 'immutable' cache: 'immutable',
}); });
this.version(1).stores({ this.version(1).stores({
fileids: ',&fileid', fileids: ',&fileid',
@ -41,10 +93,15 @@ class Database extends Dexie {
export const db = new Database(); export const db = new Database();
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB // Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
export function bidirectionalDexieStore<K, V>(table: Dexie.Table<V, K>, key: K, initial: V, initialize: boolean = true): Writable<V | undefined> { export function bidirectionalDexieStore<K, V>(
table: Dexie.Table<V, K>,
key: K,
initial: V,
initialize: boolean = true
): Writable<V | undefined> {
let first = true; let first = true;
let store = writable<V | undefined>(initialize ? initial : undefined); let store = writable<V | undefined>(initialize ? initial : undefined);
liveQuery(() => table.get(key)).subscribe(value => { liveQuery(() => table.get(key)).subscribe((value) => {
if (value === undefined) { if (value === undefined) {
if (first) { if (first) {
if (!initialize) { if (!initialize) {
@ -70,11 +127,15 @@ export function bidirectionalDexieStore<K, V>(table: Dexie.Table<V, K>, key: K,
if (typeof newValue === 'object' || newValue !== get(store)) { if (typeof newValue === 'object' || newValue !== get(store)) {
table.put(newValue, key); table.put(newValue, key);
} }
} },
}; };
} }
export function dexieSettingStore<T>(key: string, initial: T, initialize: boolean = true): Writable<T> { export function dexieSettingStore<T>(
key: string,
initial: T,
initialize: boolean = true
): Writable<T> {
return bidirectionalDexieStore(db.settings, key, initial, initialize); return bidirectionalDexieStore(db.settings, key, initial, initialize);
} }
@ -96,7 +157,11 @@ export const settings = {
currentOverlays: dexieSettingStore('currentOverlays', defaultOverlays, false), currentOverlays: dexieSettingStore('currentOverlays', defaultOverlays, false),
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays), previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree), selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
currentOverpassQueries: dexieSettingStore('currentOverpassQueries', defaultOverpassQueries, false), currentOverpassQueries: dexieSettingStore(
'currentOverpassQueries',
defaultOverpassQueries,
false
),
selectedOverpassTree: dexieSettingStore('selectedOverpassTree', defaultOverpassTree), selectedOverpassTree: dexieSettingStore('selectedOverpassTree', defaultOverpassTree),
opacities: dexieSettingStore('opacities', defaultOpacities), opacities: dexieSettingStore('opacities', defaultOpacities),
customLayers: dexieSettingStore<Record<string, CustomLayer>>('customLayers', {}), customLayers: dexieSettingStore<Record<string, CustomLayer>>('customLayers', {}),
@ -107,7 +172,7 @@ export const settings = {
streetViewSource: dexieSettingStore('streetViewSource', 'mapillary'), streetViewSource: dexieSettingStore('streetViewSource', 'mapillary'),
fileOrder: dexieSettingStore<string[]>('fileOrder', []), fileOrder: dexieSettingStore<string[]>('fileOrder', []),
defaultOpacity: dexieSettingStore('defaultOpacity', 0.7), defaultOpacity: dexieSettingStore('defaultOpacity', 0.7),
defaultWidth: dexieSettingStore('defaultWidth', (browser && window.innerWidth < 600) ? 8 : 5), defaultWidth: dexieSettingStore('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: dexieSettingStore('bottomPanelSize', 170), bottomPanelSize: dexieSettingStore('bottomPanelSize', 170),
rightPanelSize: dexieSettingStore('rightPanelSize', 240), rightPanelSize: dexieSettingStore('rightPanelSize', 240),
}; };
@ -115,7 +180,7 @@ export const settings = {
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber // Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> { function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
let store = writable<T>(initial); let store = writable<T>(initial);
liveQuery(querier).subscribe(value => { liveQuery(querier).subscribe((value) => {
if (value !== undefined) { if (value !== undefined) {
store.set(value); store.set(value);
} }
@ -149,7 +214,7 @@ export class GPXStatisticsTree {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
let id = item.getIdAtLevel(this.level); let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') { if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach(key => { Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) { if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]); statistics.mergeWith(this.statistics[key]);
} else { } else {
@ -166,26 +231,30 @@ export class GPXStatisticsTree {
} }
return statistics; return statistics;
} }
}; }
export type GPXFileWithStatistics = { file: GPXFile, statistics: GPXStatisticsTree }; export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object // Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } { function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } {
let store = writable<GPXFileWithStatistics>(undefined); let store = writable<GPXFileWithStatistics>(undefined);
let query = liveQuery(() => db.files.get(id)).subscribe(value => { let query = liveQuery(() => db.files.get(id)).subscribe((value) => {
if (value !== undefined) { if (value !== undefined) {
let gpx = new GPXFile(value); let gpx = new GPXFile(value);
updateAnchorPoints(gpx); updateAnchorPoints(gpx);
let statistics = new GPXStatisticsTree(gpx); let statistics = new GPXStatisticsTree(gpx);
if (!fileState.has(id)) { // Update the map bounds for new files if (!fileState.has(id)) {
updateTargetMapBounds(id, statistics.getStatisticsFor(new ListFileItem(id)).global.bounds); // Update the map bounds for new files
updateTargetMapBounds(
id,
statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
);
} }
fileState.set(id, gpx); fileState.set(id, gpx);
store.set({ store.set({
file: gpx, file: gpx,
statistics statistics,
}); });
if (get(selection).hasAnyChildren(new ListFileItem(id))) { if (get(selection).hasAnyChildren(new ListFileItem(id))) {
@ -198,7 +267,7 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
destroy: () => { destroy: () => {
fileState.delete(id); fileState.delete(id);
query.unsubscribe(); query.unsubscribe();
} },
}; };
} }
@ -210,22 +279,30 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
if (file) { if (file) {
items.forEach((item) => { items.forEach((item) => {
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex()); let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) { if (newTrackIndex === -1) {
removedItems.push(item); removedItems.push(item);
} }
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex()); let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) { if (newTrackIndex === -1) {
removedItems.push(item); removedItems.push(item);
} else { } else {
let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex((segment) => segment._data.segmentIndex === item.getSegmentIndex()); let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
(segment) => segment._data.segmentIndex === item.getSegmentIndex()
);
if (newSegmentIndex === -1) { if (newSegmentIndex === -1) {
removedItems.push(item); removedItems.push(item);
} }
} }
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
let newWaypointIndex = file.wpt.findIndex((wpt) => wpt._data.index === item.getWaypointIndex()); let newWaypointIndex = file.wpt.findIndex(
(wpt) => wpt._data.index === item.getWaypointIndex()
);
if (newWaypointIndex === -1) { if (newWaypointIndex === -1) {
removedItems.push(item); removedItems.push(item);
} }
@ -255,9 +332,10 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
// Commit the changes to the file state to the database // Commit the changes to the file state to the database
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) { function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
let changedFileIds = getChangedFileIds(patch); let changedFileIds = getChangedFileIds(patch);
let updatedFileIds: string[] = [], deletedFileIds: string[] = []; let updatedFileIds: string[] = [],
deletedFileIds: string[] = [];
changedFileIds.forEach(id => { changedFileIds.forEach((id) => {
if (newFileState.has(id)) { if (newFileState.has(id)) {
updatedFileIds.push(id); updatedFileIds.push(id);
} else { } else {
@ -265,8 +343,10 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
} }
}); });
let updatedFiles = updatedFileIds.map(id => newFileState.get(id)).filter(file => file !== undefined) as GPXFile[]; let updatedFiles = updatedFileIds
updatedFileIds = updatedFiles.map(file => file._data.id); .map((id) => newFileState.get(id))
.filter((file) => file !== undefined) as GPXFile[];
updatedFileIds = updatedFiles.map((file) => file._data.id);
updateSelection(updatedFiles, deletedFileIds); updateSelection(updatedFiles, deletedFileIds);
@ -282,13 +362,15 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
}); });
} }
export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy?: () => void }>> = writable(new Map()); export const fileObservers: Writable<
Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy?: () => void }>
> = writable(new Map());
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files // Observe the file ids in the database, and maintain a map of file observers for the corresponding files
export function observeFilesFromDatabase(fitBounds: boolean) { export function observeFilesFromDatabase(fitBounds: boolean) {
let initialize = true; let initialize = true;
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => { liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
if (initialize) { if (initialize) {
if (fitBounds && dbFileIds.length > 0) { if (fitBounds && dbFileIds.length > 0) {
initTargetMapBounds(dbFileIds); initTargetMapBounds(dbFileIds);
@ -296,17 +378,21 @@ export function observeFilesFromDatabase(fitBounds: boolean) {
initialize = false; initialize = false;
} }
// Find new files to observe // Find new files to observe
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1])); let newFiles = dbFileIds
.filter((id) => !get(fileObservers).has(id))
.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
// Find deleted files to stop observing // Find deleted files to stop observing
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id)); let deletedFiles = Array.from(get(fileObservers).keys()).filter(
(id) => !dbFileIds.find((fileId) => fileId === id)
);
// Update the store // Update the store
if (newFiles.length > 0 || deletedFiles.length > 0) { if (newFiles.length > 0 || deletedFiles.length > 0) {
fileObservers.update($files => { fileObservers.update(($files) => {
newFiles.forEach(id => { newFiles.forEach((id) => {
$files.set(id, dexieGPXFileStore(id)); $files.set(id, dexieGPXFileStore(id));
}); });
deletedFiles.forEach(id => { deletedFiles.forEach((id) => {
$files.get(id)?.destroy?.(); $files.get(id)?.destroy?.();
$files.delete(id); $files.delete(id);
}); });
@ -341,15 +427,28 @@ export function getStatistics(fileId: string): GPXStatisticsTree | undefined {
} }
const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1); const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => { const patchMinMaxIndex: Readable<{ min: number; max: number }> = dexieStore(
() =>
db.patches
.orderBy(':id')
.keys()
.then((keys) => {
if (keys.length === 0) { if (keys.length === 0) {
return { min: 0, max: 0 }; return { min: 0, max: 0 };
} else { } else {
return { min: keys[0], max: keys[keys.length - 1] + 1 }; return { min: keys[0], max: keys[keys.length - 1] + 1 };
} }
}), { min: 0, max: 0 }); }),
export const canUndo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min); { min: 0, max: 0 }
export const canRedo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1); );
export const canUndo: Readable<boolean> = derived(
[patchIndex, patchMinMaxIndex],
([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min
);
export const canRedo: Readable<boolean> = derived(
[patchIndex, patchMinMaxIndex],
([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1
);
// Helper function to apply a callback to the global file state // Helper function to apply a callback to the global file state
function applyGlobal(callback: (files: Map<string, GPXFile>) => void) { function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
@ -377,7 +476,12 @@ function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>
} }
// Helper function to apply different callbacks to multiple files // Helper function to apply different callbacks to multiple files
function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) { function applyEachToFilesAndGlobal(
fileIds: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) {
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => { const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
fileIds.forEach((fileId, index) => { fileIds.forEach((fileId, index) => {
let file = draft.get(fileId); let file = draft.get(fileId);
@ -400,16 +504,22 @@ async function storePatches(patch: Patch[], inversePatch: Patch[]) {
db.patches.where(':id').above(get(patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them db.patches.where(':id').above(get(patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them
let minmax = get(patchMinMaxIndex); let minmax = get(patchMinMaxIndex);
if (minmax.max - minmax.min + 1 > MAX_PATCHES) { if (minmax.max - minmax.min + 1 > MAX_PATCHES) {
db.patches.where(':id').belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES).delete(); db.patches
.where(':id')
.belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES)
.delete();
} }
} }
db.transaction('rw', db.patches, db.settings, async () => { db.transaction('rw', db.patches, db.settings, async () => {
let index = get(patchIndex) + 1; let index = get(patchIndex) + 1;
await db.patches.put({ await db.patches.put(
{
patch, patch,
inversePatch, inversePatch,
index,
},
index index
}, index); );
await db.settings.put(index, 'patchIndex'); await db.settings.put(index, 'patchIndex');
}); });
} }
@ -467,7 +577,12 @@ export const dbUtils = {
applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => void) => { applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => void) => {
applyToFiles(ids, callback); applyToFiles(ids, callback);
}, },
applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => { applyEachToFilesAndGlobal: (
ids: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) => {
applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context); applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context);
}, },
duplicateSelection: () => { duplicateSelection: () => {
@ -491,20 +606,33 @@ export const dbUtils = {
if (level === ListLevel.TRACK) { if (level === ListLevel.TRACK) {
for (let item of items) { for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex(); let trackIndex = (item as ListTrackItem).getTrackIndex();
file.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]); file.replaceTracks(trackIndex + 1, trackIndex, [
file.trk[trackIndex].clone(),
]);
} }
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
for (let item of items) { for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex(); let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex(); let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
file.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]); file.replaceTrackSegments(
trackIndex,
segmentIndex + 1,
segmentIndex,
[file.trk[trackIndex].trkseg[segmentIndex].clone()]
);
} }
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, file.wpt.map((wpt) => wpt.clone())); file.replaceWaypoints(
file.wpt.length,
file.wpt.length - 1,
file.wpt.map((wpt) => wpt.clone())
);
} else if (level === ListLevel.WAYPOINT) { } else if (level === ListLevel.WAYPOINT) {
for (let item of items) { for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex(); let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
file.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]); file.replaceWaypoints(waypointIndex + 1, waypointIndex, [
file.wpt[waypointIndex].clone(),
]);
} }
} }
} }
@ -513,16 +641,23 @@ export const dbUtils = {
}); });
}, },
addNewTrack: (fileId: string) => { addNewTrack: (fileId: string) => {
dbUtils.applyToFile(fileId, (file) => file.replaceTracks(file.trk.length, file.trk.length, [new Track()])); dbUtils.applyToFile(fileId, (file) =>
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
);
}, },
addNewSegment: (fileId: string, trackIndex: number) => { addNewSegment: (fileId: string, trackIndex: number) => {
dbUtils.applyToFile(fileId, (file) => { dbUtils.applyToFile(fileId, (file) => {
let track = file.trk[trackIndex]; let track = file.trk[trackIndex];
track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [new TrackSegment()]); track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [
new TrackSegment(),
]);
}); });
}, },
reverseSelection: () => { reverseSelection: () => {
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || get(gpxStatistics).local.points?.length <= 1) { if (
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
get(gpxStatistics).local.points?.length <= 1
) {
return; return;
} }
applyGlobal((draft) => { applyGlobal((draft) => {
@ -579,13 +714,13 @@ export const dbUtils = {
let target: ListItem = new ListRootItem(); let target: ListItem = new ListRootItem();
let targetFile: GPXFile | undefined = undefined; let targetFile: GPXFile | undefined = undefined;
let toMerge: { let toMerge: {
trk: Track[], trk: Track[];
trkseg: TrackSegment[], trkseg: TrackSegment[];
wpt: Waypoint[] wpt: Waypoint[];
} = { } = {
trk: [], trk: [],
trkseg: [], trkseg: [],
wpt: [] wpt: [],
}; };
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = draft.get(fileId); let file = draft.get(fileId);
@ -608,8 +743,15 @@ export const dbUtils = {
if (level === ListLevel.TRACK) { if (level === ListLevel.TRACK) {
items.forEach((item, index) => { items.forEach((item, index) => {
let trackIndex = (item as ListTrackItem).getTrackIndex(); let trackIndex = (item as ListTrackItem).getTrackIndex();
toMerge.trkseg.splice(0, 0, ...originalFile.trk[trackIndex].trkseg.map((segment) => segment.clone())); toMerge.trkseg.splice(
if (index === items.length - 1) { // Order is reversed, so the last track is the first one and the one to keep 0,
0,
...originalFile.trk[trackIndex].trkseg.map((segment) =>
segment.clone()
)
);
if (index === items.length - 1) {
// Order is reversed, so the last track is the first one and the one to keep
target = item; target = item;
file.trk[trackIndex].trkseg = []; file.trk[trackIndex].trkseg = [];
} else { } else {
@ -620,10 +762,15 @@ export const dbUtils = {
items.forEach((item, index) => { items.forEach((item, index) => {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex(); let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex(); let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
if (index === items.length - 1) { // Order is reversed, so the last segment is the first one and the one to keep if (index === items.length - 1) {
// Order is reversed, so the last segment is the first one and the one to keep
target = item; target = item;
} }
toMerge.trkseg.splice(0, 0, originalFile.trk[trackIndex].trkseg[segmentIndex].clone()); toMerge.trkseg.splice(
0,
0,
originalFile.trk[trackIndex].trkseg[segmentIndex].clone()
);
file.trk[trackIndex].trkseg.splice(segmentIndex, 1); file.trk[trackIndex].trkseg.splice(segmentIndex, 1);
}); });
} }
@ -635,15 +782,24 @@ export const dbUtils = {
if (mergeTraces) { if (mergeTraces) {
let statistics = get(gpxStatistics); let statistics = get(gpxStatistics);
let speed = statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined; let speed =
statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
let startTime: Date | undefined = undefined; let startTime: Date | undefined = undefined;
if (speed !== undefined) { if (speed !== undefined) {
if (statistics.local.points.length > 0 && statistics.local.points[0].time !== undefined) { if (
statistics.local.points.length > 0 &&
statistics.local.points[0].time !== undefined
) {
startTime = statistics.local.points[0].time; startTime = statistics.local.points[0].time;
} else { } else {
let index = statistics.local.points.findIndex((point) => point.time !== undefined); let index = statistics.local.points.findIndex(
(point) => point.time !== undefined
);
if (index !== -1) { if (index !== -1) {
startTime = new Date(statistics.local.points[index].time.getTime() - 1000 * 3600 * statistics.local.distance.total[index] / speed); startTime = new Date(
statistics.local.points[index].time.getTime() -
(1000 * 3600 * statistics.local.distance.total[index]) / speed
);
} }
} }
} }
@ -652,7 +808,14 @@ export const dbUtils = {
let s = new TrackSegment(); let s = new TrackSegment();
toMerge.trk.map((track) => { toMerge.trk.map((track) => {
track.trkseg.forEach((segment) => { track.trkseg.forEach((segment) => {
s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime, removeGaps); s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
}); });
}); });
toMerge.trk = [toMerge.trk[0]]; toMerge.trk = [toMerge.trk[0]];
@ -661,7 +824,14 @@ export const dbUtils = {
if (toMerge.trkseg.length > 0) { if (toMerge.trkseg.length > 0) {
let s = new TrackSegment(); let s = new TrackSegment();
toMerge.trkseg.forEach((segment) => { toMerge.trkseg.forEach((segment) => {
s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime, removeGaps); s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
}); });
toMerge.trkseg = [s]; toMerge.trkseg = [s];
} }
@ -677,7 +847,12 @@ export const dbUtils = {
} else if (target instanceof ListTrackSegmentItem) { } else if (target instanceof ListTrackSegmentItem) {
let trackIndex = target.getTrackIndex(); let trackIndex = target.getTrackIndex();
let segmentIndex = target.getSegmentIndex(); let segmentIndex = target.getSegmentIndex();
targetFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex - 1, toMerge.trkseg); targetFile.replaceTrackSegments(
trackIndex,
segmentIndex,
segmentIndex - 1,
toMerge.trkseg
);
} }
} }
}); });
@ -700,11 +875,15 @@ export const dbUtils = {
start -= length; start -= length;
end -= length; end -= length;
} else if (level === ListLevel.TRACK) { } else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.crop(start, end, trackIndices); file.crop(start, end, trackIndices);
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.crop(start, end, trackIndices, segmentIndices); file.crop(start, end, trackIndices, segmentIndices);
} }
} }
@ -724,14 +903,17 @@ export const dbUtils = {
return { return {
wptIndex: wptIndex, wptIndex: wptIndex,
index: [0], index: [0],
distance: Number.MAX_VALUE distance: Number.MAX_VALUE,
}; };
}) });
file.trk.forEach((track, index) => { file.trk.forEach((track, index) => {
track.getSegments().forEach((segment) => { track.getSegments().forEach((segment) => {
segment.trkpt.forEach((point) => { segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => { file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(point.getCoordinates(), wpt.getCoordinates()); let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) { if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist; closest[wptIndex].distance = dist;
closest[wptIndex].index = [index]; closest[wptIndex].index = [index];
@ -739,7 +921,7 @@ export const dbUtils = {
closest[wptIndex].index.push(index); closest[wptIndex].index.push(index);
} }
}); });
}) });
}); });
}); });
@ -754,9 +936,16 @@ export const dbUtils = {
return t; return t;
}); });
newFile.replaceTracks(0, file.trk.length - 1, tracks); newFile.replaceTracks(0, file.trk.length - 1, tracks);
newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex])); newFile.replaceWaypoints(
0,
file.wpt.length - 1,
closest
.filter((c) => c.index.includes(index))
.map((c) => file.wpt[c.wptIndex])
);
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = track.name ?? `${file.metadata.name} (${index + 1})`; newFile.metadata.name =
track.name ?? `${file.metadata.name} (${index + 1})`;
draft.set(newFile._data.id, freeze(newFile)); draft.set(newFile._data.id, freeze(newFile));
}); });
} else if (file.trk.length === 1) { } else if (file.trk.length === 1) {
@ -766,13 +955,16 @@ export const dbUtils = {
return { return {
wptIndex: wptIndex, wptIndex: wptIndex,
index: [0], index: [0],
distance: Number.MAX_VALUE distance: Number.MAX_VALUE,
}; };
}) });
file.trk[0].trkseg.forEach((segment, index) => { file.trk[0].trkseg.forEach((segment, index) => {
segment.trkpt.forEach((point) => { segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => { file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(point.getCoordinates(), wpt.getCoordinates()); let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) { if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist; closest[wptIndex].distance = dist;
closest[wptIndex].index = [index]; closest[wptIndex].index = [index];
@ -785,8 +977,16 @@ export const dbUtils = {
file.trk[0].trkseg.forEach((segment, index) => { file.trk[0].trkseg.forEach((segment, index) => {
let newFile = file.clone(); let newFile = file.clone();
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment]); newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex])); segment,
]);
newFile.replaceWaypoints(
0,
file.wpt.length - 1,
closest
.filter((c) => c.index.includes(index))
.map((c) => file.wpt[c.wptIndex])
);
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
draft.set(newFile._data.id, freeze(newFile)); draft.set(newFile._data.id, freeze(newFile));
@ -815,7 +1015,13 @@ export const dbUtils = {
}); });
}); });
}, },
split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates, trkptIndex?: number) { split(
fileId: string,
trackIndex: number,
segmentIndex: number,
coordinates: Coordinates,
trkptIndex?: number
) {
let splitType = get(splitAs); let splitType = get(splitAs);
return applyGlobal((draft) => { return applyGlobal((draft) => {
let file = getFile(fileId); let file = getFile(fileId);
@ -833,7 +1039,10 @@ export const dbUtils = {
let absoluteIndex = minIndex; let absoluteIndex = minIndex;
file.forEachSegment((seg, trkIndex, segIndex) => { file.forEachSegment((seg, trkIndex, segIndex) => {
if ((trkIndex < trackIndex && splitType === SplitType.FILES) || (trkIndex === trackIndex && segIndex < segmentIndex)) { if (
(trkIndex < trackIndex && splitType === SplitType.FILES) ||
(trkIndex === trackIndex && segIndex < segmentIndex)
) {
absoluteIndex += seg.trkpt.length; absoluteIndex += seg.trkpt.length;
} }
}); });
@ -863,13 +1072,21 @@ export const dbUtils = {
start.crop(0, minIndex); start.crop(0, minIndex);
let end = segment.clone(); let end = segment.clone();
end.crop(minIndex, segment.trkpt.length - 1); end.crop(minIndex, segment.trkpt.length - 1);
newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [start, end]); newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [
start,
end,
]);
} }
} }
} }
}); });
}, },
cleanSelection: (bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean) => { cleanSelection: (
bounds: [Coordinates, Coordinates],
inside: boolean,
deleteTrackPoints: boolean,
deleteWaypoints: boolean
) => {
if (get(selection).size === 0) { if (get(selection).size === 0) {
return; return;
} }
@ -880,16 +1097,35 @@ export const dbUtils = {
if (level === ListLevel.FILE) { if (level === ListLevel.FILE) {
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints); file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
} else if (level === ListLevel.TRACK) { } else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); let trackIndices = items.map((item) =>
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices); (item as ListTrackItem).getTrackIndex()
);
file.clean(
bounds,
inside,
deleteTrackPoints,
deleteWaypoints,
trackIndices
);
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); let segmentIndices = items.map((item) =>
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices, segmentIndices); (item as ListTrackSegmentItem).getSegmentIndex()
);
file.clean(
bounds,
inside,
deleteTrackPoints,
deleteWaypoints,
trackIndices,
segmentIndices
);
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
file.clean(bounds, inside, false, deleteWaypoints); file.clean(bounds, inside, false, deleteWaypoints);
} else if (level === ListLevel.WAYPOINT) { } else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex()); let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices); file.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices);
} }
} }
@ -911,7 +1147,15 @@ export const dbUtils = {
let segmentIndex = item.getSegmentIndex(); let segmentIndex = item.getSegmentIndex();
let points = itemsAndPoints.get(item); let points = itemsAndPoints.get(item);
if (points) { if (points) {
file.replaceTrackPoints(trackIndex, segmentIndex, 0, file.trk[trackIndex].trkseg[segmentIndex].getNumberOfTrackPoints() - 1, points); file.replaceTrackPoints(
trackIndex,
segmentIndex,
0,
file.trk[trackIndex].trkseg[
segmentIndex
].getNumberOfTrackPoints() - 1,
points
);
} }
} }
} }
@ -938,7 +1182,9 @@ export const dbUtils = {
}); });
} else { } else {
let fileIds = new Set<string>(); let fileIds = new Set<string>();
get(selection).getSelected().forEach((item) => { get(selection)
.getSelected()
.forEach((item) => {
fileIds.add(item.getFileId()); fileIds.add(item.getFileId());
}); });
let wpt = new Waypoint(waypoint); let wpt = new Waypoint(waypoint);
@ -984,16 +1230,22 @@ export const dbUtils = {
if (level === ListLevel.FILE) { if (level === ListLevel.FILE) {
file.setHidden(hidden); file.setHidden(hidden);
} else if (level === ListLevel.TRACK) { } else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.setHidden(hidden, trackIndices); file.setHidden(hidden, trackIndices);
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.setHidden(hidden, trackIndices, segmentIndices); file.setHidden(hidden, trackIndices, segmentIndices);
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
file.setHiddenWaypoints(hidden); file.setHiddenWaypoints(hidden);
} else if (level === ListLevel.WAYPOINT) { } else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex()); let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.setHiddenWaypoints(hidden, waypointIndices); file.setHiddenWaypoints(hidden, waypointIndices);
} }
} }
@ -1020,7 +1272,12 @@ export const dbUtils = {
for (let item of items) { for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex(); let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex(); let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []); file.replaceTrackSegments(
trackIndex,
segmentIndex,
segmentIndex,
[]
);
} }
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
file.replaceWaypoints(0, file.wpt.length - 1, []); file.replaceWaypoints(0, file.wpt.length - 1, []);
@ -1053,14 +1310,18 @@ export const dbUtils = {
}); });
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
let trackIndex = (items[0] as ListTrackSegmentItem).getTrackIndex(); let trackIndex = (items[0] as ListTrackSegmentItem).getTrackIndex();
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
segmentIndices.forEach((segmentIndex) => { segmentIndices.forEach((segmentIndex) => {
points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints()); points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints());
}); });
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
points.push(...file.wpt); points.push(...file.wpt);
} else if (level === ListLevel.WAYPOINT) { } else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex()); let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
points.push(...waypointIndices.map((waypointIndex) => file.wpt[waypointIndex])); points.push(...waypointIndices.map((waypointIndex) => file.wpt[waypointIndex]));
} }
} }
@ -1078,16 +1339,22 @@ export const dbUtils = {
if (level === ListLevel.FILE) { if (level === ListLevel.FILE) {
file.addElevation(elevations); file.addElevation(elevations);
} else if (level === ListLevel.TRACK) { } else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.addElevation(elevations, trackIndices, undefined, []); file.addElevation(elevations, trackIndices, undefined, []);
} else if (level === ListLevel.SEGMENT) { } else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.addElevation(elevations, trackIndices, segmentIndices, []); file.addElevation(elevations, trackIndices, segmentIndices, []);
} else if (level === ListLevel.WAYPOINTS) { } else if (level === ListLevel.WAYPOINTS) {
file.addElevation(elevations, [], [], undefined); file.addElevation(elevations, [], [], undefined);
} else if (level === ListLevel.WAYPOINT) { } else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex()); let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.addElevation(elevations, [], [], waypointIndices); file.addElevation(elevations, [], [], waypointIndices);
} }
} }
@ -1114,7 +1381,7 @@ export const dbUtils = {
undo: () => { undo: () => {
if (get(canUndo)) { if (get(canUndo)) {
let index = get(patchIndex); let index = get(patchIndex);
db.patches.get(index).then(patch => { db.patches.get(index).then((patch) => {
if (patch) { if (patch) {
applyPatch(patch.inversePatch); applyPatch(patch.inversePatch);
db.settings.put(index - 1, 'patchIndex'); db.settings.put(index - 1, 'patchIndex');
@ -1125,12 +1392,12 @@ export const dbUtils = {
redo: () => { redo: () => {
if (get(canRedo)) { if (get(canRedo)) {
let index = get(patchIndex) + 1; let index = get(patchIndex) + 1;
db.patches.get(index).then(patch => { db.patches.get(index).then((patch) => {
if (patch) { if (patch) {
applyPatch(patch.patch); applyPatch(patch.patch);
db.settings.put(index, 'patchIndex'); db.settings.put(index, 'patchIndex');
} }
}); });
} }
} },
} };

View File

@ -1,10 +1,10 @@
export const languages: Record<string, string> = { export const languages: Record<string, string> = {
'en': 'English', en: 'English',
'es': 'Español', es: 'Español',
'de': 'Deutsch', de: 'Deutsch',
'fr': 'Français', fr: 'Français',
'it': 'Italiano', it: 'Italiano',
'nl': 'Nederlands', nl: 'Nederlands',
'pt-BR': 'Português (Brasil)', 'pt-BR': 'Português (Brasil)',
'zh': '简体中文', zh: '简体中文',
}; };

View File

@ -10,13 +10,19 @@ function generateSitemap() {
const pages = glob.sync('**/*.html', { cwd: 'build' }).map((page) => `/${page}`); const pages = glob.sync('**/*.html', { cwd: 'build' }).map((page) => `/${page}`);
let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n'; let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n'; sitemap +=
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n';
pages.forEach((page) => { pages.forEach((page) => {
const path = page.replace('/index.html', '').replace('.html', ''); const path = page.replace('/index.html', '').replace('.html', '');
const rootDir = path.split('/')[1]; const rootDir = path.split('/')[1];
if (path.includes('embed') || path.includes('404') || languages[path] || languages[rootDir]) { if (
path.includes('embed') ||
path.includes('404') ||
languages[path] ||
languages[rootDir]
) {
// Skip localized pages // Skip localized pages
return; return;
} }

View File

@ -11,7 +11,7 @@ import {
applyToOrderedSelectedItemsFromFile, applyToOrderedSelectedItemsFromFile,
selectFile, selectFile,
selectItem, selectItem,
selection selection,
} from '$lib/components/file-list/Selection'; } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
@ -19,7 +19,7 @@ import {
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
ListWaypointsItem ListWaypointsItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
@ -43,7 +43,10 @@ export function updateGPXData() {
if (stats) { if (stats) {
let first = true; let first = true;
items.forEach((item) => { items.forEach((item) => {
if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) { if (
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
first
) {
statistics.mergeWith(stats.getStatisticsFor(item)); statistics.mergeWith(stats.getStatisticsFor(item));
first = false; first = false;
} }
@ -110,7 +113,8 @@ derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
let currentZoom = $map.getZoom(); let currentZoom = $map.getZoom();
let currentBounds = $map.getBounds(); let currentBounds = $map.getBounds();
if (bounds.total !== get(fileObservers).size && if (
bounds.total !== get(fileObservers).size &&
currentBounds && currentBounds &&
currentZoom > 2 // Extend current bounds only if the map is zoomed in currentZoom > 2 // Extend current bounds only if the map is zoomed in
) { ) {
@ -137,7 +141,10 @@ export function initTargetMapBounds(ids: string[]) {
}); });
} }
export function updateTargetMapBounds(id: string, bounds: { southWest: Coordinates; northEast: Coordinates }) { export function updateTargetMapBounds(
id: string,
bounds: { southWest: Coordinates; northEast: Coordinates }
) {
if (get(targetMapBounds).ids.indexOf(id) === -1) { if (get(targetMapBounds).ids.indexOf(id) === -1) {
return; return;
} }
@ -159,8 +166,7 @@ export function updateTargetMapBounds(id: string, bounds: { southWest: Coordinat
}); });
} }
export function centerMapOnSelection( export function centerMapOnSelection() {
) {
let selected = get(selection).getSelected(); let selected = get(selection).getSelected();
let bounds = new mapboxgl.LngLatBounds(); let bounds = new mapboxgl.LngLatBounds();
@ -187,7 +193,7 @@ export function centerMapOnSelection(
get(map)?.fitBounds(bounds, { get(map)?.fitBounds(bounds, {
padding: 80, padding: 80,
easing: () => 1, easing: () => 1,
maxZoom: 15 maxZoom: 15,
}); });
} }
@ -203,7 +209,7 @@ export enum Tool {
EXTRACT, EXTRACT,
ELEVATION, ELEVATION,
REDUCE, REDUCE,
CLEAN CLEAN,
} }
export const currentTool = writable<Tool | null>(null); export const currentTool = writable<Tool | null>(null);
export const splitAs = writable(SplitType.FILES); export const splitAs = writable(SplitType.FILES);
@ -410,7 +416,7 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) {
async function exportFiles(fileIds: string[], exclude: string[]) { async function exportFiles(fileIds: string[], exclude: string[]) {
if (fileIds.length > 1) { if (fileIds.length > 1) {
await exportFilesAsZip(fileIds, exclude) await exportFilesAsZip(fileIds, exclude);
} else { } else {
const firstFileId = fileIds.at(0); const firstFileId = fileIds.at(0);
if (firstFileId != null) { if (firstFileId != null) {
@ -468,7 +474,10 @@ export function updateAllHidden() {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
hidden = hidden && file._data.hidden === true; hidden = hidden && file._data.hidden === true;
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { } else if (
item instanceof ListTrackItem &&
item.getTrackIndex() < file.trk.length
) {
hidden = hidden && file.trk[item.getTrackIndex()]._data.hidden === true; hidden = hidden && file.trk[item.getTrackIndex()]._data.hidden === true;
} else if ( } else if (
item instanceof ListTrackSegmentItem && item instanceof ListTrackSegmentItem &&
@ -477,10 +486,14 @@ export function updateAllHidden() {
) { ) {
hidden = hidden =
hidden && hidden &&
file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true; file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data
.hidden === true;
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
hidden = hidden && file._data.hiddenWpt === true; hidden = hidden && file._data.hiddenWpt === true;
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) { } else if (
item instanceof ListWaypointItem &&
item.getWaypointIndex() < file.wpt.length
) {
hidden = hidden && file.wpt[item.getWaypointIndex()]._data.hidden === true; hidden = hidden && file.wpt[item.getWaypointIndex()]._data.hidden === true;
} }
} }
@ -496,6 +509,6 @@ export const editStyle = writable(false);
export enum ExportState { export enum ExportState {
NONE, NONE,
SELECTION, SELECTION,
ALL ALL,
} }
export const exportState = writable<ExportState>(ExportState.NONE); export const exportState = writable<ExportState>(ExportState.NONE);

View File

@ -161,7 +161,9 @@ export function getPowerUnits() {
} }
export function getTemperatureUnits() { export function getTemperatureUnits() {
return get(temperatureUnits) === 'celsius' ? get(_)('units.celsius') : get(_)('units.fahrenheit'); return get(temperatureUnits) === 'celsius'
? get(_)('units.celsius')
: get(_)('units.fahrenheit');
} }
// Convert only the value // Convert only the value

View File

@ -1,18 +1,18 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
import { cubicOut } from "svelte/easing"; import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from "svelte/transition"; import type { TransitionConfig } from 'svelte/transition';
import { get } from "svelte/store"; import { get } from 'svelte/store';
import { map } from "./stores"; import { map } from './stores';
import { base } from "$app/paths"; import { base } from '$app/paths';
import { languages } from "$lib/languages"; import { languages } from '$lib/languages';
import { locale } from "svelte-i18n"; import { locale } from 'svelte-i18n';
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from "gpx"; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import tilebelt from "@mapbox/tilebelt"; import tilebelt from '@mapbox/tilebelt';
import { PUBLIC_MAPBOX_TOKEN } from "$env/static/public"; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import PNGReader from "png.js"; import PNGReader from 'png.js';
import type { DateFormatter } from "@internationalized/date"; import type { DateFormatter } from '@internationalized/date';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -30,7 +30,7 @@ export const flyAndScale = (
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 50 } params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 50 }
): TransitionConfig => { ): TransitionConfig => {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = ( const scaleConversion = (
valueA: number, valueA: number,
@ -46,13 +46,11 @@ export const flyAndScale = (
return valueB; return valueB;
}; };
const styleToString = ( const styleToString = (style: Record<string, number | string | undefined>): string => {
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => { return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str; if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`; return str + `${key}:${style[key]};`;
}, ""); }, '');
}; };
return { return {
@ -65,14 +63,18 @@ export const flyAndScale = (
return styleToString({ return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t opacity: t,
}); });
}, },
easing: cubicOut easing: cubicOut,
}; };
}; };
export function getClosestLinePoint(points: TrackPoint[], point: TrackPoint | Coordinates, details: any = {}): TrackPoint { export function getClosestLinePoint(
points: TrackPoint[],
point: TrackPoint | Coordinates,
details: any = {}
): TrackPoint {
let closest = points[0]; let closest = points[0];
let closestDist = Number.MAX_VALUE; let closestDist = Number.MAX_VALUE;
for (let i = 0; i < points.length - 1; i++) { for (let i = 0; i < points.length - 1; i++) {
@ -94,16 +96,34 @@ export function getClosestLinePoint(points: TrackPoint[], point: TrackPoint | Co
return closest; return closest;
} }
export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], ELEVATION_ZOOM: number = 13, tileSize = 512): Promise<number[]> { export function getElevation(
let coordinates = points.map((point) => (point instanceof TrackPoint || point instanceof Waypoint) ? point.getCoordinates() : point); points: (TrackPoint | Waypoint | Coordinates)[],
ELEVATION_ZOOM: number = 13,
tileSize = 512
): Promise<number[]> {
let coordinates = points.map((point) =>
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
);
let bbox = new mapboxgl.LngLatBounds(); let bbox = new mapboxgl.LngLatBounds();
coordinates.forEach((coord) => bbox.extend(coord)); coordinates.forEach((coord) => bbox.extend(coord));
let tiles = coordinates.map((coord) => tilebelt.pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM)); let tiles = coordinates.map((coord) =>
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) => tile.split(',').map((x) => parseInt(x))); 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<string, any>(); let pngs = new Map<string, any>();
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 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)); let png = new PNGReader(new Uint8Array(buffer));
png.parse((err, png) => { png.parse((err, png) => {
if (err) { if (err) {
@ -113,9 +133,12 @@ export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], EL
resolve(true); resolve(true);
} }
}); });
}))); })
)
);
return Promise.all(promises).then(() => coordinates.map((coord, index) => { return Promise.all(promises).then(() =>
coordinates.map((coord, index) => {
let tile = tiles[index]; let tile = tiles[index];
let png = pngs.get(tile.join(',')); let png = pngs.get(tile.join(','));
@ -134,15 +157,24 @@ export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], EL
const p00 = png.getPixel(_x, _y); const p00 = png.getPixel(_x, _y);
const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1)); const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1));
const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y); 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)); 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 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 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 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); 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; return (
})); ele00 * (1 - dx) * (1 - dy) +
ele01 * (1 - dx) * dy +
ele10 * dx * (1 - dy) +
ele11 * dx * dy
);
})
);
} }
let previousCursors: string[] = []; let previousCursors: string[] = [];
@ -226,7 +258,7 @@ export function getURLForLanguage(lang: string | null | undefined, path: string)
function getDateFormatter(locale: string) { function getDateFormatter(locale: string) {
return new Intl.DateTimeFormat(locale, { return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium', dateStyle: 'medium',
timeStyle: 'medium' timeStyle: 'medium',
}); });
} }

View File

@ -18,7 +18,9 @@ export async function load({ params }) {
for (let guide of Object.keys(guides)) { for (let guide of Object.keys(guides)) {
guideTitles[guide] = (await getModule(language, guide)).metadata.title; guideTitles[guide] = (await getModule(language, guide)).metadata.title;
for (let subguide of guides[guide]) { for (let subguide of guides[guide]) {
guideTitles[`${guide}/${subguide}`] = (await getModule(language, `${guide}/${subguide}`)).metadata.title; guideTitles[`${guide}/${subguide}`] = (
await getModule(language, `${guide}/${subguide}`)
).metadata.title;
} }
} }

View File

@ -23,7 +23,8 @@
if ($page.url.searchParams.has('embed')) { if ($page.url.searchParams.has('embed')) {
// convert old embedding options to new format and redirect to new embed page // convert old embedding options to new format and redirect to new embed page
let folders = $page.url.pathname.split('/'); let folders = $page.url.pathname.split('/');
let locale = folders.indexOf('l') >= 0 ? folders[folders.indexOf('l') + 1] ?? 'en' : 'en'; let locale =
folders.indexOf('l') >= 0 ? (folders[folders.indexOf('l') + 1] ?? 'en') : 'en';
window.location.href = `${getURLForLanguage(locale, '/embed')}?options=${encodeURIComponent(JSON.stringify(convertOldEmbeddingOptions($page.url.searchParams)))}`; window.location.href = `${getURLForLanguage(locale, '/embed')}?options=${encodeURIComponent(JSON.stringify(convertOldEmbeddingOptions($page.url.searchParams)))}`;
} }
}); });

View File

@ -11,7 +11,10 @@
<p class="text-xl -mt-6">{$_('page_not_found')}</p> <p class="text-xl -mt-6">{$_('page_not_found')}</p>
<Logo class="h-40 my-3 animate-spin" style="animation-duration: 20000ms" iconOnly={true} /> <Logo class="h-40 my-3 animate-spin" style="animation-duration: 20000ms" iconOnly={true} />
<div class="w-full flex flex-row flex-wrap gap-3 justify-center"> <div class="w-full flex flex-row flex-wrap gap-3 justify-center">
<Button href={getURLForLanguage($locale, '/')} class="text-base w-1/4 min-w-fit rounded-full"> <Button
href={getURLForLanguage($locale, '/')}
class="text-base w-1/4 min-w-fit rounded-full"
>
<Home size="18" class="mr-1.5" /> <Home size="18" class="mr-1.5" />
{$_('homepage.home')} {$_('homepage.home')}
</Button> </Button>

View File

@ -13,7 +13,7 @@
PencilRuler, PencilRuler,
PenLine, PenLine,
Route, Route,
Scale Scale,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
@ -49,7 +49,9 @@
<div class="-mt-12 sm:mt-0"> <div class="-mt-12 sm:mt-0">
<div class="px-12 w-full flex flex-col items-center"> <div class="px-12 w-full flex flex-col items-center">
<div class="flex flex-col gap-6 items-center max-w-3xl"> <div class="flex flex-col gap-6 items-center max-w-3xl">
<h1 class="text-4xl sm:text-6xl font-black text-center">{$_('metadata.home_title')}</h1> <h1 class="text-4xl sm:text-6xl font-black text-center">
{$_('metadata.home_title')}
</h1>
<div class="text-lg sm:text-xl text-muted-foreground text-center"> <div class="text-lg sm:text-xl text-muted-foreground text-center">
{$_('metadata.description')} {$_('metadata.description')}
</div> </div>
@ -81,7 +83,9 @@
</div> </div>
</div> </div>
<div class="px-12 sm:px-24 w-full flex flex-col items-center"> <div class="px-12 sm:px-24 w-full flex flex-col items-center">
<div class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"> <div
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
>
<div class="markdown text-center"> <div class="markdown text-center">
<h1> <h1>
<Route size="24" class="mr-1 inline-block align-baseline" /> <Route size="24" class="mr-1 inline-block align-baseline" />
@ -95,7 +99,9 @@
</div> </div>
</div> </div>
<div class="px-12 sm:px-24 w-full flex flex-col items-center"> <div class="px-12 sm:px-24 w-full flex flex-col items-center">
<div class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"> <div
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
>
<div class="markdown text-center md:hidden"> <div class="markdown text-center md:hidden">
<h1> <h1>
<PencilRuler size="24" class="mr-1 inline-block align-baseline" /> <PencilRuler size="24" class="mr-1 inline-block align-baseline" />
@ -167,7 +173,9 @@
<ChartArea size="24" class="mr-1 inline-block align-baseline" /> <ChartArea size="24" class="mr-1 inline-block align-baseline" />
{$_('homepage.data_visualization')} {$_('homepage.data_visualization')}
</h1> </h1>
<p class="text-muted-foreground mb-6">{$_('homepage.data_visualization_description')}</p> <p class="text-muted-foreground mb-6">
{$_('homepage.data_visualization_description')}
</p>
</div> </div>
<div class="h-48 w-full"> <div class="h-48 w-full">
<ElevationProfile <ElevationProfile
@ -189,7 +197,9 @@
</div> </div>
</div> </div>
<div class="px-12 sm:px-24 w-full flex flex-col items-center"> <div class="px-12 sm:px-24 w-full flex flex-col items-center">
<div class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"> <div
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
>
<div class="markdown text-center md:hidden"> <div class="markdown text-center md:hidden">
<h1> <h1>
<Scale size="24" class="mr-1 inline-block align-baseline" /> <Scale size="24" class="mr-1 inline-block align-baseline" />
@ -228,7 +238,12 @@
<DocsContainer module={fundingModule.default} /> <DocsContainer module={fundingModule.default} />
{/await} {/await}
<Button href="https://ko-fi.com/gpxstudio" target="_blank" class="text-base"> <Button href="https://ko-fi.com/gpxstudio" target="_blank" class="text-base">
<Heart size="16" class="mr-1" fill="rgb(var(--support))" color="rgb(var(--support))" /> <Heart
size="16"
class="mr-1"
fill="rgb(var(--support))"
color="rgb(var(--support))"
/>
<span>{$_('homepage.support_button')}</span> <span>{$_('homepage.support_button')}</span>
</Button> </Button>
</div> </div>
@ -248,7 +263,9 @@
<div <div
class="max-w-4xl flex flex-col lg:flex-row items-center justify-center gap-x-12 gap-y-6 p-6 border rounded-2xl shadow-xl bg-secondary" class="max-w-4xl flex flex-col lg:flex-row items-center justify-center gap-x-12 gap-y-6 p-6 border rounded-2xl shadow-xl bg-secondary"
> >
<div class="shrink-0 flex flex-col sm:flex-row lg:flex-col items-center gap-x-4 gap-y-2"> <div
class="shrink-0 flex flex-col sm:flex-row lg:flex-col items-center gap-x-4 gap-y-2"
>
<div class="text-lg font-semibold text-muted-foreground"> <div class="text-lg font-semibold text-muted-foreground">
❤️ {$_('homepage.supported_by')} ❤️ {$_('homepage.supported_by')}
</div> </div>

View File

@ -26,7 +26,7 @@
bottomPanelSize, bottomPanelSize,
rightPanelSize, rightPanelSize,
additionalDatasets, additionalDatasets,
elevationFill elevationFill,
} = settings; } = settings;
onMount(() => { onMount(() => {
@ -109,7 +109,12 @@
{/if} {/if}
</div> </div>
{#if $elevationProfile} {#if $elevationProfile}
<Resizer orientation="row" bind:after={$bottomPanelSize} minAfter={100} maxAfter={300} /> <Resizer
orientation="row"
bind:after={$bottomPanelSize}
minAfter={100}
maxAfter={300}
/>
{/if} {/if}
<div <div
class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4" class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"

View File

@ -4,7 +4,7 @@
import Embedding from '$lib/components/embedding/Embedding.svelte'; import Embedding from '$lib/components/embedding/Embedding.svelte';
import { import {
getMergedEmbeddingOptions, getMergedEmbeddingOptions,
type EmbeddingOptions type EmbeddingOptions,
} from '$lib/components/embedding/Embedding'; } from '$lib/components/embedding/Embedding';
let embeddingOptions: EmbeddingOptions | undefined = undefined; let embeddingOptions: EmbeddingOptions | undefined = undefined;

View File

@ -11,7 +11,9 @@
</script> </script>
<div class="grow px-12 pt-6 pb-12 flex flex-row gap-24"> <div class="grow px-12 pt-6 pb-12 flex flex-row gap-24">
<div class="hidden md:flex flex-col gap-2 w-40 sticky mt-[27px] top-[108px] self-start shrink-0"> <div
class="hidden md:flex flex-col gap-2 w-40 sticky mt-[27px] top-[108px] self-start shrink-0"
>
{#each Object.keys(guides) as guide} {#each Object.keys(guides) as guide}
<Button <Button
variant="link" variant="link"

View File

@ -30,7 +30,11 @@
href={getURLForLanguage($locale, `/help/${guide}/${subGuide}`)} href={getURLForLanguage($locale, `/help/${guide}/${subGuide}`)}
class="min-h-8 h-fit min-w-24 px-0 py-1 text-muted-foreground text-base text-center whitespace-normal" class="min-h-8 h-fit min-w-24 px-0 py-1 text-muted-foreground text-base text-center whitespace-normal"
> >
<svelte:component this={guideIcons[subGuide]} size="16" class="mr-1 shrink-0" /> <svelte:component
this={guideIcons[subGuide]}
size="16"
class="mr-1 shrink-0"
/>
{data.guideTitles[`${guide}/${subGuide}`]} {data.guideTitles[`${guide}/${subGuide}`]}
</Button> </Button>
{/each} {/each}

View File

@ -59,8 +59,8 @@
{#if $locale === 'en'} {#if $locale === 'en'}
<Button <Button
variant="link" variant="link"
href="https://github.com/gpxstudio/gpx.studio/edit/dev/website/src/lib/docs/en/{$page.params href="https://github.com/gpxstudio/gpx.studio/edit/dev/website/src/lib/docs/en/{$page
.guide}.mdx" .params.guide}.mdx"
target="_blank" target="_blank"
class="p-0 h-6 ml-auto text-link" class="p-0 h-6 ml-auto text-link"
> >

View File

@ -4,7 +4,7 @@ import { mdsvex } from 'mdsvex';
/** @type {import('mdsvex').MdsvexOptions} */ /** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = { const mdsvexOptions = {
extensions: ['.mdx'] extensions: ['.mdx'],
}; };
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
@ -16,7 +16,7 @@ const config = {
pages: 'build', pages: 'build',
assets: 'build', assets: 'build',
precompress: false, precompress: false,
strict: true strict: true,
}), }),
paths: { paths: {
base: process.argv.includes('dev') ? '' : process.env.BASE_PATH, base: process.argv.includes('dev') ? '' : process.env.BASE_PATH,
@ -25,8 +25,8 @@ const config = {
prerender: { prerender: {
entries: ['/', '/404'], entries: ['/', '/404'],
crawl: true, crawl: true,
} },
} },
}; };
export default config; export default config;

View File

@ -1,64 +1,64 @@
import { fontFamily } from "tailwindcss/defaultTheme"; import { fontFamily } from 'tailwindcss/defaultTheme';
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const config = { const config = {
darkMode: ["class"], darkMode: ['class'],
content: ["./src/**/*.{html,js,svelte,ts}"], content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ["dark"], safelist: ['dark'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border) / <alpha-value>)", border: 'hsl(var(--border) / <alpha-value>)',
input: "hsl(var(--input) / <alpha-value>)", input: 'hsl(var(--input) / <alpha-value>)',
ring: "hsl(var(--ring) / <alpha-value>)", ring: 'hsl(var(--ring) / <alpha-value>)',
background: "hsl(var(--background) / <alpha-value>)", background: 'hsl(var(--background) / <alpha-value>)',
foreground: "hsl(var(--foreground) / <alpha-value>)", foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: { primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)", DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: "hsl(var(--primary-foreground) / <alpha-value>)" foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)", DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)" foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)", DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)" foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)", DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: "hsl(var(--muted-foreground) / <alpha-value>)" foreground: 'hsl(var(--muted-foreground) / <alpha-value>)',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)", DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: "hsl(var(--accent-foreground) / <alpha-value>)" foreground: 'hsl(var(--accent-foreground) / <alpha-value>)',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)", DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: "hsl(var(--popover-foreground) / <alpha-value>)" foreground: 'hsl(var(--popover-foreground) / <alpha-value>)',
}, },
card: { card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)", DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: "hsl(var(--card-foreground) / <alpha-value>)" foreground: 'hsl(var(--card-foreground) / <alpha-value>)',
}, },
support: "rgb(var(--support))", support: 'rgb(var(--support))',
link: "rgb(var(--link))" link: 'rgb(var(--link))',
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)" sm: 'calc(var(--radius) - 4px)',
}, },
fontFamily: { fontFamily: {
sans: [...fontFamily.sans] sans: [...fontFamily.sans],
}, },
screens: { screens: {
"xs": "540px", xs: '540px',
} },
}, },
supports: { supports: {
dvh: 'height: 100dvh', dvh: 'height: 100dvh',
} },
}, },
}; };

View File

@ -1,12 +1,16 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { enhancedImages } from '@sveltejs/enhanced-img'; import { enhancedImages } from '@sveltejs/enhanced-img';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills' import { nodePolyfills } from 'vite-plugin-node-polyfills';
export default defineConfig({ export default defineConfig({
plugins: [nodePolyfills({ plugins: [
nodePolyfills({
globals: { globals: {
Buffer: true, Buffer: true,
}, },
}), enhancedImages(), sveltekit()] }),
enhancedImages(),
sveltekit(),
],
}); });