prettier config + format all, closes #175
This commit is contained in:
parent
01cfd448f0
commit
0b457f9a1e
16
.prettierrc
Normal file
16
.prettierrc
Normal 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
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
|
||||
13
.vscode/settings.json
vendored
Normal file
13
.vscode/settings.json
vendored
Normal 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
1
gpx/.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
package-lock.json
|
||||
1573
gpx/package-lock.json
generated
1573
gpx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,10 +18,14 @@
|
||||
"devDependencies": {
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/node": "^20.16.10",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postinstall": "npm run build"
|
||||
"postinstall": "npm run build",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
}
|
||||
}
|
||||
|
||||
955
gpx/src/gpx.ts
955
gpx/src/gpx.ts
File diff suppressed because it is too large
Load Diff
@ -2,4 +2,3 @@ export * from './gpx';
|
||||
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||
export { parseGPX, buildGPX } from './io';
|
||||
export * from './simplify';
|
||||
|
||||
|
||||
@ -1,32 +1,40 @@
|
||||
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
||||
import { GPXFileType } from "./types";
|
||||
import { GPXFile } from "./gpx";
|
||||
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||
import { GPXFileType } from './types';
|
||||
import { GPXFile } from './gpx';
|
||||
|
||||
const attributesWithNamespace = {
|
||||
'RoutePointExtension': 'gpxx:RoutePointExtension',
|
||||
'rpt': 'gpxx:rpt',
|
||||
'TrackPointExtension': 'gpxtpx:TrackPointExtension',
|
||||
'PowerExtension': 'gpxpx:PowerExtension',
|
||||
'atemp': 'gpxtpx:atemp',
|
||||
'hr': 'gpxtpx:hr',
|
||||
'cad': 'gpxtpx:cad',
|
||||
'Extensions': 'gpxtpx:Extensions',
|
||||
'PowerInWatts': 'gpxpx:PowerInWatts',
|
||||
'power': 'gpxpx:PowerExtension',
|
||||
'line': 'gpx_style:line',
|
||||
'color': 'gpx_style:color',
|
||||
'opacity': 'gpx_style:opacity',
|
||||
'width': 'gpx_style:width',
|
||||
RoutePointExtension: 'gpxx:RoutePointExtension',
|
||||
rpt: 'gpxx:rpt',
|
||||
TrackPointExtension: 'gpxtpx:TrackPointExtension',
|
||||
PowerExtension: 'gpxpx:PowerExtension',
|
||||
atemp: 'gpxtpx:atemp',
|
||||
hr: 'gpxtpx:hr',
|
||||
cad: 'gpxtpx:cad',
|
||||
Extensions: 'gpxtpx:Extensions',
|
||||
PowerInWatts: 'gpxpx:PowerInWatts',
|
||||
power: 'gpxpx:PowerExtension',
|
||||
line: 'gpx_style:line',
|
||||
color: 'gpx_style:color',
|
||||
opacity: 'gpx_style:opacity',
|
||||
width: 'gpx_style:width',
|
||||
};
|
||||
|
||||
export function parseGPX(gpxData: string): GPXFile {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "",
|
||||
attributeNamePrefix: '',
|
||||
attributesGroupName: 'attributes',
|
||||
removeNSPrefix: true,
|
||||
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) {
|
||||
if (attrName === 'lat' || attrName === 'lon') {
|
||||
@ -51,8 +59,14 @@ export function parseGPX(gpxData: string): GPXFile {
|
||||
return new Date(tagValue);
|
||||
}
|
||||
|
||||
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' ||
|
||||
tagName === 'gpx_style:opacity' || tagName === 'gpx_style:width') {
|
||||
if (
|
||||
tagName === 'gpxtpx:atemp' ||
|
||||
tagName === 'gpxtpx:hr' ||
|
||||
tagName === 'gpxtpx:cad' ||
|
||||
tagName === 'gpxpx:PowerInWatts' ||
|
||||
tagName === 'gpx_style:opacity' ||
|
||||
tagName === 'gpx_style:width'
|
||||
) {
|
||||
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
|
||||
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
||||
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;
|
||||
|
||||
// @ts-ignore
|
||||
if (parsed.metadata === "") {
|
||||
if (parsed.metadata === '') {
|
||||
parsed.metadata = {};
|
||||
}
|
||||
|
||||
@ -85,7 +99,7 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
||||
const builder = new XMLBuilder({
|
||||
format: true,
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "",
|
||||
attributeNamePrefix: '',
|
||||
attributesGroupName: 'attributes',
|
||||
suppressEmptyNode: true,
|
||||
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
|
||||
@ -96,13 +110,13 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
||||
},
|
||||
});
|
||||
|
||||
if (!gpx.attributes)
|
||||
gpx.attributes = {};
|
||||
if (!gpx.attributes) gpx.attributes = {};
|
||||
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
|
||||
gpx.attributes['version'] = '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['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:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
||||
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({
|
||||
"?xml": {
|
||||
'?xml': {
|
||||
attributes: {
|
||||
version: "1.0",
|
||||
encoding: "UTF-8",
|
||||
}
|
||||
version: '1.0',
|
||||
encoding: 'UTF-8',
|
||||
},
|
||||
gpx: removeEmptyElements(gpx)
|
||||
},
|
||||
gpx: removeEmptyElements(gpx),
|
||||
});
|
||||
}
|
||||
|
||||
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
||||
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];
|
||||
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
||||
removeEmptyElements(obj[key]);
|
||||
|
||||
@ -1,33 +1,48 @@
|
||||
import { TrackPoint } from "./gpx";
|
||||
import { Coordinates } from "./types";
|
||||
import { TrackPoint } from './gpx';
|
||||
import { Coordinates } from './types';
|
||||
|
||||
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
|
||||
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||
|
||||
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) {
|
||||
return [];
|
||||
} else if (points.length == 1) {
|
||||
return [{
|
||||
point: points[0]
|
||||
}];
|
||||
return [
|
||||
{
|
||||
point: points[0],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let simplified = [{
|
||||
point: points[0]
|
||||
}];
|
||||
let simplified = [
|
||||
{
|
||||
point: points[0],
|
||||
},
|
||||
];
|
||||
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
|
||||
simplified.push({
|
||||
point: points[points.length - 1]
|
||||
point: points[points.length - 1],
|
||||
});
|
||||
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 = {
|
||||
index: 0,
|
||||
distance: 0
|
||||
distance: 0,
|
||||
};
|
||||
|
||||
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 {
|
||||
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
||||
export function crossarcDistance(
|
||||
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 {
|
||||
@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
||||
}
|
||||
|
||||
// Is relative bearing obtuse?
|
||||
if (diff > (Math.PI / 2)) {
|
||||
if (diff > Math.PI / 2) {
|
||||
return dis13;
|
||||
}
|
||||
|
||||
@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
||||
|
||||
// Is p4 beyond the arc?
|
||||
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) {
|
||||
return distance(lat2, lon2, lat3, lon3);
|
||||
} else {
|
||||
@ -93,18 +117,32 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
||||
|
||||
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||
// 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 {
|
||||
// Finds the bearing from one lat / lon point to another.
|
||||
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
|
||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
|
||||
return Math.atan2(
|
||||
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 {
|
||||
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
||||
export function projectedPoint(
|
||||
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 {
|
||||
@ -132,7 +170,7 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
|
||||
}
|
||||
|
||||
// Is relative bearing obtuse?
|
||||
if (diff > (Math.PI / 2)) {
|
||||
if (diff > Math.PI / 2) {
|
||||
return coord1;
|
||||
}
|
||||
|
||||
@ -141,14 +179,22 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
|
||||
|
||||
// Is p4 beyond the arc?
|
||||
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) {
|
||||
return coord2;
|
||||
} else {
|
||||
// Determine the closest point (p4) on the great circle
|
||||
const f = dis14 / earthRadius;
|
||||
const lat4 = Math.asin(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));
|
||||
const lat4 = Math.asin(
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -93,11 +93,11 @@ export type TrackPointExtension = {
|
||||
'gpxtpx:hr'?: number;
|
||||
'gpxtpx:cad'?: number;
|
||||
'gpxtpx:Extensions'?: Record<string, string>;
|
||||
}
|
||||
};
|
||||
|
||||
export type PowerExtension = {
|
||||
'gpxpx:PowerInWatts'?: number;
|
||||
}
|
||||
};
|
||||
|
||||
export type Author = {
|
||||
name?: string;
|
||||
@ -114,12 +114,12 @@ export type RouteType = {
|
||||
type?: string;
|
||||
extensions?: TrackExtensions;
|
||||
rtept: WaypointType[];
|
||||
}
|
||||
};
|
||||
|
||||
export type RoutePointExtension = {
|
||||
'gpxx:rpt'?: GPXXRoutePoint[];
|
||||
}
|
||||
};
|
||||
|
||||
export type GPXXRoutePoint = {
|
||||
attributes: Coordinates;
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,9 +4,7 @@
|
||||
"target": "ES2015",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"include": ["src"]
|
||||
}
|
||||
@ -5,27 +5,27 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
extraFileExtensions: ['.svelte'],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -2,3 +2,5 @@
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
src/lib/components/ui
|
||||
*.mdx
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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%
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -72,7 +72,7 @@
|
||||
|
||||
--link: 80 190 255;
|
||||
|
||||
--ring: hsl(212.7,26.8%,83.9);
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,8 @@ export async function handle({ event, resolve }) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
export const surfaceColors: { [key: string]: string } = {
|
||||
"missing": "#d1d1d1",
|
||||
"paved": "#8c8c8c",
|
||||
"unpaved": "#6b443a",
|
||||
"asphalt": "#8c8c8c",
|
||||
"concrete": "#8c8c8c",
|
||||
"cobblestone": "#ffd991",
|
||||
"paving_stones": "#8c8c8c",
|
||||
"sett": "#ffd991",
|
||||
"metal": "#8c8c8c",
|
||||
"wood": "#6b443a",
|
||||
"compacted": "#ffffa8",
|
||||
"fine_gravel": "#ffffa8",
|
||||
"gravel": "#ffffa8",
|
||||
"pebblestone": "#ffffa8",
|
||||
"rock": "#ffd991",
|
||||
"dirt": "#ffffa8",
|
||||
"ground": "#6b443a",
|
||||
"earth": "#6b443a",
|
||||
"mud": "#6b443a",
|
||||
"sand": "#ffffc4",
|
||||
"grass": "#61b55c",
|
||||
"grass_paver": "#61b55c",
|
||||
"clay": "#6b443a",
|
||||
"stone": "#ffd991",
|
||||
missing: '#d1d1d1',
|
||||
paved: '#8c8c8c',
|
||||
unpaved: '#6b443a',
|
||||
asphalt: '#8c8c8c',
|
||||
concrete: '#8c8c8c',
|
||||
cobblestone: '#ffd991',
|
||||
paving_stones: '#8c8c8c',
|
||||
sett: '#ffd991',
|
||||
metal: '#8c8c8c',
|
||||
wood: '#6b443a',
|
||||
compacted: '#ffffa8',
|
||||
fine_gravel: '#ffffa8',
|
||||
gravel: '#ffffa8',
|
||||
pebblestone: '#ffffa8',
|
||||
rock: '#ffd991',
|
||||
dirt: '#ffffa8',
|
||||
ground: '#6b443a',
|
||||
earth: '#6b443a',
|
||||
mud: '#6b443a',
|
||||
sand: '#ffffc4',
|
||||
grass: '#61b55c',
|
||||
grass_paver: '#61b55c',
|
||||
clay: '#6b443a',
|
||||
stone: '#ffd991',
|
||||
};
|
||||
|
||||
export function getSurfaceColor(surface: string): string {
|
||||
@ -30,66 +30,72 @@ export function getSurfaceColor(surface: string): string {
|
||||
}
|
||||
|
||||
export const highwayColors: { [key: string]: string } = {
|
||||
"missing": "#d1d1d1",
|
||||
"motorway": "#ff4d33",
|
||||
"motorway_link": "#ff4d33",
|
||||
"trunk": "#ff5e4d",
|
||||
"trunk_link": "#ff947f",
|
||||
"primary": "#ff6e5c",
|
||||
"primary_link": "#ff6e5c",
|
||||
"secondary": "#ff8d7b",
|
||||
"secondary_link": "#ff8d7b",
|
||||
"tertiary": "#ffd75f",
|
||||
"tertiary_link": "#ffd75f",
|
||||
"unclassified": "#f1f2a5",
|
||||
"road": "#f1f2a5",
|
||||
"residential": "#73b2ff",
|
||||
"living_street": "#73b2ff",
|
||||
"service": "#9c9cd9",
|
||||
"track": "#a8e381",
|
||||
"footway": "#a8e381",
|
||||
"path": "#a8e381",
|
||||
"pedestrian": "#a8e381",
|
||||
"cycleway": "#9de2ff",
|
||||
"construction": "#e09a4a",
|
||||
"bridleway": "#946f43",
|
||||
"raceway": "#ff0000",
|
||||
"rest_area": "#9c9cd9",
|
||||
"services": "#9c9cd9",
|
||||
"corridor": "#474747",
|
||||
"elevator": "#474747",
|
||||
"steps": "#474747",
|
||||
"bus_stop": "#8545a3",
|
||||
"busway": "#8545a3",
|
||||
"via_ferrata": "#474747"
|
||||
missing: '#d1d1d1',
|
||||
motorway: '#ff4d33',
|
||||
motorway_link: '#ff4d33',
|
||||
trunk: '#ff5e4d',
|
||||
trunk_link: '#ff947f',
|
||||
primary: '#ff6e5c',
|
||||
primary_link: '#ff6e5c',
|
||||
secondary: '#ff8d7b',
|
||||
secondary_link: '#ff8d7b',
|
||||
tertiary: '#ffd75f',
|
||||
tertiary_link: '#ffd75f',
|
||||
unclassified: '#f1f2a5',
|
||||
road: '#f1f2a5',
|
||||
residential: '#73b2ff',
|
||||
living_street: '#73b2ff',
|
||||
service: '#9c9cd9',
|
||||
track: '#a8e381',
|
||||
footway: '#a8e381',
|
||||
path: '#a8e381',
|
||||
pedestrian: '#a8e381',
|
||||
cycleway: '#9de2ff',
|
||||
construction: '#e09a4a',
|
||||
bridleway: '#946f43',
|
||||
raceway: '#ff0000',
|
||||
rest_area: '#9c9cd9',
|
||||
services: '#9c9cd9',
|
||||
corridor: '#474747',
|
||||
elevator: '#474747',
|
||||
steps: '#474747',
|
||||
bus_stop: '#8545a3',
|
||||
busway: '#8545a3',
|
||||
via_ferrata: '#474747',
|
||||
};
|
||||
|
||||
export const sacScaleColors: { [key: string]: string } = {
|
||||
"hiking": "#007700",
|
||||
"mountain_hiking": "#1843ad",
|
||||
"demanding_mountain_hiking": "#ffff00",
|
||||
"alpine_hiking": "#ff9233",
|
||||
"demanding_alpine_hiking": "#ff0000",
|
||||
"difficult_alpine_hiking": "#000000",
|
||||
hiking: '#007700',
|
||||
mountain_hiking: '#1843ad',
|
||||
demanding_mountain_hiking: '#ffff00',
|
||||
alpine_hiking: '#ff9233',
|
||||
demanding_alpine_hiking: '#ff0000',
|
||||
difficult_alpine_hiking: '#000000',
|
||||
};
|
||||
|
||||
export const mtbScaleColors: { [key: string]: string } = {
|
||||
"0-": "#007700",
|
||||
"0": "#007700",
|
||||
"0+": "#007700",
|
||||
"1-": "#1843ad",
|
||||
"1": "#1843ad",
|
||||
"1+": "#1843ad",
|
||||
"2-": "#ffff00",
|
||||
"2": "#ffff00",
|
||||
"2+": "#ffff00",
|
||||
"3": "#ff0000",
|
||||
"4": "#00ff00",
|
||||
"5": "#000000",
|
||||
"6": "#b105eb",
|
||||
'0-': '#007700',
|
||||
'0': '#007700',
|
||||
'0+': '#007700',
|
||||
'1-': '#1843ad',
|
||||
'1': '#1843ad',
|
||||
'1+': '#1843ad',
|
||||
'2-': '#ffff00',
|
||||
'2': '#ffff00',
|
||||
'2+': '#ffff00',
|
||||
'3': '#ff0000',
|
||||
'4': '#00ff00',
|
||||
'5': '#000000',
|
||||
'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');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
@ -104,11 +110,11 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
||||
if (sacScaleColor) {
|
||||
ctx.strokeStyle = sacScaleColor;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(halfSize - halfLineWidth, - halfLineWidth);
|
||||
ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
|
||||
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(- halfLineWidth, halfSize - halfLineWidth);
|
||||
ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
|
||||
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
|
||||
ctx.stroke();
|
||||
}
|
||||
@ -119,8 +125,8 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
||||
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(- halfLineWidth, halfSize + halfLineWidth);
|
||||
ctx.lineTo(halfSize + halfLineWidth, - halfLineWidth);
|
||||
ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
|
||||
ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
@ -128,12 +134,16 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
||||
}
|
||||
|
||||
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 sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
|
||||
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
|
||||
if (sacScale || mtbScale) {
|
||||
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter(x => x).join('-')}`;
|
||||
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter((x) => x).join('-')}`;
|
||||
if (!patterns[patternId]) {
|
||||
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
@ -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 { 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";
|
||||
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 {
|
||||
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 = {
|
||||
value: string;
|
||||
@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
|
||||
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
||||
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
||||
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' },
|
||||
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 },
|
||||
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
||||
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
||||
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
||||
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
||||
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 },
|
||||
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
||||
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
||||
@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Object.keys(symbols).find(key => symbols[key].value === value);
|
||||
return Object.keys(symbols).find((key) => symbols[key].value === value);
|
||||
}
|
||||
}
|
||||
@ -13,14 +13,14 @@
|
||||
indexName: 'gpx',
|
||||
container: '#docsearch',
|
||||
searchParameters: {
|
||||
facetFilters: ['lang:' + ($locale ?? 'en')]
|
||||
facetFilters: ['lang:' + ($locale ?? 'en')],
|
||||
},
|
||||
placeholder: $_('docs.search.search'),
|
||||
disableUserPersonalization: true,
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: $_('docs.search.search'),
|
||||
buttonAriaLabel: $_('docs.search.search')
|
||||
buttonAriaLabel: $_('docs.search.search'),
|
||||
},
|
||||
modal: {
|
||||
searchBox: {
|
||||
@ -28,19 +28,19 @@
|
||||
resetButtonAriaLabel: $_('docs.search.clear'),
|
||||
cancelButtonText: $_('docs.search.cancel'),
|
||||
cancelButtonAriaLabel: $_('docs.search.cancel'),
|
||||
searchInputLabel: $_('docs.search.search')
|
||||
searchInputLabel: $_('docs.search.search'),
|
||||
},
|
||||
footer: {
|
||||
selectText: $_('docs.search.to_select'),
|
||||
navigateText: $_('docs.search.to_navigate'),
|
||||
closeText: $_('docs.search.to_close')
|
||||
closeText: $_('docs.search.to_close'),
|
||||
},
|
||||
noResultsScreen: {
|
||||
noResultsText: $_('docs.search.no_results'),
|
||||
suggestedQueryText: $_('docs.search.no_results_suggestion')
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestedQueryText: $_('docs.search.no_results_suggestion'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
item: new TrackPoint({
|
||||
attributes: {
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng
|
||||
}
|
||||
})
|
||||
lon: e.lngLat.lng,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
Circle,
|
||||
Check,
|
||||
ChartNoAxesColumn,
|
||||
Construction
|
||||
Construction,
|
||||
} from 'lucide-svelte';
|
||||
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
|
||||
import { _ } from 'svelte-i18n';
|
||||
@ -33,7 +33,7 @@
|
||||
getHeartRateWithUnits,
|
||||
getPowerWithUnits,
|
||||
getTemperatureWithUnits,
|
||||
getVelocityWithUnits
|
||||
getVelocityWithUnits,
|
||||
} from '$lib/units';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { GPXStatistics } from 'gpx';
|
||||
@ -72,37 +72,37 @@
|
||||
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||
},
|
||||
align: 'inner',
|
||||
maxRotation: 0
|
||||
}
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
callback: function (value: number) {
|
||||
return getElevationWithUnits(value, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
datasets: {
|
||||
line: {
|
||||
pointRadius: 0,
|
||||
tension: 0.4,
|
||||
borderWidth: 2,
|
||||
cubicInterpolationMode: 'monotone'
|
||||
}
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
display: false,
|
||||
},
|
||||
decimation: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: () => !dragging && !panning,
|
||||
@ -141,16 +141,20 @@
|
||||
let slope = {
|
||||
at: point.slope.at.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 highway = point.extensions.highway ? point.extensions.highway : 'unknown';
|
||||
let surface = point.extensions.surface
|
||||
? point.extensions.surface
|
||||
: 'unknown';
|
||||
let highway = point.extensions.highway
|
||||
? point.extensions.highway
|
||||
: 'unknown';
|
||||
let sacScale = point.extensions.sac_scale;
|
||||
let mtbScale = point.extensions.mtb_scale;
|
||||
|
||||
let labels = [
|
||||
` ${$_('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') {
|
||||
@ -162,7 +166,9 @@
|
||||
if (elevationFill === 'highway') {
|
||||
labels.push(
|
||||
` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${
|
||||
sacScale ? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})` : ''
|
||||
sacScale
|
||||
? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})`
|
||||
: ''
|
||||
}`
|
||||
);
|
||||
if (mtbScale) {
|
||||
@ -175,8 +181,8 @@
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
@ -190,18 +196,19 @@
|
||||
},
|
||||
onPanComplete: function () {
|
||||
panning = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
},
|
||||
mode: 'x',
|
||||
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
|
||||
if (
|
||||
event.deltaY < 0 &&
|
||||
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()
|
||||
) < 0.01
|
||||
) {
|
||||
@ -210,21 +217,21 @@
|
||||
}
|
||||
|
||||
$slicedGPXStatistics = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
x: {
|
||||
min: 'original',
|
||||
max: 'original',
|
||||
minRange: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
minRange: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: false,
|
||||
onResize: function () {
|
||||
updateOverlay();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||
@ -233,10 +240,10 @@
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: {
|
||||
display: false
|
||||
display: false,
|
||||
},
|
||||
reverse: () => id === 'speed' && $velocityUnits === 'pace',
|
||||
display: false
|
||||
display: false,
|
||||
};
|
||||
});
|
||||
|
||||
@ -246,7 +253,7 @@
|
||||
chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: []
|
||||
datasets: [],
|
||||
},
|
||||
options,
|
||||
plugins: [
|
||||
@ -259,16 +266,16 @@
|
||||
marker.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Map marker to show on hover
|
||||
let element = document.createElement('div');
|
||||
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
||||
marker = new mapboxgl.Marker({
|
||||
element
|
||||
element,
|
||||
});
|
||||
|
||||
let startIndex = 0;
|
||||
@ -278,7 +285,7 @@
|
||||
evt,
|
||||
'x',
|
||||
{
|
||||
intersect: false
|
||||
intersect: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
@ -321,9 +328,12 @@
|
||||
startIndex = endIndex;
|
||||
} else if (startIndex !== endIndex) {
|
||||
$slicedGPXStatistics = [
|
||||
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
|
||||
$gpxStatistics.slice(
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex)
|
||||
),
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -357,76 +367,76 @@
|
||||
slope: {
|
||||
at: data.local.slope.at[index],
|
||||
segment: data.local.slope.segment[index],
|
||||
length: data.local.slope.length[index]
|
||||
length: data.local.slope.length[index],
|
||||
},
|
||||
extensions: point.getExtensions(),
|
||||
coordinates: point.getCoordinates(),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
fill: 'start',
|
||||
order: 1
|
||||
order: 1,
|
||||
};
|
||||
chart.data.datasets[1] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yspeed',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[2] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate(),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yhr',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[3] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence(),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ycad',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[4] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yatemp',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[5] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower(),
|
||||
index: index
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ypower',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
};
|
||||
chart.options.scales.x['min'] = 0;
|
||||
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||
@ -453,15 +463,15 @@
|
||||
$: if (chart) {
|
||||
if (elevationFill === 'slope') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: slopeFillCallback
|
||||
backgroundColor: slopeFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'surface') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: surfaceFillCallback
|
||||
backgroundColor: surfaceFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'highway') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: highwayFillCallback
|
||||
backgroundColor: highwayFillCallback,
|
||||
};
|
||||
} else {
|
||||
chart.data.datasets[0]['segment'] = {};
|
||||
@ -553,7 +563,11 @@
|
||||
<ChartNoAxesColumn size="18" />
|
||||
</ButtonWithTooltip>
|
||||
</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
|
||||
class="flex flex-col items-start gap-0 p-1"
|
||||
type="single"
|
||||
@ -613,7 +627,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<Zap size="15" class="mr-1" />
|
||||
{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}
|
||||
{$velocityUnits === 'speed'
|
||||
? $_('quantities.speed')
|
||||
: $_('quantities.pace')}
|
||||
</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"
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
exportSelectedFiles,
|
||||
ExportState,
|
||||
exportState,
|
||||
gpxStatistics
|
||||
gpxStatistics,
|
||||
} from '$lib/stores';
|
||||
import { fileObservers } from '$lib/db';
|
||||
import {
|
||||
@ -20,7 +20,7 @@
|
||||
HeartPulse,
|
||||
Orbit,
|
||||
Thermometer,
|
||||
SquareActivity
|
||||
SquareActivity,
|
||||
} from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { selection } from './file-list/Selection';
|
||||
@ -35,7 +35,7 @@
|
||||
cad: true,
|
||||
atemp: true,
|
||||
power: true,
|
||||
extensions: true
|
||||
extensions: true,
|
||||
};
|
||||
let hide: Record<string, boolean> = {
|
||||
time: false,
|
||||
@ -43,7 +43,7 @@
|
||||
cad: false,
|
||||
atemp: false,
|
||||
power: false,
|
||||
extensions: false
|
||||
extensions: false,
|
||||
};
|
||||
|
||||
$: if ($exportState !== ExportState.NONE) {
|
||||
@ -121,7 +121,9 @@
|
||||
</Button>
|
||||
</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'}"
|
||||
>
|
||||
@ -144,7 +146,9 @@
|
||||
{$_('quantities.time')}
|
||||
</Label>
|
||||
</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} />
|
||||
<Label for="export-extensions" class="flex flex-row items-center gap-1">
|
||||
<Earth size="16" />
|
||||
|
||||
@ -53,13 +53,17 @@
|
||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||
<Tooltip
|
||||
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
|
||||
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
||||
'quantities.moving'
|
||||
)} / {$_('quantities.total')})"
|
||||
label="{$velocityUnits === 'speed'
|
||||
? $_('quantities.speed')
|
||||
: $_('quantities.pace')} ({$_('quantities.moving')} / {$_('quantities.total')})"
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<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>
|
||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||
</span>
|
||||
@ -68,7 +72,9 @@
|
||||
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||
<Tooltip
|
||||
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">
|
||||
<Timer size="16" class="mr-1" />
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
|
||||
let selected = {
|
||||
value: '',
|
||||
label: ''
|
||||
label: '',
|
||||
};
|
||||
|
||||
$: if ($locale) {
|
||||
selected = {
|
||||
value: $locale,
|
||||
label: languages[$locale]
|
||||
label: languages[$locale],
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -26,13 +26,13 @@
|
||||
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
||||
maxZoom: 15,
|
||||
linear: true,
|
||||
easing: () => 1
|
||||
easing: () => 1,
|
||||
};
|
||||
|
||||
const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
|
||||
settings;
|
||||
let scaleControl = new mapboxgl.ScaleControl({
|
||||
unit: $distanceUnits
|
||||
unit: $distanceUnits,
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
@ -70,12 +70,12 @@
|
||||
sources: {},
|
||||
layers: [],
|
||||
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',
|
||||
url: ''
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
id: 'overlays',
|
||||
@ -83,10 +83,10 @@
|
||||
data: {
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: []
|
||||
}
|
||||
}
|
||||
]
|
||||
layers: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
projection: 'globe',
|
||||
zoom: 0,
|
||||
@ -94,7 +94,7 @@
|
||||
language,
|
||||
attributionControl: false,
|
||||
logoPosition: 'bottom-right',
|
||||
boxZoom: false
|
||||
boxZoom: false,
|
||||
});
|
||||
newMap.on('load', () => {
|
||||
$map = newMap; // only set the store after the map has loaded
|
||||
@ -104,13 +104,13 @@
|
||||
|
||||
newMap.addControl(
|
||||
new mapboxgl.AttributionControl({
|
||||
compact: true
|
||||
compact: true,
|
||||
})
|
||||
);
|
||||
|
||||
newMap.addControl(
|
||||
new mapboxgl.NavigationControl({
|
||||
visualizePitch: true
|
||||
visualizePitch: true,
|
||||
})
|
||||
);
|
||||
|
||||
@ -134,12 +134,12 @@
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
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;
|
||||
geocoder._onKeyDown = (e: KeyboardEvent) => {
|
||||
@ -157,11 +157,11 @@
|
||||
newMap.addControl(
|
||||
new mapboxgl.GeolocateControl({
|
||||
positionOptions: {
|
||||
enableHighAccuracy: true
|
||||
enableHighAccuracy: true,
|
||||
},
|
||||
fitBoundsOptions,
|
||||
trackUserLocation: true,
|
||||
showUserHeading: true
|
||||
showUserHeading: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -173,25 +173,25 @@
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14
|
||||
maxzoom: 14,
|
||||
});
|
||||
if (newMap.getPitch() > 0) {
|
||||
newMap.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: 1
|
||||
exaggeration: 1,
|
||||
});
|
||||
}
|
||||
newMap.setFog({
|
||||
color: 'rgb(186, 210, 235)',
|
||||
'high-color': 'rgb(36, 92, 223)',
|
||||
'horizon-blend': 0.1,
|
||||
'space-color': 'rgb(156, 240, 255)'
|
||||
'space-color': 'rgb(156, 240, 255)',
|
||||
});
|
||||
newMap.on('pitch', () => {
|
||||
if (newMap.getPitch() > 0) {
|
||||
newMap.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: 1
|
||||
exaggeration: 1,
|
||||
});
|
||||
} else {
|
||||
newMap.setTerrain(null);
|
||||
@ -215,7 +215,8 @@
|
||||
<div {...$$restProps}>
|
||||
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></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'
|
||||
: ''} {embeddedApp ? 'z-30' : ''}"
|
||||
>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { TrackPoint, Waypoint } from "gpx";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { tick } from "svelte";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import MapPopupComponent from "./MapPopup.svelte";
|
||||
import { TrackPoint, Waypoint } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { tick } from 'svelte';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import MapPopupComponent from './MapPopup.svelte';
|
||||
|
||||
export type PopupItem<T = Waypoint | TrackPoint | any> = {
|
||||
item: T;
|
||||
@ -23,16 +23,15 @@ export class MapPopup {
|
||||
let component = new MapPopupComponent({
|
||||
target: document.body,
|
||||
props: {
|
||||
item: this.item
|
||||
}
|
||||
item: this.item,
|
||||
},
|
||||
});
|
||||
|
||||
tick().then(() => this.popup.setDOMContent(component.container));
|
||||
}
|
||||
|
||||
setItem(item: PopupItem | null) {
|
||||
if (item)
|
||||
item.hide = () => this.hide();
|
||||
if (item) item.hide = () => this.hide();
|
||||
this.item.set(item);
|
||||
if (item === null) {
|
||||
this.hide();
|
||||
@ -76,6 +75,8 @@ export class MapPopup {
|
||||
if (i === null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@
|
||||
FileX,
|
||||
BookOpenText,
|
||||
ChartArea,
|
||||
Maximize
|
||||
Maximize,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import {
|
||||
@ -56,7 +56,7 @@
|
||||
editStyle,
|
||||
exportState,
|
||||
ExportState,
|
||||
centerMapOnSelection
|
||||
centerMapOnSelection,
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
copied,
|
||||
@ -64,7 +64,7 @@
|
||||
cutSelection,
|
||||
pasteSelection,
|
||||
selectAll,
|
||||
selection
|
||||
selection,
|
||||
} from '$lib/components/file-list/Selection';
|
||||
import { derived } from 'svelte/store';
|
||||
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
||||
@ -91,7 +91,7 @@
|
||||
distanceMarkers,
|
||||
directionMarkers,
|
||||
streetViewSource,
|
||||
routing
|
||||
routing,
|
||||
} = settings;
|
||||
|
||||
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
|
||||
@ -151,18 +151,27 @@
|
||||
<Shortcut key="O" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<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" />
|
||||
{$_('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<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" />
|
||||
{$_('menu.close')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
</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" />
|
||||
{$_('menu.close_all')}
|
||||
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||
@ -207,7 +216,11 @@
|
||||
disabled={$selection.size !== 1 ||
|
||||
!$selection
|
||||
.getSelected()
|
||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||
.every(
|
||||
(item) =>
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem
|
||||
)}
|
||||
on:click={() => ($editMetadata = true)}
|
||||
>
|
||||
<Info size="16" class="mr-1" />
|
||||
@ -218,7 +231,11 @@
|
||||
disabled={$selection.size === 0 ||
|
||||
!$selection
|
||||
.getSelected()
|
||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||
.every(
|
||||
(item) =>
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem
|
||||
)}
|
||||
on:click={() => ($editStyle = true)}
|
||||
>
|
||||
<PaintBucket size="16" class="mr-1" />
|
||||
@ -247,13 +264,16 @@
|
||||
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
|
||||
on:click={() =>
|
||||
dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
|
||||
disabled={$selection.size !== 1}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
{$_('menu.new_track')}
|
||||
</Menubar.Item>
|
||||
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)}
|
||||
{:else if $selection
|
||||
.getSelected()
|
||||
.some((item) => item instanceof ListTrackItem)}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
on:click={() => {
|
||||
@ -300,7 +320,9 @@
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 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}
|
||||
>
|
||||
<ClipboardPaste size="16" class="mr-1" />
|
||||
@ -309,7 +331,10 @@
|
||||
</Menubar.Item>
|
||||
{/if}
|
||||
<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" />
|
||||
{$_('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
@ -334,17 +359,25 @@
|
||||
</Menubar.CheckboxItem>
|
||||
<Menubar.Separator />
|
||||
<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 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.Separator />
|
||||
<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 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.Separator />
|
||||
<Menubar.Item inset on:click={toggle3D}>
|
||||
@ -368,9 +401,15 @@
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
||||
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="metric"
|
||||
>{$_('menu.metric')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="imperial"
|
||||
>{$_('menu.imperial')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="nautical"
|
||||
>{$_('menu.nautical')}</Menubar.RadioItem
|
||||
>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
@ -380,8 +419,12 @@
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
||||
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="speed"
|
||||
>{$_('quantities.speed')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="pace"
|
||||
>{$_('quantities.pace')}</Menubar.RadioItem
|
||||
>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
@ -391,8 +434,12 @@
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$temperatureUnits}>
|
||||
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="celsius"
|
||||
>{$_('menu.celsius')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="fahrenheit"
|
||||
>{$_('menu.fahrenheit')}</Menubar.RadioItem
|
||||
>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
@ -428,8 +475,11 @@
|
||||
setMode(value);
|
||||
}}
|
||||
>
|
||||
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="light"
|
||||
>{$_('menu.light')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem
|
||||
>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
@ -441,8 +491,12 @@
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$streetViewSource}>
|
||||
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="mapillary"
|
||||
>{$_('menu.mapillary')}</Menubar.RadioItem
|
||||
>
|
||||
<Menubar.RadioItem value="google"
|
||||
>{$_('menu.google')}</Menubar.RadioItem
|
||||
>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
|
||||
const handleMouseMove = (event: PointerEvent) => {
|
||||
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) {
|
||||
after = newAfter;
|
||||
} else if (newAfter < minAfter && after !== minAfter) {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
getDistanceUnits,
|
||||
getElevationUnits,
|
||||
getVelocityUnits,
|
||||
secondsToHHMMSS
|
||||
secondsToHHMMSS,
|
||||
} from '$lib/units';
|
||||
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
@ -18,7 +18,11 @@
|
||||
class="w-full max-w-3xl"
|
||||
/>
|
||||
{: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}
|
||||
</div>
|
||||
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
||||
|
||||
@ -1,39 +1,64 @@
|
||||
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
|
||||
import type { ComponentType } from "svelte";
|
||||
import {
|
||||
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[]> = {
|
||||
'getting-started': [],
|
||||
menu: ['file', 'edit', 'view', 'settings'],
|
||||
'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': [],
|
||||
'gpx': [],
|
||||
'integration': [],
|
||||
'faq': [],
|
||||
gpx: [],
|
||||
integration: [],
|
||||
faq: [],
|
||||
};
|
||||
|
||||
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
||||
"getting-started": "🚀",
|
||||
"menu": "📂 ⚙️",
|
||||
"file": File,
|
||||
"edit": FilePen,
|
||||
"view": View,
|
||||
"settings": Settings,
|
||||
"files-and-stats": "🗂 📈",
|
||||
"toolbar": "🧰",
|
||||
"routing": Pencil,
|
||||
"poi": MapPin,
|
||||
"scissors": Scissors,
|
||||
"time": CalendarClock,
|
||||
"merge": Group,
|
||||
"extract": Ungroup,
|
||||
"elevation": MountainSnow,
|
||||
"minify": Filter,
|
||||
"clean": SquareDashedMousePointer,
|
||||
"map-controls": "🗺",
|
||||
"gpx": "💾",
|
||||
"integration": "{ 👩💻 }",
|
||||
"faq": "🔮",
|
||||
'getting-started': '🚀',
|
||||
menu: '📂 ⚙️',
|
||||
file: File,
|
||||
edit: FilePen,
|
||||
view: View,
|
||||
settings: Settings,
|
||||
'files-and-stats': '🗂 📈',
|
||||
toolbar: '🧰',
|
||||
routing: Pencil,
|
||||
poi: MapPin,
|
||||
scissors: Scissors,
|
||||
time: CalendarClock,
|
||||
merge: Group,
|
||||
extract: Ungroup,
|
||||
elevation: MountainSnow,
|
||||
minify: Filter,
|
||||
clean: SquareDashedMousePointer,
|
||||
'map-controls': '🗺',
|
||||
gpx: '💾',
|
||||
integration: '{ 👩💻 }',
|
||||
faq: '🔮',
|
||||
};
|
||||
|
||||
export function getPreviousGuide(currentGuide: string): string | undefined {
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
embedding,
|
||||
loadFile,
|
||||
map,
|
||||
updateGPXData
|
||||
updateGPXData,
|
||||
} from '$lib/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
|
||||
@ -23,7 +23,7 @@
|
||||
import {
|
||||
allowedEmbeddingBasemaps,
|
||||
getFilesFromEmbeddingOptions,
|
||||
type EmbeddingOptions
|
||||
type EmbeddingOptions,
|
||||
} from './Embedding';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import { browser } from '$app/environment';
|
||||
@ -37,7 +37,7 @@
|
||||
temperatureUnits,
|
||||
fileOrder,
|
||||
distanceMarkers,
|
||||
directionMarkers
|
||||
directionMarkers,
|
||||
} = settings;
|
||||
|
||||
export let useHash = true;
|
||||
@ -50,7 +50,7 @@
|
||||
distanceUnits: 'metric',
|
||||
velocityUnits: 'speed',
|
||||
temperatureUnits: 'celsius',
|
||||
theme: 'system'
|
||||
theme: 'system',
|
||||
};
|
||||
|
||||
function applyOptions() {
|
||||
@ -74,12 +74,12 @@
|
||||
let bounds = {
|
||||
southWest: {
|
||||
lat: 90,
|
||||
lon: 180
|
||||
lon: 180,
|
||||
},
|
||||
northEast: {
|
||||
lat: -90,
|
||||
lon: -180
|
||||
}
|
||||
lon: -180,
|
||||
},
|
||||
};
|
||||
|
||||
fileObservers.update(($fileObservers) => {
|
||||
@ -96,12 +96,13 @@
|
||||
id,
|
||||
readable({
|
||||
file,
|
||||
statistics
|
||||
statistics,
|
||||
})
|
||||
);
|
||||
|
||||
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.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
|
||||
@ -130,12 +131,12 @@
|
||||
bounds.southWest.lon,
|
||||
bounds.southWest.lat,
|
||||
bounds.northEast.lon,
|
||||
bounds.northEast.lat
|
||||
bounds.northEast.lat,
|
||||
],
|
||||
{
|
||||
padding: 80,
|
||||
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;
|
||||
}
|
||||
|
||||
@ -257,7 +261,7 @@
|
||||
options.elevation.hr ? 'hr' : null,
|
||||
options.elevation.cad ? 'cad' : null,
|
||||
options.elevation.temp ? 'temp' : null,
|
||||
options.elevation.power ? 'power' : null
|
||||
options.elevation.power ? 'power' : null,
|
||||
].filter((dataset) => dataset !== null)}
|
||||
elevationFill={options.elevation.fill}
|
||||
showControls={options.elevation.controls}
|
||||
|
||||
@ -39,14 +39,14 @@ export const defaultEmbeddingOptions = {
|
||||
hr: false,
|
||||
cad: false,
|
||||
temp: false,
|
||||
power: false
|
||||
power: false,
|
||||
},
|
||||
distanceMarkers: false,
|
||||
directionMarkers: false,
|
||||
distanceUnits: 'metric',
|
||||
velocityUnits: 'speed',
|
||||
temperatureUnits: 'celsius',
|
||||
theme: 'system'
|
||||
theme: 'system',
|
||||
};
|
||||
|
||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
||||
@ -59,7 +59,11 @@ export function getMergedEmbeddingOptions(
|
||||
): EmbeddingOptions {
|
||||
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||
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]);
|
||||
} else {
|
||||
mergedOptions[key] = options[key];
|
||||
@ -79,7 +83,10 @@ export function getCleanedEmbeddingOptions(
|
||||
cleanedOptions[key] !== null &&
|
||||
!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) {
|
||||
delete cleanedOptions[key];
|
||||
}
|
||||
@ -141,7 +148,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
||||
}
|
||||
if (options.has('slope')) {
|
||||
newOptions.elevation = {
|
||||
fill: 'slope'
|
||||
fill: 'slope',
|
||||
};
|
||||
}
|
||||
return newOptions;
|
||||
|
||||
@ -13,13 +13,13 @@
|
||||
SquareActivity,
|
||||
Coins,
|
||||
Milestone,
|
||||
Video
|
||||
Video,
|
||||
} from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import {
|
||||
allowedEmbeddingBasemaps,
|
||||
getCleanedEmbeddingOptions,
|
||||
getDefaultEmbeddingOptions
|
||||
getDefaultEmbeddingOptions,
|
||||
} from './Embedding';
|
||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||
import Embedding from './Embedding.svelte';
|
||||
@ -30,7 +30,7 @@
|
||||
let options = getDefaultEmbeddingOptions();
|
||||
options.token = 'YOUR_MAPBOX_TOKEN';
|
||||
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];
|
||||
@ -130,7 +130,11 @@
|
||||
<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">
|
||||
{$_('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>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span class="shrink-0">
|
||||
@ -142,7 +146,11 @@
|
||||
let value = selected?.value;
|
||||
if (value === 'none') {
|
||||
options.elevation.fill = undefined;
|
||||
} else if (value === 'slope' || value === 'surface' || value === 'highway') {
|
||||
} else if (
|
||||
value === 'slope' ||
|
||||
value === 'surface' ||
|
||||
value === 'highway'
|
||||
) {
|
||||
options.elevation.fill = value;
|
||||
}
|
||||
}}
|
||||
@ -152,8 +160,10 @@
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
||||
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
|
||||
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item>
|
||||
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item
|
||||
>
|
||||
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item
|
||||
>
|
||||
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@ -318,7 +328,8 @@
|
||||
<Label>
|
||||
{$_('embedding.code')}
|
||||
</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">
|
||||
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
||||
</code>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { dbUtils, getFile } from "$lib/db";
|
||||
import { freeze } from "immer";
|
||||
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
||||
import { selection } from "./Selection";
|
||||
import { newGPXFile } from "$lib/stores";
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
import { freeze } from 'immer';
|
||||
import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
|
||||
import { selection } from './Selection';
|
||||
import { newGPXFile } from '$lib/stores';
|
||||
|
||||
export enum ListLevel {
|
||||
ROOT,
|
||||
@ -10,7 +10,7 @@ export enum ListLevel {
|
||||
TRACK,
|
||||
SEGMENT,
|
||||
WAYPOINTS,
|
||||
WAYPOINT
|
||||
WAYPOINT,
|
||||
}
|
||||
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
@ -19,7 +19,7 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||
};
|
||||
|
||||
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.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||
[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 {
|
||||
@ -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) {
|
||||
return;
|
||||
}
|
||||
@ -338,11 +344,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
context.push(file.clone());
|
||||
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||
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());
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -359,7 +372,12 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
if (item instanceof ListTrackItem) {
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
||||
} 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) {
|
||||
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
||||
} else if (item instanceof ListWaypointItem) {
|
||||
@ -371,25 +389,43 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
toItems.forEach((item, i) => {
|
||||
if (item instanceof ListTrackItem) {
|
||||
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) {
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
|
||||
trkseg: [context[i]]
|
||||
})]);
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
|
||||
new Track({
|
||||
trkseg: [context[i]],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
|
||||
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
|
||||
} else if (
|
||||
item instanceof ListTrackSegmentItem &&
|
||||
context[i] instanceof TrackSegment
|
||||
) {
|
||||
file.replaceTrackSegments(
|
||||
item.getTrackIndex(),
|
||||
item.getSegmentIndex(),
|
||||
item.getSegmentIndex() - 1,
|
||||
[context[i]]
|
||||
);
|
||||
} 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]);
|
||||
} else if (context[i] instanceof Waypoint) {
|
||||
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
|
||||
}
|
||||
} 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) {
|
||||
@ -400,7 +436,10 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
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) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
if (context[i] instanceof GPXFile) {
|
||||
@ -421,14 +460,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
} else if (context[i] instanceof TrackSegment) {
|
||||
let newFile = newGPXFile();
|
||||
newFile._data.id = item.getFileId();
|
||||
newFile.replaceTracks(0, 0, [new Track({
|
||||
trkseg: [context[i]]
|
||||
})]);
|
||||
newFile.replaceTracks(0, 0, [
|
||||
new Track({
|
||||
trkseg: [context[i]],
|
||||
}),
|
||||
]);
|
||||
files.set(item.getFileId(), freeze(newFile));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, context);
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
TrackSegment,
|
||||
Waypoint,
|
||||
type AnyGPXTreeElement,
|
||||
type GPXTreeElement
|
||||
type GPXTreeElement,
|
||||
} from 'gpx';
|
||||
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||
import { settings, type GPXFileWithStatistics } from '$lib/db';
|
||||
@ -19,7 +19,7 @@
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
type ListItem,
|
||||
type ListTrackItem
|
||||
type ListTrackItem,
|
||||
} from './FileList';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { selection } from './Selection';
|
||||
@ -43,7 +43,8 @@
|
||||
: node instanceof TrackSegment
|
||||
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||
: 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
|
||||
? $_('gpx.waypoints')
|
||||
: '';
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
ListWaypointsItem,
|
||||
allowedMoves,
|
||||
moveItems,
|
||||
type ListItem
|
||||
type ListItem,
|
||||
} from './FileList';
|
||||
import { selection } from './Selection';
|
||||
import { isMac } from '$lib/utils';
|
||||
@ -113,7 +113,7 @@
|
||||
Sortable.utils.select(element);
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest'
|
||||
block: 'nearest',
|
||||
});
|
||||
} else {
|
||||
Sortable.utils.deselect(element);
|
||||
@ -155,7 +155,7 @@
|
||||
group: {
|
||||
name: sortableLevel,
|
||||
pull: allowedMoves[sortableLevel],
|
||||
put: true
|
||||
put: true,
|
||||
},
|
||||
direction: orientation,
|
||||
forceAutoScrollFallback: true,
|
||||
@ -233,16 +233,16 @@
|
||||
|
||||
moveItems(fromItem, toItem, fromItems, toItems);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
Object.defineProperty(sortable, '_item', {
|
||||
value: item,
|
||||
writable: true
|
||||
writable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(sortable, '_waypointRoot', {
|
||||
value: waypointRoot,
|
||||
writable: true
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
Maximize,
|
||||
Scissors,
|
||||
FileStack,
|
||||
FileX
|
||||
FileX,
|
||||
} from 'lucide-svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
@ -26,7 +26,7 @@
|
||||
ListTrackItem,
|
||||
ListWaypointItem,
|
||||
allowedPastes,
|
||||
type ListItem
|
||||
type ListItem,
|
||||
} from './FileList';
|
||||
import {
|
||||
copied,
|
||||
@ -36,7 +36,7 @@
|
||||
pasteSelection,
|
||||
selectAll,
|
||||
selectItem,
|
||||
selection
|
||||
selection,
|
||||
} from './Selection';
|
||||
import { getContext } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
@ -47,7 +47,7 @@
|
||||
embedding,
|
||||
centerMapOnSelection,
|
||||
gpxLayers,
|
||||
map
|
||||
map,
|
||||
} from '$lib/stores';
|
||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||
import { _ } from 'svelte-i18n';
|
||||
@ -177,7 +177,10 @@
|
||||
if (layer && file) {
|
||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||
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" />
|
||||
{:else if item.level === ListLevel.WAYPOINT}
|
||||
{#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}
|
||||
<MapPin size="16" class="mr-1 shrink-0" />
|
||||
{/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}
|
||||
</span>
|
||||
{#if hidden}
|
||||
<EyeOff
|
||||
size="12"
|
||||
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
|
||||
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
|
||||
class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
|
||||
? 'mr-2'
|
||||
: ''} {item.level === ListLevel.SEGMENT ||
|
||||
item.level === ListLevel.WAYPOINT
|
||||
? 'mr-3'
|
||||
: ''}"
|
||||
/>
|
||||
|
||||
@ -17,15 +17,15 @@
|
||||
|
||||
let name: string =
|
||||
node instanceof GPXFile
|
||||
? node.metadata.name ?? ''
|
||||
? (node.metadata.name ?? '')
|
||||
: node instanceof Track
|
||||
? node.name ?? ''
|
||||
? (node.name ?? '')
|
||||
: '';
|
||||
let description: string =
|
||||
node instanceof GPXFile
|
||||
? node.metadata.desc ?? ''
|
||||
? (node.metadata.desc ?? '')
|
||||
: node instanceof Track
|
||||
? node.desc ?? ''
|
||||
? (node.desc ?? '')
|
||||
: '';
|
||||
|
||||
$: if (!open) {
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
|
||||
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
|
||||
import { get, writable } from 'svelte/store';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListItem,
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListLevel,
|
||||
sortItems,
|
||||
ListWaypointsItem,
|
||||
moveItems,
|
||||
} from './FileList';
|
||||
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
|
||||
|
||||
export class SelectionTreeType {
|
||||
item: ListItem;
|
||||
selected: boolean;
|
||||
children: {
|
||||
[key: string | number]: SelectionTreeType
|
||||
[key: string | number]: SelectionTreeType;
|
||||
};
|
||||
size: number = 0;
|
||||
|
||||
@ -67,7 +78,11 @@ export class SelectionTreeType {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
@ -80,7 +95,11 @@ export class SelectionTreeType {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
@ -131,7 +150,7 @@ export class SelectionTreeType {
|
||||
delete this.children[id];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
|
||||
|
||||
@ -181,7 +200,10 @@ export function selectAll() {
|
||||
let file = getFile(item.getFileId());
|
||||
if (file) {
|
||||
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) {
|
||||
@ -205,14 +227,24 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
|
||||
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) => {
|
||||
let level: ListLevel | undefined = undefined;
|
||||
let items: ListItem[] = [];
|
||||
selectedItems.forEach((item) => {
|
||||
if (item.getFileId() === fileId) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -270,7 +305,11 @@ export function pasteSelection() {
|
||||
let startIndex: number | undefined = undefined;
|
||||
|
||||
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;
|
||||
}
|
||||
toParent = toParent.getParent();
|
||||
@ -288,20 +327,41 @@ export function pasteSelection() {
|
||||
fromItems.forEach((item, index) => {
|
||||
if (toParent instanceof ListFileItem) {
|
||||
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) {
|
||||
toItems.push(new ListWaypointsItem(toParent.getFileId()));
|
||||
} 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) {
|
||||
if (item instanceof ListTrackSegmentItem) {
|
||||
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) {
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -60,10 +60,16 @@
|
||||
let track = file.trk[item.getTrackIndex()];
|
||||
let style = track.getStyle();
|
||||
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']);
|
||||
}
|
||||
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']);
|
||||
}
|
||||
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
|
||||
import { settings } from "$lib/db";
|
||||
import { gpxStatistics } from "$lib/stores";
|
||||
import { get } from "svelte/store";
|
||||
import { settings } from '$lib/db';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
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 {
|
||||
map: mapboxgl.Map;
|
||||
@ -30,7 +36,7 @@ export class DistanceMarkers {
|
||||
} else {
|
||||
this.map.addSource('distance-markers', {
|
||||
type: 'geojson',
|
||||
data: this.getDistanceMarkersGeoJSON()
|
||||
data: this.getDistanceMarkersGeoJSON(),
|
||||
});
|
||||
}
|
||||
stops.forEach(([d, minzoom, maxzoom]) => {
|
||||
@ -39,7 +45,14 @@ export class DistanceMarkers {
|
||||
id: `distance-markers-${d}`,
|
||||
type: 'symbol',
|
||||
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,
|
||||
maxzoom: maxzoom ?? 24,
|
||||
layout: {
|
||||
@ -51,7 +64,7 @@ export class DistanceMarkers {
|
||||
'text-color': 'black',
|
||||
'text-halo-width': 2,
|
||||
'text-halo-color': 'white',
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
}
|
||||
|
||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||
@ -79,20 +93,28 @@ export class DistanceMarkers {
|
||||
let features = [];
|
||||
let currentTargetDistance = 1;
|
||||
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 [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [0, 0];
|
||||
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
||||
0, 0,
|
||||
];
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
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: {
|
||||
distance,
|
||||
level,
|
||||
minzoom,
|
||||
}
|
||||
},
|
||||
} as GeoJSON.Feature);
|
||||
currentTargetDistance += 1;
|
||||
}
|
||||
@ -100,7 +122,7 @@ export class DistanceMarkers {
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
features,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,28 @@
|
||||
import { currentTool, map, Tool } from "$lib/stores";
|
||||
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
|
||||
import { get, type Readable } from "svelte/store";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { waypointPopup, deleteWaypoint, trackpointPopup } from "./GPXLayerPopup";
|
||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
|
||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, 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";
|
||||
import { currentTool, map, Tool } from '$lib/stores';
|
||||
import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
|
||||
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
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 = [
|
||||
'#ff0000',
|
||||
@ -21,7 +35,7 @@ const colors = [
|
||||
'#288228',
|
||||
'#9933ff',
|
||||
'#50f0be',
|
||||
'#8c645a'
|
||||
'#8c645a',
|
||||
];
|
||||
|
||||
const colorCount: { [key: string]: number } = {};
|
||||
@ -56,12 +70,12 @@ class KeyDown {
|
||||
if (e.key === this.key) {
|
||||
this.down = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === this.key) {
|
||||
this.down = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
isDown() {
|
||||
return this.down;
|
||||
}
|
||||
@ -70,22 +84,26 @@ class KeyDown {
|
||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
${Square
|
||||
.replace('width="24"', 'width="12"')
|
||||
${Square.replace('width="24"', 'width="12"')
|
||||
.replace('height="24"', 'height="12"')
|
||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||
${MapPin
|
||||
.replace('width="24"', '')
|
||||
${MapPin.replace('width="24"', '')
|
||||
.replace('height="24"', '')
|
||||
.replace('stroke="currentColor"', '')
|
||||
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
||||
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
|
||||
${symbolSvg?.replace('width="24"', 'width="10"')
|
||||
.replace(
|
||||
'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('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>`;
|
||||
}
|
||||
|
||||
@ -108,13 +126,18 @@ export class GPXLayer {
|
||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.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.fileId = fileId;
|
||||
this.file = file;
|
||||
this.layerColor = getColor();
|
||||
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));
|
||||
if (this.selected || newSelected) {
|
||||
this.selected = newSelected;
|
||||
@ -123,17 +146,20 @@ export class GPXLayer {
|
||||
if (newSelected) {
|
||||
this.moveToFront();
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
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) {
|
||||
this.draggable = true;
|
||||
this.markers.forEach(marker => marker.setDraggable(true));
|
||||
this.markers.forEach((marker) => marker.setDraggable(true));
|
||||
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||
this.draggable = false;
|
||||
this.markers.forEach(marker => marker.setDraggable(false));
|
||||
this.markers.forEach((marker) => marker.setDraggable(false));
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||
|
||||
this.map.on('style.import.load', this.updateBinded);
|
||||
@ -149,7 +175,11 @@ export class GPXLayer {
|
||||
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);
|
||||
this.layerColor = `#${file._data.style.color}`;
|
||||
}
|
||||
@ -161,7 +191,7 @@ export class GPXLayer {
|
||||
} else {
|
||||
this.map.addSource(this.fileId, {
|
||||
type: 'geojson',
|
||||
data: this.getGeoJSON()
|
||||
data: this.getGeoJSON(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -172,13 +202,13 @@ export class GPXLayer {
|
||||
source: this.fileId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
'line-cap': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': ['get', 'color'],
|
||||
'line-width': ['get', 'width'],
|
||||
'line-opacity': ['get', 'opacity']
|
||||
}
|
||||
'line-opacity': ['get', 'opacity'],
|
||||
},
|
||||
});
|
||||
|
||||
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
||||
@ -190,7 +220,8 @@ export class GPXLayer {
|
||||
|
||||
if (get(directionMarkers)) {
|
||||
if (!this.map.getLayer(this.fileId + '-direction')) {
|
||||
this.map.addLayer({
|
||||
this.map.addLayer(
|
||||
{
|
||||
id: this.fileId + '-direction',
|
||||
type: 'symbol',
|
||||
source: this.fileId,
|
||||
@ -208,9 +239,11 @@ export class GPXLayer {
|
||||
'text-color': 'white',
|
||||
'text-opacity': 0.7,
|
||||
'text-halo-width': 0.2,
|
||||
'text-halo-color': 'white'
|
||||
}
|
||||
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
},
|
||||
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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')) {
|
||||
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;
|
||||
}
|
||||
|
||||
let markerIndex = 0;
|
||||
|
||||
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);
|
||||
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());
|
||||
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
|
||||
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
|
||||
value: waypoint,
|
||||
writable: true,
|
||||
});
|
||||
} else {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||
@ -249,7 +312,7 @@ export class GPXLayer {
|
||||
let marker = new mapboxgl.Marker({
|
||||
draggable: this.draggable,
|
||||
element,
|
||||
anchor: 'bottom'
|
||||
anchor: 'bottom',
|
||||
}).setLngLat(waypoint.getCoordinates());
|
||||
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||
let dragEndTimestamp = 0;
|
||||
@ -272,10 +335,20 @@ export class GPXLayer {
|
||||
}
|
||||
|
||||
if (get(treeFileView)) {
|
||||
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
|
||||
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
||||
if (
|
||||
(e.ctrlKey || e.metaKey) &&
|
||||
get(selection).hasAnyChildren(
|
||||
new ListWaypointsItem(this.fileId),
|
||||
false
|
||||
)
|
||||
) {
|
||||
addSelectItem(
|
||||
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||
);
|
||||
} 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) {
|
||||
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||
@ -298,12 +371,12 @@ export class GPXLayer {
|
||||
let wpt = file.wpt[marker._waypoint._data.index];
|
||||
wpt.setCoordinates({
|
||||
lat: latLng.lat,
|
||||
lon: latLng.lng
|
||||
lon: latLng.lng,
|
||||
});
|
||||
wpt.ele = ele[0];
|
||||
});
|
||||
});
|
||||
dragEndTimestamp = Date.now()
|
||||
dragEndTimestamp = Date.now();
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
@ -364,7 +438,10 @@ export class GPXLayer {
|
||||
this.map.moveLayer(this.fileId);
|
||||
}
|
||||
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 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();
|
||||
} else {
|
||||
setPointerCursor();
|
||||
@ -390,22 +472,36 @@ export class GPXLayer {
|
||||
|
||||
const file = get(this.file)?.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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let trackIndex = e.features[0].properties.trackIndex;
|
||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||
|
||||
if (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 });
|
||||
if (
|
||||
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;
|
||||
}
|
||||
|
||||
@ -415,8 +511,12 @@ export class GPXLayer {
|
||||
}
|
||||
|
||||
let item = undefined;
|
||||
if (get(treeFileView) && file.getSegments().length > 1) { // Select inner item
|
||||
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
||||
if (get(treeFileView) && file.getSegments().length > 1) {
|
||||
// Select inner item
|
||||
item =
|
||||
file.children[trackIndex].children.length > 1
|
||||
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||
: new ListTrackItem(this.fileId, trackIndex);
|
||||
} else {
|
||||
item = new ListFileItem(this.fileId);
|
||||
}
|
||||
@ -439,13 +539,14 @@ export class GPXLayer {
|
||||
if (!file) {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: []
|
||||
features: [],
|
||||
};
|
||||
}
|
||||
|
||||
let data = file.toGeoJSON();
|
||||
|
||||
let trackIndex = 0, segmentIndex = 0;
|
||||
let trackIndex = 0,
|
||||
segmentIndex = 0;
|
||||
for (let feature of data.features) {
|
||||
if (!feature.properties) {
|
||||
feature.properties = {};
|
||||
@ -459,7 +560,12 @@ export class GPXLayer {
|
||||
if (!feature.properties.width) {
|
||||
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.opacity = Math.min(1, feature.properties.opacity + 0.1);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { dbUtils } from "$lib/db";
|
||||
import { MapPopup } from "$lib/components/MapPopup";
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { MapPopup } from '$lib/components/MapPopup';
|
||||
|
||||
export let waypointPopup: MapPopup | null = null;
|
||||
export let trackpointPopup: MapPopup | null = null;
|
||||
@ -11,14 +11,14 @@ export function createPopups(map: mapboxgl.Map) {
|
||||
focusAfterOpen: false,
|
||||
maxWidth: undefined,
|
||||
offset: {
|
||||
'top': [0, 0],
|
||||
top: [0, 0],
|
||||
'top-left': [0, 0],
|
||||
'top-right': [0, 0],
|
||||
'bottom': [0, -30],
|
||||
bottom: [0, -30],
|
||||
'bottom-left': [0, -30],
|
||||
'bottom-right': [0, -30],
|
||||
'left': [10, -15],
|
||||
'right': [-10, -15],
|
||||
left: [10, -15],
|
||||
right: [-10, -15],
|
||||
},
|
||||
});
|
||||
trackpointPopup = new MapPopup(map, {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { get } from "svelte/store";
|
||||
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export class StartEndMarkers {
|
||||
map: mapboxgl.Map;
|
||||
@ -16,7 +16,8 @@ export class StartEndMarkers {
|
||||
let endElement = document.createElement('div');
|
||||
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.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.end = new mapboxgl.Marker({ element: endElement });
|
||||
@ -31,7 +32,11 @@ export class StartEndMarkers {
|
||||
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
||||
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
|
||||
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 {
|
||||
this.start.remove();
|
||||
this.end.remove();
|
||||
@ -39,7 +44,7 @@ export class StartEndMarkers {
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
this.start.remove();
|
||||
this.end.remove();
|
||||
|
||||
@ -25,8 +25,8 @@
|
||||
allowedTags: ['a', 'br', 'img'],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'target'],
|
||||
img: ['src']
|
||||
}
|
||||
img: ['src'],
|
||||
},
|
||||
}).trim();
|
||||
}
|
||||
</script>
|
||||
@ -61,7 +61,9 @@
|
||||
</span>
|
||||
<Dot size="16" />
|
||||
{/if}
|
||||
{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item.getLongitude().toFixed(6)}°
|
||||
{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item
|
||||
.getLongitude()
|
||||
.toFixed(6)}°
|
||||
{#if waypoint.item.ele !== undefined}
|
||||
<Dot size="16" />
|
||||
<WithUnits value={waypoint.item.ele} type="elevation" />
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
Trash2,
|
||||
Move,
|
||||
Map,
|
||||
Layers2
|
||||
Layers2,
|
||||
} from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { settings } from '$lib/db';
|
||||
@ -34,7 +34,7 @@
|
||||
currentOverlays,
|
||||
previousOverlays,
|
||||
customBasemapOrder,
|
||||
customOverlayOrder
|
||||
customOverlayOrder,
|
||||
} = settings;
|
||||
|
||||
let name: string = '';
|
||||
@ -68,7 +68,7 @@
|
||||
acc[id] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
});
|
||||
overlaySortable = Sortable.create(overlayContainer, {
|
||||
onSort: (e) => {
|
||||
@ -77,7 +77,7 @@
|
||||
acc[id] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
basemapSortable.sort($customBasemapOrder);
|
||||
@ -118,7 +118,7 @@
|
||||
maxZoom: maxZoom,
|
||||
layerType: layerType,
|
||||
resourceType: resourceType,
|
||||
value: ''
|
||||
value: '',
|
||||
};
|
||||
|
||||
if (resourceType === 'vector') {
|
||||
@ -131,16 +131,16 @@
|
||||
type: 'raster',
|
||||
tiles: layer.tileUrls,
|
||||
tileSize: is512 ? 512 : 256,
|
||||
maxzoom: maxZoom
|
||||
}
|
||||
maxzoom: maxZoom,
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: layerId,
|
||||
type: 'raster',
|
||||
source: layerId
|
||||
}
|
||||
]
|
||||
source: layerId,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
$customLayers[layerId] = layer;
|
||||
@ -230,7 +230,10 @@
|
||||
layerId
|
||||
);
|
||||
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);
|
||||
} else {
|
||||
@ -247,7 +250,10 @@
|
||||
layerId
|
||||
);
|
||||
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);
|
||||
|
||||
@ -367,7 +373,8 @@
|
||||
/>
|
||||
{#if tileUrls.length > 1}
|
||||
<Button
|
||||
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||
on:click={() =>
|
||||
(tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||
variant="outline"
|
||||
class="p-1 h-8"
|
||||
>
|
||||
@ -387,7 +394,14 @@
|
||||
{/each}
|
||||
{#if resourceType === 'raster'}
|
||||
<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}
|
||||
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
||||
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
selectedOverlayTree,
|
||||
selectedOverpassTree,
|
||||
customLayers,
|
||||
opacities
|
||||
opacities,
|
||||
} = settings;
|
||||
|
||||
function setStyle() {
|
||||
@ -41,7 +41,7 @@
|
||||
$map.addImport(
|
||||
{
|
||||
id: 'basemap',
|
||||
data: basemap
|
||||
data: basemap,
|
||||
},
|
||||
'overlays'
|
||||
);
|
||||
@ -70,12 +70,12 @@
|
||||
layer.paint['raster-opacity'] = $opacities[id];
|
||||
}
|
||||
return layer;
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
$map.addImport({
|
||||
id,
|
||||
data: overlay
|
||||
data: overlay,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
defaultBasemap,
|
||||
overlays,
|
||||
overlayTree,
|
||||
overpassTree
|
||||
overpassTree,
|
||||
} from '$lib/assets/layers';
|
||||
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
|
||||
import { settings } from '$lib/db';
|
||||
@ -31,7 +31,7 @@
|
||||
currentBasemap,
|
||||
currentOverlays,
|
||||
customLayers,
|
||||
opacities
|
||||
opacities,
|
||||
} = settings;
|
||||
|
||||
export let open: boolean;
|
||||
@ -137,7 +137,9 @@
|
||||
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
|
||||
{#each Object.keys(overlays) as id}
|
||||
{#if isSelected($selectedOverlayTree, id)}
|
||||
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item>
|
||||
<Select.Item value={id}
|
||||
>{$_(`layers.label.${id}`)}</Select.Item
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each Object.entries($customLayers) as [id, layer]}
|
||||
@ -159,7 +161,13 @@
|
||||
disabled={$selectedOverlay === undefined}
|
||||
onValueChange={(value) => {
|
||||
if ($selectedOverlay) {
|
||||
if ($map && isSelected($currentOverlays, $selectedOverlay.value)) {
|
||||
if (
|
||||
$map &&
|
||||
isSelected(
|
||||
$currentOverlays,
|
||||
$selectedOverlay.value
|
||||
)
|
||||
) {
|
||||
try {
|
||||
$map.removeImport($selectedOverlay.value);
|
||||
} catch (e) {
|
||||
|
||||
@ -49,7 +49,13 @@
|
||||
aria-label={$_(`layers.label.${id}`)}
|
||||
/>
|
||||
{: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}
|
||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||
{#if $customLayers.hasOwnProperty(id)}
|
||||
@ -64,7 +70,13 @@
|
||||
<CollapsibleTreeNode {id}>
|
||||
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
|
||||
<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>
|
||||
</CollapsibleTreeNode>
|
||||
{/if}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import SphericalMercator from "@mapbox/sphericalmercator";
|
||||
import { getLayers } from "./utils";
|
||||
import { get, writable } from "svelte/store";
|
||||
import { liveQuery } from "dexie";
|
||||
import { db, settings } from "$lib/db";
|
||||
import { overpassQueryData } from "$lib/assets/layers";
|
||||
import { MapPopup } from "$lib/components/MapPopup";
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
import { getLayers } from './utils';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db, settings } from '$lib/db';
|
||||
import { overpassQueryData } from '$lib/assets/layers';
|
||||
import { MapPopup } from '$lib/components/MapPopup';
|
||||
|
||||
const {
|
||||
currentOverpassQueries
|
||||
} = settings;
|
||||
const { currentOverpassQueries } = settings;
|
||||
|
||||
const mercator = new SphericalMercator({
|
||||
size: 256,
|
||||
@ -29,7 +27,7 @@ export class OverpassLayer {
|
||||
popup: MapPopup;
|
||||
|
||||
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)[] = [];
|
||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||
@ -50,10 +48,12 @@ export class OverpassLayer {
|
||||
this.map.on('moveend', this.queryIfNeededBinded);
|
||||
this.map.on('style.import.load', this.updateBinded);
|
||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
||||
this.unsubscribes.push(
|
||||
currentOverpassQueries.subscribe(() => {
|
||||
this.updateBinded();
|
||||
this.queryIfNeededBinded();
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
this.update();
|
||||
}
|
||||
@ -126,8 +126,8 @@ export class OverpassLayer {
|
||||
this.popup.setItem({
|
||||
item: {
|
||||
...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;
|
||||
}
|
||||
|
||||
db.overpasstiles.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));
|
||||
db.overpasstiles
|
||||
.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) {
|
||||
this.queryTile(x, y, missingQueries);
|
||||
}
|
||||
@ -165,13 +176,16 @@ export class OverpassLayer {
|
||||
|
||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
||||
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
||||
.then((response) => {
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
this.currentQueries.delete(`${x},${y}`);
|
||||
return Promise.reject();
|
||||
}, () => (this.currentQueries.delete(`${x},${y}`)))
|
||||
},
|
||||
() => this.currentQueries.delete(`${x},${y}`)
|
||||
)
|
||||
.then((data) => this.storeOverpassData(x, y, queries, data))
|
||||
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
||||
}
|
||||
@ -179,7 +193,7 @@ export class OverpassLayer {
|
||||
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
||||
let time = Date.now();
|
||||
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) {
|
||||
return;
|
||||
@ -195,7 +209,9 @@ export class OverpassLayer {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
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: {
|
||||
id: element.id,
|
||||
@ -203,9 +219,9 @@ export class OverpassLayer {
|
||||
lon: element.center ? element.center.lon : element.lon,
|
||||
query: query,
|
||||
icon: `overpass-${query}`,
|
||||
tags: element.tags
|
||||
tags: element.tags,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -228,11 +244,13 @@ export class OverpassLayer {
|
||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||
this.map.addImage(`overpass-${query}`, icon);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Lucide icons are SVG files with a 24x24 viewBox
|
||||
// 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">
|
||||
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||
<g transform="translate(8 8)">
|
||||
@ -264,9 +282,14 @@ function getQuery(query: string) {
|
||||
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
||||
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
||||
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}]`)
|
||||
.join('')};`).join('');
|
||||
.join('')};`
|
||||
)
|
||||
.join('');
|
||||
} else {
|
||||
return `nwr${Object.entries(tags)
|
||||
.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[]>) {
|
||||
return Object.entries(tags)
|
||||
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value);
|
||||
return Object.entries(tags).every(([tag, value]) =>
|
||||
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
||||
);
|
||||
}
|
||||
|
||||
function getCurrentQueries() {
|
||||
@ -293,5 +317,7 @@ function getCurrentQueries() {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query);
|
||||
return Object.entries(getLayers(currentQueries))
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([query, _]) => query);
|
||||
}
|
||||
@ -79,12 +79,12 @@
|
||||
dbUtils.addOrUpdateWaypoint({
|
||||
attributes: {
|
||||
lat: poi.item.lat,
|
||||
lon: poi.item.lon
|
||||
lon: poi.item.lon,
|
||||
},
|
||||
name: name,
|
||||
desc: desc,
|
||||
cmt: desc,
|
||||
sym: poi.item.sym
|
||||
sym: poi.item.sym,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { LayerTreeType } from "$lib/assets/layers";
|
||||
import { writable } from "svelte/store";
|
||||
import type { LayerTreeType } from '$lib/assets/layers';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function anySelectedLayer(node: LayerTreeType) {
|
||||
return Object.keys(node).find((id) => {
|
||||
if (typeof node[id] == "boolean") {
|
||||
return (
|
||||
Object.keys(node).find((id) => {
|
||||
if (typeof node[id] == 'boolean') {
|
||||
if (node[id]) {
|
||||
return true;
|
||||
}
|
||||
@ -13,12 +14,16 @@ export function anySelectedLayer(node: LayerTreeType) {
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
if (typeof node[id] == "boolean") {
|
||||
if (typeof node[id] == 'boolean') {
|
||||
layers[id] = node[id];
|
||||
} else {
|
||||
getLayers(node[id], layers);
|
||||
@ -32,7 +37,7 @@ export function isSelected(node: LayerTreeType, id: string) {
|
||||
if (key === id) {
|
||||
return node[key];
|
||||
}
|
||||
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) {
|
||||
if (typeof node[key] !== 'boolean' && isSelected(node[key], id)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -43,7 +48,7 @@ export function toggle(node: LayerTreeType, id: string) {
|
||||
Object.keys(node).forEach((key) => {
|
||||
if (key === id) {
|
||||
node[key] = !node[key];
|
||||
} else if (typeof node[key] !== "boolean") {
|
||||
} else if (typeof node[key] !== 'boolean') {
|
||||
toggle(node[key], id);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { resetCursor, setCrosshairCursor } from "$lib/utils";
|
||||
import type mapboxgl from "mapbox-gl";
|
||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import type mapboxgl from 'mapbox-gl';
|
||||
|
||||
export class GoogleRedirect {
|
||||
map: mapboxgl.Map;
|
||||
|
||||
@ -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 'mapillary-js/dist/mapillary.css';
|
||||
import { resetCursor, setPointerCursor } from "$lib/utils";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { resetCursor, setPointerCursor } from '$lib/utils';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
const mapillarySource: VectorSourceSpecification = {
|
||||
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,
|
||||
maxzoom: 14,
|
||||
};
|
||||
@ -70,7 +72,7 @@ export class MapillaryLayer {
|
||||
|
||||
this.marker = new mapboxgl.Marker({
|
||||
rotationAlignment: 'map',
|
||||
element
|
||||
element,
|
||||
});
|
||||
|
||||
this.viewer.on('position', async () => {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
MapPin,
|
||||
Filter,
|
||||
Scissors,
|
||||
MountainSnow
|
||||
MountainSnow,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
onMount(() => {
|
||||
popup = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
maxWidth: undefined
|
||||
maxWidth: undefined,
|
||||
});
|
||||
popup.setDOMContent(popupElement);
|
||||
popupElement.classList.remove('hidden');
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
enum CleanType {
|
||||
INSIDE = 'inside',
|
||||
OUTSIDE = 'outside'
|
||||
OUTSIDE = 'outside',
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -41,10 +41,10 @@
|
||||
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
|
||||
[rectangleCoordinates[1].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');
|
||||
if (source) {
|
||||
@ -52,7 +52,7 @@
|
||||
} else {
|
||||
$map.addSource('rectangle', {
|
||||
type: 'geojson',
|
||||
data: data
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
if (!$map.getLayer('rectangle')) {
|
||||
@ -62,8 +62,8 @@
|
||||
source: 'rectangle',
|
||||
paint: {
|
||||
'fill-color': 'SteelBlue',
|
||||
'fill-opacity': 0.5
|
||||
}
|
||||
'fill-opacity': 0.5,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -161,12 +161,12 @@
|
||||
[
|
||||
{
|
||||
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),
|
||||
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
|
||||
}
|
||||
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
|
||||
},
|
||||
],
|
||||
cleanType === CleanType.INSIDE,
|
||||
deleteTrackpoints,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
enum MergeType {
|
||||
TRACES = 'traces',
|
||||
CONTENTS = 'contents'
|
||||
CONTENTS = 'contents',
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
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 { Filter } from 'lucide-svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
@ -35,7 +39,7 @@
|
||||
|
||||
let data: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
features: []
|
||||
features: [],
|
||||
};
|
||||
|
||||
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
||||
@ -52,10 +56,10 @@
|
||||
type: 'LineString',
|
||||
coordinates: current.map((point) => [
|
||||
point.point.getLongitude(),
|
||||
point.point.getLatitude()
|
||||
])
|
||||
point.point.getLatitude(),
|
||||
]),
|
||||
},
|
||||
properties: {}
|
||||
properties: {},
|
||||
});
|
||||
});
|
||||
|
||||
@ -66,7 +70,7 @@
|
||||
} else {
|
||||
$map.addSource('simplified', {
|
||||
type: 'geojson',
|
||||
data: data
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
if (!$map.getLayer('simplified')) {
|
||||
@ -76,8 +80,8 @@
|
||||
source: 'simplified',
|
||||
paint: {
|
||||
'line-color': 'white',
|
||||
'line-width': 3
|
||||
}
|
||||
'line-width': 3,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
$map.moveLayer('simplified');
|
||||
@ -94,17 +98,23 @@
|
||||
});
|
||||
$fileObservers.forEach((fileStore, fileId) => {
|
||||
if (!unsubscribes.has(fileId)) {
|
||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
|
||||
([fs, sel]) => {
|
||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
|
||||
fs,
|
||||
sel,
|
||||
]).subscribe(([fs, sel]) => {
|
||||
if (fs) {
|
||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
|
||||
let segmentItem = new ListTrackSegmentItem(
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
);
|
||||
if (sel.hasAnyParent(segmentItem)) {
|
||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||
simplified.set(segmentItem.getFullId(), [
|
||||
segmentItem,
|
||||
statistics.local.points.length,
|
||||
ramerDouglasPeucker(statistics.local.points, minTolerance)
|
||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||
]);
|
||||
update();
|
||||
} else if (simplified.has(segmentItem.getFullId())) {
|
||||
@ -113,8 +123,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
unsubscribes.set(fileId, unsubscribe);
|
||||
}
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
distancePerHourToSecondsPerDistance,
|
||||
getConvertedVelocity,
|
||||
milesToKilometers,
|
||||
nauticalMilesToKilometers
|
||||
nauticalMilesToKilometers,
|
||||
} from '$lib/units';
|
||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
||||
@ -23,7 +23,7 @@
|
||||
ListFileItem,
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem
|
||||
ListTrackSegmentItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
@ -305,7 +305,11 @@
|
||||
class="grow whitespace-normal h-fit"
|
||||
on:click={() => {
|
||||
let effectiveSpeed = getSpeed();
|
||||
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
||||
if (
|
||||
startDate === undefined ||
|
||||
startTime === undefined ||
|
||||
effectiveSpeed === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -326,9 +330,16 @@
|
||||
dbUtils.applyToFile(fileId, (file) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
if (artificial) {
|
||||
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
|
||||
file.createArtificialTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
movingTime
|
||||
);
|
||||
} else {
|
||||
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||
file.changeTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
effectiveSpeed,
|
||||
ratio
|
||||
);
|
||||
}
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
if (artificial) {
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
let selectedSymbol = {
|
||||
value: '',
|
||||
label: ''
|
||||
label: '',
|
||||
};
|
||||
|
||||
const { treeFileView } = settings;
|
||||
@ -74,12 +74,12 @@
|
||||
if (symbolKey) {
|
||||
selectedSymbol = {
|
||||
value: symbol,
|
||||
label: $_(`gpx.symbol.${symbolKey}`)
|
||||
label: $_(`gpx.symbol.${symbolKey}`),
|
||||
};
|
||||
} else {
|
||||
selectedSymbol = {
|
||||
value: symbol,
|
||||
label: ''
|
||||
label: '',
|
||||
};
|
||||
}
|
||||
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
||||
@ -99,7 +99,7 @@
|
||||
link = '';
|
||||
selectedSymbol = {
|
||||
value: '',
|
||||
label: ''
|
||||
label: '',
|
||||
};
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
@ -134,13 +134,13 @@
|
||||
{
|
||||
attributes: {
|
||||
lat: latitude,
|
||||
lon: longitude
|
||||
lon: longitude,
|
||||
},
|
||||
name: name.length > 0 ? name : undefined,
|
||||
desc: description.length > 0 ? description : undefined,
|
||||
cmt: description.length > 0 ? description : 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
|
||||
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
||||
@ -195,7 +195,11 @@
|
||||
/>
|
||||
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
||||
<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.Trigger>
|
||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||
@ -218,7 +222,12 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<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="grow">
|
||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
RouteOff,
|
||||
Repeat,
|
||||
SquareArrowUpLeft,
|
||||
SquareArrowOutDownRight
|
||||
SquareArrowOutDownRight,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
|
||||
@ -37,7 +37,7 @@
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
type ListItem
|
||||
type ListItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@ -68,7 +68,10 @@
|
||||
// add controls for new files
|
||||
$fileObservers.forEach((file, 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({
|
||||
attributes: {
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng
|
||||
}
|
||||
})
|
||||
lon: e.lngLat.lng,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
file._data.id = getFileIds(1)[0];
|
||||
dbUtils.add(file);
|
||||
@ -195,7 +198,8 @@
|
||||
if (selected[0] instanceof ListFileItem) {
|
||||
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
|
||||
} 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) {
|
||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
||||
selected[0].getSegmentIndex()
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Coordinates } from "gpx";
|
||||
import { TrackPoint, distance } from "gpx";
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import { settings } from "$lib/db";
|
||||
import { _, isLoading, locale } from "svelte-i18n";
|
||||
import { getElevation } from "$lib/utils";
|
||||
import type { Coordinates } from 'gpx';
|
||||
import { TrackPoint, distance } from 'gpx';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { settings } from '$lib/db';
|
||||
import { _, isLoading, locale } from 'svelte-i18n';
|
||||
import { getElevation } from '$lib/utils';
|
||||
|
||||
const { routing, routingProfile, privateRoads } = settings;
|
||||
|
||||
@ -15,22 +15,31 @@ export const brouterProfiles: { [key: string]: string } = {
|
||||
foot: 'Hiking-Alpine-SAC6',
|
||||
motorcycle: 'Car-FastEco',
|
||||
water: 'river',
|
||||
railway: 'rail'
|
||||
railway: 'rail',
|
||||
};
|
||||
export const routingProfileSelectItem = writable({
|
||||
value: '',
|
||||
label: ''
|
||||
label: '',
|
||||
});
|
||||
|
||||
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => {
|
||||
if (!i && profile !== '' && (profile !== get(routingProfileSelectItem).value || get(_)(`toolbar.routing.activities.${profile}`) !== get(routingProfileSelectItem).label) && l !== null) {
|
||||
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(
|
||||
([profile, l, i]) => {
|
||||
if (
|
||||
!i &&
|
||||
profile !== '' &&
|
||||
(profile !== get(routingProfileSelectItem).value ||
|
||||
get(_)(`toolbar.routing.activities.${profile}`) !==
|
||||
get(routingProfileSelectItem).label) &&
|
||||
l !== null
|
||||
) {
|
||||
routingProfileSelectItem.update((item) => {
|
||||
item.value = profile;
|
||||
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
routingProfileSelectItem.subscribe((item) => {
|
||||
if (item.value !== '' && item.value !== get(routingProfile)) {
|
||||
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[]> {
|
||||
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`;
|
||||
async function getRoute(
|
||||
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);
|
||||
|
||||
@ -61,25 +74,29 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
||||
let coordinates = geojson.features[0].geometry.coordinates;
|
||||
let messages = geojson.features[0].properties.messages;
|
||||
|
||||
const lngIdx = messages[0].indexOf("Longitude");
|
||||
const latIdx = messages[0].indexOf("Latitude");
|
||||
const tagIdx = messages[0].indexOf("WayTags");
|
||||
const lngIdx = messages[0].indexOf('Longitude');
|
||||
const latIdx = messages[0].indexOf('Latitude');
|
||||
const tagIdx = messages[0].indexOf('WayTags');
|
||||
let messageIdx = 1;
|
||||
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
||||
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
let coord = coordinates[i];
|
||||
route.push(new TrackPoint({
|
||||
route.push(
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
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][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
||||
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000
|
||||
) {
|
||||
messageIdx++;
|
||||
|
||||
if (messageIdx == messages.length) tags = {};
|
||||
@ -93,10 +110,10 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
||||
}
|
||||
|
||||
function getTags(message: string): { [key: string]: string } {
|
||||
const fields = message.split(" ");
|
||||
const fields = message.split(' ');
|
||||
let tags: { [key: string]: string } = {};
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let [key, value] = fields[i].split("=");
|
||||
let [key, value] = fields[i].split('=');
|
||||
key = key.replace(/:/g, '_');
|
||||
tags[key] = value;
|
||||
}
|
||||
@ -107,26 +124,31 @@ function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||
let route: TrackPoint[] = [];
|
||||
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;
|
||||
for (let d = 0; d < dist; d += step) {
|
||||
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);
|
||||
route.push(new TrackPoint({
|
||||
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);
|
||||
route.push(
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
lat: lat,
|
||||
lon: lon
|
||||
}
|
||||
}));
|
||||
lon: lon,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
route.push(new TrackPoint({
|
||||
route.push(
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
lat: points[points.length - 1].lat,
|
||||
lon: points[points.length - 1].lon
|
||||
}
|
||||
}));
|
||||
lon: points[points.length - 1].lon,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return getElevation(route).then((elevations) => {
|
||||
route.forEach((point, i) => {
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from "gpx";
|
||||
import { get, writable, type Readable } from "svelte/store";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { route } from "./Routing";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { dbUtils, settings, type GPXFileWithStatistics } from "$lib/db";
|
||||
import { getOrderedSelection, selection } from "$lib/components/file-list/Selection";
|
||||
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
||||
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
|
||||
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils";
|
||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
||||
import { get, writable, type Readable } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { route } from './Routing';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
ListFileItem,
|
||||
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;
|
||||
export const canChangeStart = writable(false);
|
||||
@ -28,15 +32,22 @@ export class RoutingControls {
|
||||
popupElement: HTMLElement;
|
||||
temporaryAnchor: AnchorWithMarker;
|
||||
lastDragEvent = 0;
|
||||
fileUnsubscribe: () => void = () => { };
|
||||
fileUnsubscribe: () => void = () => {};
|
||||
unsubscribes: Function[] = [];
|
||||
|
||||
toggleAnchorsForZoomLevelAndBoundsBinded: () => void = this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
||||
toggleAnchorsForZoomLevelAndBoundsBinded: () => void =
|
||||
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
||||
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
|
||||
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.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.fileId = fileId;
|
||||
this.file = file;
|
||||
@ -46,8 +57,8 @@ export class RoutingControls {
|
||||
let point = new TrackPoint({
|
||||
attributes: {
|
||||
lat: 0,
|
||||
lon: 0
|
||||
}
|
||||
lon: 0,
|
||||
},
|
||||
});
|
||||
this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0);
|
||||
this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers
|
||||
@ -65,7 +76,9 @@ export class RoutingControls {
|
||||
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 (this.active) {
|
||||
this.updateControls();
|
||||
@ -88,7 +101,8 @@ export class RoutingControls {
|
||||
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;
|
||||
if (!file) {
|
||||
return;
|
||||
@ -96,8 +110,13 @@ export class RoutingControls {
|
||||
|
||||
let anchorIndex = 0;
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (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 (
|
||||
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 (anchorIndex < this.anchors.length) {
|
||||
this.anchors[anchorIndex].point = point;
|
||||
@ -106,7 +125,9 @@ export class RoutingControls {
|
||||
this.anchors[anchorIndex].segmentIndex = segmentIndex;
|
||||
this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates());
|
||||
} else {
|
||||
this.anchors.push(this.createAnchor(point, segment, trackIndex, segmentIndex));
|
||||
this.anchors.push(
|
||||
this.createAnchor(point, segment, trackIndex, segmentIndex)
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
@ -141,14 +163,19 @@ export class RoutingControls {
|
||||
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');
|
||||
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({
|
||||
draggable: true,
|
||||
className: 'z-10',
|
||||
element
|
||||
element,
|
||||
}).setLngLat(point.getCoordinates());
|
||||
|
||||
let anchor = {
|
||||
@ -157,7 +184,7 @@ export class RoutingControls {
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
marker,
|
||||
inZoom: false
|
||||
inZoom: false,
|
||||
};
|
||||
|
||||
marker.on('dragstart', (e) => {
|
||||
@ -185,7 +212,8 @@ export class RoutingControls {
|
||||
e.preventDefault();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -204,7 +232,12 @@ export class RoutingControls {
|
||||
return false;
|
||||
}
|
||||
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 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);
|
||||
|
||||
let center = this.map.getCenter();
|
||||
@ -245,7 +279,8 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -253,7 +288,15 @@ export class RoutingControls {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -263,7 +306,7 @@ export class RoutingControls {
|
||||
|
||||
this.temporaryAnchor.point.setCoordinates({
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng
|
||||
lon: e.lngLat.lng,
|
||||
});
|
||||
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
|
||||
|
||||
@ -271,12 +314,17 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
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);
|
||||
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.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
return;
|
||||
@ -294,14 +342,16 @@ export class RoutingControls {
|
||||
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 = {
|
||||
lat: anchorWithMarker.marker.getLngLat().lat,
|
||||
lon: anchorWithMarker.marker.getLngLat().lng
|
||||
lon: anchorWithMarker.marker.getLngLat().lng,
|
||||
};
|
||||
|
||||
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();
|
||||
anchor = this.getPermanentAnchor();
|
||||
}
|
||||
@ -326,7 +376,8 @@ export class RoutingControls {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -338,16 +389,24 @@ export class RoutingControls {
|
||||
let minDetails: any = { distance: Number.MAX_VALUE };
|
||||
let minAnchor = this.temporaryAnchor as Anchor;
|
||||
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 closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
||||
let closest = getClosestLinePoint(
|
||||
segment.trkpt,
|
||||
this.temporaryAnchor.point,
|
||||
details
|
||||
);
|
||||
if (details.distance < minDetails.distance) {
|
||||
minDetails = details;
|
||||
minAnchor = {
|
||||
point: closest,
|
||||
segment,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
segmentIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -374,41 +433,67 @@ export class RoutingControls {
|
||||
point: this.temporaryAnchor.point,
|
||||
trackIndex: -1,
|
||||
segmentIndex: -1,
|
||||
trkptIndex: -1
|
||||
trkptIndex: -1,
|
||||
};
|
||||
|
||||
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 = {};
|
||||
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
||||
if (details.distance < minDetails.distance) {
|
||||
minDetails = details;
|
||||
let before = details.before ? details.index : details.index - 1;
|
||||
|
||||
let projectedPt = projectedPoint(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 projectedPt = projectedPoint(
|
||||
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();
|
||||
point.setCoordinates(projectedPt);
|
||||
point.ele = (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.ele =
|
||||
(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 = {
|
||||
anchor: true,
|
||||
zoom: 0
|
||||
zoom: 0,
|
||||
};
|
||||
|
||||
minInfo = {
|
||||
point,
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
trkptIndex: before + 1
|
||||
trkptIndex: before + 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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
|
||||
|
||||
if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it
|
||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []));
|
||||
} 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
|
||||
if (previousAnchor === null && nextAnchor === null) {
|
||||
// Only one point, remove it
|
||||
dbUtils.applyToFile(this.fileId, (file) =>
|
||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
|
||||
);
|
||||
} 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) => {
|
||||
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
|
||||
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
|
||||
} else {
|
||||
// Route between previousAnchor and nextAnchor
|
||||
this.routeBetweenAnchors(
|
||||
[previousAnchor, nextAnchor],
|
||||
[previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -447,27 +556,43 @@ export class RoutingControls {
|
||||
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;
|
||||
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.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]);
|
||||
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.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') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appendAnchorWithCoordinates({
|
||||
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();
|
||||
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
|
||||
return;
|
||||
@ -477,7 +602,7 @@ export class RoutingControls {
|
||||
let lastAnchor = this.anchors[this.anchors.length - 1];
|
||||
|
||||
let newPoint = new TrackPoint({
|
||||
attributes: coordinates
|
||||
attributes: coordinates,
|
||||
});
|
||||
newPoint._data.anchor = true;
|
||||
newPoint._data.zoom = 0;
|
||||
@ -488,7 +613,10 @@ export class RoutingControls {
|
||||
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||
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) {
|
||||
segmentIndex = item.getSegmentIndex();
|
||||
}
|
||||
@ -512,10 +640,13 @@ export class RoutingControls {
|
||||
point: newPoint,
|
||||
segment: lastAnchor.segment,
|
||||
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] {
|
||||
@ -525,11 +656,17 @@ export class RoutingControls {
|
||||
for (let i = 0; i < this.anchors.length; i++) {
|
||||
if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) {
|
||||
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];
|
||||
}
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
@ -539,7 +676,10 @@ export class RoutingControls {
|
||||
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 fileWithStats = get(this.file);
|
||||
@ -547,10 +687,15 @@ export class RoutingControls {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anchors.length === 1) { // 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({
|
||||
if (anchors.length === 1) {
|
||||
// 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],
|
||||
})]));
|
||||
}),
|
||||
])
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -559,23 +704,28 @@ export class RoutingControls {
|
||||
response = await route(targetCoordinates);
|
||||
} catch (e: any) {
|
||||
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')) {
|
||||
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')) {
|
||||
toast.error(get(_)("toolbar.routing.error.to"));
|
||||
toast.error(get(_)('toolbar.routing.error.to'));
|
||||
} else if (e.message.includes('Time-out')) {
|
||||
toast.error(get(_)("toolbar.routing.error.timeout"));
|
||||
toast.error(get(_)('toolbar.routing.error.timeout'));
|
||||
} else {
|
||||
toast.error(e.message);
|
||||
}
|
||||
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._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._data.index = segment.trkpt.length - 1;
|
||||
} else {
|
||||
@ -583,7 +733,8 @@ export class RoutingControls {
|
||||
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._data.index = segment.trkpt.length - 1;
|
||||
} else {
|
||||
@ -594,7 +745,7 @@ export class RoutingControls {
|
||||
for (let i = 1; i < anchors.length - 1; i++) {
|
||||
// Find the closest point to the intermediate anchor
|
||||
// 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) => {
|
||||
@ -602,36 +753,64 @@ export class RoutingControls {
|
||||
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 startTime = anchors[0].point.time;
|
||||
|
||||
if (stats.global.speed.moving > 0) {
|
||||
let replacingDistance = 0;
|
||||
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 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;
|
||||
|
||||
if (replacingTime <= 0) { // 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];
|
||||
if (replacingTime <= 0) {
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx";
|
||||
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
|
||||
|
||||
const earthRadius = 6371008.8;
|
||||
|
||||
@ -17,7 +17,8 @@ export function updateAnchorPoints(file: GPXFile) {
|
||||
let segments = file.getSegments();
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
@ -42,4 +43,3 @@ function computeAnchorPoints(segment: TrackSegment) {
|
||||
});
|
||||
segment._data.anchors = true;
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
export enum SplitType {
|
||||
FILES = 'files',
|
||||
TRACKS = 'tracks',
|
||||
SEGMENTS = 'segments'
|
||||
SEGMENTS = 'segments',
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
$slicedGPXStatistics = [
|
||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
||||
sliderValues[0],
|
||||
sliderValues[1]
|
||||
sliderValues[1],
|
||||
];
|
||||
} else {
|
||||
$slicedGPXStatistics = undefined;
|
||||
@ -93,7 +93,7 @@
|
||||
const splitTypes = [
|
||||
{ value: SplitType.FILES, label: $_('gpx.files') },
|
||||
{ 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];
|
||||
@ -111,7 +111,12 @@
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||
<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>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { TrackPoint, TrackSegment } from "gpx";
|
||||
import { get } from "svelte/store";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { dbUtils, getFile } from "$lib/db";
|
||||
import { applyToOrderedSelectedItemsFromFile, selection } from "$lib/components/file-list/Selection";
|
||||
import { ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
||||
import { currentTool, gpxStatistics, Tool } from "$lib/stores";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Scissors } from "lucide-static";
|
||||
import { TrackPoint, TrackSegment } from 'gpx';
|
||||
import { get } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
import {
|
||||
applyToOrderedSelectedItemsFromFile,
|
||||
selection,
|
||||
} from '$lib/components/file-list/Selection';
|
||||
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 {
|
||||
active: boolean = false;
|
||||
@ -15,7 +18,8 @@ export class SplitControls {
|
||||
shownControls: ControlWithMarker[] = [];
|
||||
unsubscribes: Function[] = [];
|
||||
|
||||
toggleControlsForZoomLevelAndBoundsBinded: () => void = this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||
toggleControlsForZoomLevelAndBoundsBinded: () => void =
|
||||
this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
@ -48,15 +52,21 @@ export class SplitControls {
|
||||
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;
|
||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = getFile(fileId);
|
||||
|
||||
if (file) {
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (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 (
|
||||
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 (controlIndex < this.controls.length) {
|
||||
this.controls[controlIndex].fileId = fileId;
|
||||
@ -64,20 +74,30 @@ export class SplitControls {
|
||||
this.controls[controlIndex].segment = segment;
|
||||
this.controls[controlIndex].trackIndex = trackIndex;
|
||||
this.controls[controlIndex].segmentIndex = segmentIndex;
|
||||
this.controls[controlIndex].marker.setLngLat(point.getCoordinates());
|
||||
this.controls[controlIndex].marker.setLngLat(
|
||||
point.getCoordinates()
|
||||
);
|
||||
} else {
|
||||
this.controls.push(this.createControl(point, segment, fileId, trackIndex, segmentIndex));
|
||||
this.controls.push(
|
||||
this.createControl(
|
||||
point,
|
||||
segment,
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
)
|
||||
);
|
||||
}
|
||||
controlIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}, false);
|
||||
|
||||
while (controlIndex < this.controls.length) { // Remove the extra controls
|
||||
while (controlIndex < this.controls.length) {
|
||||
// Remove the extra controls
|
||||
this.controls.pop()?.marker.remove();
|
||||
}
|
||||
|
||||
@ -94,7 +114,8 @@ export class SplitControls {
|
||||
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);
|
||||
|
||||
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');
|
||||
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({
|
||||
draggable: true,
|
||||
className: 'z-10',
|
||||
element
|
||||
element,
|
||||
}).setLngLat(point.getCoordinates());
|
||||
|
||||
let control = {
|
||||
@ -131,12 +160,18 @@ export class SplitControls {
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
marker,
|
||||
inZoom: false
|
||||
inZoom: false,
|
||||
};
|
||||
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
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;
|
||||
|
||||
@ -1,11 +1,58 @@
|
||||
import Dexie, { liveQuery } from 'dexie';
|
||||
import { 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 {
|
||||
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 { gpxStatistics, initTargetMapBounds, 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 {
|
||||
gpxStatistics,
|
||||
initTargetMapBounds,
|
||||
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 { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||
@ -15,17 +62,22 @@ enableMapSet();
|
||||
enablePatches();
|
||||
|
||||
class Database extends Dexie {
|
||||
|
||||
fileids!: Dexie.Table<string, 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>;
|
||||
overpasstiles!: Dexie.Table<{ query: string, x: number, y: number, time: number }, [string, number, number]>;
|
||||
overpassdata!: Dexie.Table<{ query: string, id: number, poi: GeoJSON.Feature }, [string, number]>;
|
||||
overpasstiles!: Dexie.Table<
|
||||
{ query: string; x: number; y: number; time: number },
|
||||
[string, number, number]
|
||||
>;
|
||||
overpassdata!: Dexie.Table<
|
||||
{ query: string; id: number; poi: GeoJSON.Feature },
|
||||
[string, number]
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
super("Database", {
|
||||
cache: 'immutable'
|
||||
super('Database', {
|
||||
cache: 'immutable',
|
||||
});
|
||||
this.version(1).stores({
|
||||
fileids: ',&fileid',
|
||||
@ -41,10 +93,15 @@ class Database extends Dexie {
|
||||
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
|
||||
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 store = writable<V | undefined>(initialize ? initial : undefined);
|
||||
liveQuery(() => table.get(key)).subscribe(value => {
|
||||
liveQuery(() => table.get(key)).subscribe((value) => {
|
||||
if (value === undefined) {
|
||||
if (first) {
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -96,7 +157,11 @@ export const settings = {
|
||||
currentOverlays: dexieSettingStore('currentOverlays', defaultOverlays, false),
|
||||
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
|
||||
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
|
||||
currentOverpassQueries: dexieSettingStore('currentOverpassQueries', defaultOverpassQueries, false),
|
||||
currentOverpassQueries: dexieSettingStore(
|
||||
'currentOverpassQueries',
|
||||
defaultOverpassQueries,
|
||||
false
|
||||
),
|
||||
selectedOverpassTree: dexieSettingStore('selectedOverpassTree', defaultOverpassTree),
|
||||
opacities: dexieSettingStore('opacities', defaultOpacities),
|
||||
customLayers: dexieSettingStore<Record<string, CustomLayer>>('customLayers', {}),
|
||||
@ -107,7 +172,7 @@ export const settings = {
|
||||
streetViewSource: dexieSettingStore('streetViewSource', 'mapillary'),
|
||||
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
|
||||
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),
|
||||
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
|
||||
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
|
||||
let store = writable<T>(initial);
|
||||
liveQuery(querier).subscribe(value => {
|
||||
liveQuery(querier).subscribe((value) => {
|
||||
if (value !== undefined) {
|
||||
store.set(value);
|
||||
}
|
||||
@ -149,7 +214,7 @@ export class GPXStatisticsTree {
|
||||
let statistics = new GPXStatistics();
|
||||
let id = item.getIdAtLevel(this.level);
|
||||
if (id === undefined || id === 'waypoints') {
|
||||
Object.keys(this.statistics).forEach(key => {
|
||||
Object.keys(this.statistics).forEach((key) => {
|
||||
if (this.statistics[key] instanceof GPXStatistics) {
|
||||
statistics.mergeWith(this.statistics[key]);
|
||||
} else {
|
||||
@ -166,26 +231,30 @@ export class GPXStatisticsTree {
|
||||
}
|
||||
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
|
||||
function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } {
|
||||
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) {
|
||||
let gpx = new GPXFile(value);
|
||||
updateAnchorPoints(gpx);
|
||||
|
||||
let statistics = new GPXStatisticsTree(gpx);
|
||||
if (!fileState.has(id)) { // Update the map bounds for new files
|
||||
updateTargetMapBounds(id, statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
|
||||
if (!fileState.has(id)) {
|
||||
// Update the map bounds for new files
|
||||
updateTargetMapBounds(
|
||||
id,
|
||||
statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
|
||||
);
|
||||
}
|
||||
|
||||
fileState.set(id, gpx);
|
||||
store.set({
|
||||
file: gpx,
|
||||
statistics
|
||||
statistics,
|
||||
});
|
||||
|
||||
if (get(selection).hasAnyChildren(new ListFileItem(id))) {
|
||||
@ -198,7 +267,7 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
|
||||
destroy: () => {
|
||||
fileState.delete(id);
|
||||
query.unsubscribe();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -210,22 +279,30 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
|
||||
if (file) {
|
||||
items.forEach((item) => {
|
||||
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) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
} 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) {
|
||||
removedItems.push(item);
|
||||
} 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) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
@ -255,9 +332,10 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
|
||||
// Commit the changes to the file state to the database
|
||||
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: 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)) {
|
||||
updatedFileIds.push(id);
|
||||
} 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[];
|
||||
updatedFileIds = updatedFiles.map(file => file._data.id);
|
||||
let updatedFiles = updatedFileIds
|
||||
.map((id) => newFileState.get(id))
|
||||
.filter((file) => file !== undefined) as GPXFile[];
|
||||
updatedFileIds = updatedFiles.map((file) => file._data.id);
|
||||
|
||||
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
|
||||
|
||||
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
||||
export function observeFilesFromDatabase(fitBounds: boolean) {
|
||||
let initialize = true;
|
||||
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
||||
liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
|
||||
if (initialize) {
|
||||
if (fitBounds && dbFileIds.length > 0) {
|
||||
initTargetMapBounds(dbFileIds);
|
||||
@ -296,17 +378,21 @@ export function observeFilesFromDatabase(fitBounds: boolean) {
|
||||
initialize = false;
|
||||
}
|
||||
// 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
|
||||
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
|
||||
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
||||
fileObservers.update($files => {
|
||||
newFiles.forEach(id => {
|
||||
fileObservers.update(($files) => {
|
||||
newFiles.forEach((id) => {
|
||||
$files.set(id, dexieGPXFileStore(id));
|
||||
});
|
||||
deletedFiles.forEach(id => {
|
||||
deletedFiles.forEach((id) => {
|
||||
$files.get(id)?.destroy?.();
|
||||
$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 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) {
|
||||
return { min: 0, max: 0 };
|
||||
} else {
|
||||
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);
|
||||
export const canRedo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1);
|
||||
}),
|
||||
{ min: 0, max: 0 }
|
||||
);
|
||||
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
|
||||
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
|
||||
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) => {
|
||||
fileIds.forEach((fileId, index) => {
|
||||
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
|
||||
let minmax = get(patchMinMaxIndex);
|
||||
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 () => {
|
||||
let index = get(patchIndex) + 1;
|
||||
await db.patches.put({
|
||||
await db.patches.put(
|
||||
{
|
||||
patch,
|
||||
inversePatch,
|
||||
index,
|
||||
},
|
||||
index
|
||||
}, index);
|
||||
);
|
||||
await db.settings.put(index, 'patchIndex');
|
||||
});
|
||||
}
|
||||
@ -467,7 +577,12 @@ export const dbUtils = {
|
||||
applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => void) => {
|
||||
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);
|
||||
},
|
||||
duplicateSelection: () => {
|
||||
@ -491,20 +606,33 @@ export const dbUtils = {
|
||||
if (level === ListLevel.TRACK) {
|
||||
for (let item of items) {
|
||||
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) {
|
||||
for (let item of items) {
|
||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
||||
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) {
|
||||
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) {
|
||||
for (let item of items) {
|
||||
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) => {
|
||||
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) => {
|
||||
dbUtils.applyToFile(fileId, (file) => {
|
||||
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: () => {
|
||||
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;
|
||||
}
|
||||
applyGlobal((draft) => {
|
||||
@ -579,13 +714,13 @@ export const dbUtils = {
|
||||
let target: ListItem = new ListRootItem();
|
||||
let targetFile: GPXFile | undefined = undefined;
|
||||
let toMerge: {
|
||||
trk: Track[],
|
||||
trkseg: TrackSegment[],
|
||||
wpt: Waypoint[]
|
||||
trk: Track[];
|
||||
trkseg: TrackSegment[];
|
||||
wpt: Waypoint[];
|
||||
} = {
|
||||
trk: [],
|
||||
trkseg: [],
|
||||
wpt: []
|
||||
wpt: [],
|
||||
};
|
||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = draft.get(fileId);
|
||||
@ -608,8 +743,15 @@ export const dbUtils = {
|
||||
if (level === ListLevel.TRACK) {
|
||||
items.forEach((item, index) => {
|
||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
||||
toMerge.trkseg.splice(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
|
||||
toMerge.trkseg.splice(
|
||||
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;
|
||||
file.trk[trackIndex].trkseg = [];
|
||||
} else {
|
||||
@ -620,10 +762,15 @@ export const dbUtils = {
|
||||
items.forEach((item, index) => {
|
||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
||||
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;
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -635,15 +782,24 @@ export const dbUtils = {
|
||||
|
||||
if (mergeTraces) {
|
||||
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;
|
||||
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;
|
||||
} else {
|
||||
let index = statistics.local.points.findIndex((point) => point.time !== undefined);
|
||||
let index = statistics.local.points.findIndex(
|
||||
(point) => point.time !== undefined
|
||||
);
|
||||
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();
|
||||
toMerge.trk.map((track) => {
|
||||
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]];
|
||||
@ -661,7 +824,14 @@ export const dbUtils = {
|
||||
if (toMerge.trkseg.length > 0) {
|
||||
let s = new TrackSegment();
|
||||
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];
|
||||
}
|
||||
@ -677,7 +847,12 @@ export const dbUtils = {
|
||||
} else if (target instanceof ListTrackSegmentItem) {
|
||||
let trackIndex = target.getTrackIndex();
|
||||
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;
|
||||
end -= length;
|
||||
} 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);
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -724,14 +903,17 @@ export const dbUtils = {
|
||||
return {
|
||||
wptIndex: wptIndex,
|
||||
index: [0],
|
||||
distance: Number.MAX_VALUE
|
||||
distance: Number.MAX_VALUE,
|
||||
};
|
||||
})
|
||||
});
|
||||
file.trk.forEach((track, index) => {
|
||||
track.getSegments().forEach((segment) => {
|
||||
segment.trkpt.forEach((point) => {
|
||||
file.wpt.forEach((wpt, wptIndex) => {
|
||||
let dist = distance(point.getCoordinates(), wpt.getCoordinates());
|
||||
let dist = distance(
|
||||
point.getCoordinates(),
|
||||
wpt.getCoordinates()
|
||||
);
|
||||
if (dist < closest[wptIndex].distance) {
|
||||
closest[wptIndex].distance = dist;
|
||||
closest[wptIndex].index = [index];
|
||||
@ -739,7 +921,7 @@ export const dbUtils = {
|
||||
closest[wptIndex].index.push(index);
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -754,9 +936,16 @@ export const dbUtils = {
|
||||
return t;
|
||||
});
|
||||
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.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));
|
||||
});
|
||||
} else if (file.trk.length === 1) {
|
||||
@ -766,13 +955,16 @@ export const dbUtils = {
|
||||
return {
|
||||
wptIndex: wptIndex,
|
||||
index: [0],
|
||||
distance: Number.MAX_VALUE
|
||||
distance: Number.MAX_VALUE,
|
||||
};
|
||||
})
|
||||
});
|
||||
file.trk[0].trkseg.forEach((segment, index) => {
|
||||
segment.trkpt.forEach((point) => {
|
||||
file.wpt.forEach((wpt, wptIndex) => {
|
||||
let dist = distance(point.getCoordinates(), wpt.getCoordinates());
|
||||
let dist = distance(
|
||||
point.getCoordinates(),
|
||||
wpt.getCoordinates()
|
||||
);
|
||||
if (dist < closest[wptIndex].distance) {
|
||||
closest[wptIndex].distance = dist;
|
||||
closest[wptIndex].index = [index];
|
||||
@ -785,8 +977,16 @@ export const dbUtils = {
|
||||
|
||||
file.trk[0].trkseg.forEach((segment, index) => {
|
||||
let newFile = file.clone();
|
||||
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment]);
|
||||
newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]));
|
||||
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
||||
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.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
||||
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);
|
||||
return applyGlobal((draft) => {
|
||||
let file = getFile(fileId);
|
||||
@ -833,7 +1039,10 @@ export const dbUtils = {
|
||||
|
||||
let absoluteIndex = minIndex;
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -863,13 +1072,21 @@ export const dbUtils = {
|
||||
start.crop(0, minIndex);
|
||||
let end = segment.clone();
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -880,16 +1097,35 @@ export const dbUtils = {
|
||||
if (level === ListLevel.FILE) {
|
||||
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
|
||||
} else if (level === ListLevel.TRACK) {
|
||||
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
|
||||
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices);
|
||||
let trackIndices = items.map((item) =>
|
||||
(item as ListTrackItem).getTrackIndex()
|
||||
);
|
||||
file.clean(
|
||||
bounds,
|
||||
inside,
|
||||
deleteTrackPoints,
|
||||
deleteWaypoints,
|
||||
trackIndices
|
||||
);
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
|
||||
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
|
||||
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices, segmentIndices);
|
||||
let segmentIndices = items.map((item) =>
|
||||
(item as ListTrackSegmentItem).getSegmentIndex()
|
||||
);
|
||||
file.clean(
|
||||
bounds,
|
||||
inside,
|
||||
deleteTrackPoints,
|
||||
deleteWaypoints,
|
||||
trackIndices,
|
||||
segmentIndices
|
||||
);
|
||||
} else if (level === ListLevel.WAYPOINTS) {
|
||||
file.clean(bounds, inside, false, deleteWaypoints);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@ -911,7 +1147,15 @@ export const dbUtils = {
|
||||
let segmentIndex = item.getSegmentIndex();
|
||||
let points = itemsAndPoints.get(item);
|
||||
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 {
|
||||
let fileIds = new Set<string>();
|
||||
get(selection).getSelected().forEach((item) => {
|
||||
get(selection)
|
||||
.getSelected()
|
||||
.forEach((item) => {
|
||||
fileIds.add(item.getFileId());
|
||||
});
|
||||
let wpt = new Waypoint(waypoint);
|
||||
@ -984,16 +1230,22 @@ export const dbUtils = {
|
||||
if (level === ListLevel.FILE) {
|
||||
file.setHidden(hidden);
|
||||
} 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);
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
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);
|
||||
} else if (level === ListLevel.WAYPOINTS) {
|
||||
file.setHiddenWaypoints(hidden);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@ -1020,7 +1272,12 @@ export const dbUtils = {
|
||||
for (let item of items) {
|
||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
||||
file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
|
||||
file.replaceTrackSegments(
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
segmentIndex,
|
||||
[]
|
||||
);
|
||||
}
|
||||
} else if (level === ListLevel.WAYPOINTS) {
|
||||
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
||||
@ -1053,14 +1310,18 @@ export const dbUtils = {
|
||||
});
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
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) => {
|
||||
points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints());
|
||||
});
|
||||
} else if (level === ListLevel.WAYPOINTS) {
|
||||
points.push(...file.wpt);
|
||||
} 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]));
|
||||
}
|
||||
}
|
||||
@ -1078,16 +1339,22 @@ export const dbUtils = {
|
||||
if (level === ListLevel.FILE) {
|
||||
file.addElevation(elevations);
|
||||
} 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, []);
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
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, []);
|
||||
} else if (level === ListLevel.WAYPOINTS) {
|
||||
file.addElevation(elevations, [], [], undefined);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@ -1114,7 +1381,7 @@ export const dbUtils = {
|
||||
undo: () => {
|
||||
if (get(canUndo)) {
|
||||
let index = get(patchIndex);
|
||||
db.patches.get(index).then(patch => {
|
||||
db.patches.get(index).then((patch) => {
|
||||
if (patch) {
|
||||
applyPatch(patch.inversePatch);
|
||||
db.settings.put(index - 1, 'patchIndex');
|
||||
@ -1125,12 +1392,12 @@ export const dbUtils = {
|
||||
redo: () => {
|
||||
if (get(canRedo)) {
|
||||
let index = get(patchIndex) + 1;
|
||||
db.patches.get(index).then(patch => {
|
||||
db.patches.get(index).then((patch) => {
|
||||
if (patch) {
|
||||
applyPatch(patch.patch);
|
||||
db.settings.put(index, 'patchIndex');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
export const languages: Record<string, string> = {
|
||||
'en': 'English',
|
||||
'es': 'Español',
|
||||
'de': 'Deutsch',
|
||||
'fr': 'Français',
|
||||
'it': 'Italiano',
|
||||
'nl': 'Nederlands',
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
de: 'Deutsch',
|
||||
fr: 'Français',
|
||||
it: 'Italiano',
|
||||
nl: 'Nederlands',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'zh': '简体中文',
|
||||
zh: '简体中文',
|
||||
};
|
||||
@ -10,13 +10,19 @@ function generateSitemap() {
|
||||
const pages = glob.sync('**/*.html', { cwd: 'build' }).map((page) => `/${page}`);
|
||||
|
||||
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) => {
|
||||
const path = page.replace('/index.html', '').replace('.html', '');
|
||||
|
||||
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
|
||||
return;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
applyToOrderedSelectedItemsFromFile,
|
||||
selectFile,
|
||||
selectItem,
|
||||
selection
|
||||
selection,
|
||||
} from '$lib/components/file-list/Selection';
|
||||
import {
|
||||
ListFileItem,
|
||||
@ -19,7 +19,7 @@ import {
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/FileList';
|
||||
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
|
||||
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
@ -43,7 +43,10 @@ export function updateGPXData() {
|
||||
if (stats) {
|
||||
let first = true;
|
||||
items.forEach((item) => {
|
||||
if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) {
|
||||
if (
|
||||
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
||||
first
|
||||
) {
|
||||
statistics.mergeWith(stats.getStatisticsFor(item));
|
||||
first = false;
|
||||
}
|
||||
@ -110,7 +113,8 @@ derived([targetMapBounds, map], (x) => x).subscribe(([bounds, $map]) => {
|
||||
|
||||
let currentZoom = $map.getZoom();
|
||||
let currentBounds = $map.getBounds();
|
||||
if (bounds.total !== get(fileObservers).size &&
|
||||
if (
|
||||
bounds.total !== get(fileObservers).size &&
|
||||
currentBounds &&
|
||||
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) {
|
||||
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 bounds = new mapboxgl.LngLatBounds();
|
||||
|
||||
@ -187,7 +193,7 @@ export function centerMapOnSelection(
|
||||
get(map)?.fitBounds(bounds, {
|
||||
padding: 80,
|
||||
easing: () => 1,
|
||||
maxZoom: 15
|
||||
maxZoom: 15,
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,7 +209,7 @@ export enum Tool {
|
||||
EXTRACT,
|
||||
ELEVATION,
|
||||
REDUCE,
|
||||
CLEAN
|
||||
CLEAN,
|
||||
}
|
||||
export const currentTool = writable<Tool | null>(null);
|
||||
export const splitAs = writable(SplitType.FILES);
|
||||
@ -410,7 +416,7 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) {
|
||||
|
||||
async function exportFiles(fileIds: string[], exclude: string[]) {
|
||||
if (fileIds.length > 1) {
|
||||
await exportFilesAsZip(fileIds, exclude)
|
||||
await exportFilesAsZip(fileIds, exclude);
|
||||
} else {
|
||||
const firstFileId = fileIds.at(0);
|
||||
if (firstFileId != null) {
|
||||
@ -468,7 +474,10 @@ export function updateAllHidden() {
|
||||
|
||||
if (item instanceof ListFileItem) {
|
||||
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;
|
||||
} else if (
|
||||
item instanceof ListTrackSegmentItem &&
|
||||
@ -477,10 +486,14 @@ export function updateAllHidden() {
|
||||
) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -496,6 +509,6 @@ export const editStyle = writable(false);
|
||||
export enum ExportState {
|
||||
NONE,
|
||||
SELECTION,
|
||||
ALL
|
||||
ALL,
|
||||
}
|
||||
export const exportState = writable<ExportState>(ExportState.NONE);
|
||||
|
||||
@ -161,7 +161,9 @@ export function getPowerUnits() {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import type { TransitionConfig } from "svelte/transition";
|
||||
import { get } from "svelte/store";
|
||||
import { map } from "./stores";
|
||||
import { base } from "$app/paths";
|
||||
import { languages } from "$lib/languages";
|
||||
import { locale } from "svelte-i18n";
|
||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from "gpx";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import tilebelt from "@mapbox/tilebelt";
|
||||
import { PUBLIC_MAPBOX_TOKEN } from "$env/static/public";
|
||||
import PNGReader from "png.js";
|
||||
import type { DateFormatter } from "@internationalized/date";
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import type { TransitionConfig } from 'svelte/transition';
|
||||
import { get } from 'svelte/store';
|
||||
import { map } from './stores';
|
||||
import { base } from '$app/paths';
|
||||
import { languages } from '$lib/languages';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import tilebelt from '@mapbox/tilebelt';
|
||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||
import PNGReader from 'png.js';
|
||||
import type { DateFormatter } from '@internationalized/date';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@ -30,7 +30,7 @@ export const flyAndScale = (
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 50 }
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
const scaleConversion = (
|
||||
valueA: number,
|
||||
@ -46,13 +46,11 @@ export const flyAndScale = (
|
||||
return valueB;
|
||||
};
|
||||
|
||||
const styleToString = (
|
||||
style: Record<string, number | string | undefined>
|
||||
): string => {
|
||||
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, "");
|
||||
}, '');
|
||||
};
|
||||
|
||||
return {
|
||||
@ -65,14 +63,18 @@ export const flyAndScale = (
|
||||
|
||||
return styleToString({
|
||||
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 closestDist = Number.MAX_VALUE;
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
@ -94,16 +96,34 @@ export function getClosestLinePoint(points: TrackPoint[], point: TrackPoint | Co
|
||||
return closest;
|
||||
}
|
||||
|
||||
export function getElevation(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);
|
||||
export function getElevation(
|
||||
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();
|
||||
coordinates.forEach((coord) => bbox.extend(coord));
|
||||
|
||||
let tiles = coordinates.map((coord) => tilebelt.pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
|
||||
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) => tile.split(',').map((x) => parseInt(x)));
|
||||
let tiles = coordinates.map((coord) =>
|
||||
tilebelt.pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM)
|
||||
);
|
||||
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
|
||||
tile.split(',').map((x) => parseInt(x))
|
||||
);
|
||||
let pngs = new Map<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));
|
||||
png.parse((err, png) => {
|
||||
if (err) {
|
||||
@ -113,9 +133,12 @@ export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], EL
|
||||
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 png = pngs.get(tile.join(','));
|
||||
|
||||
@ -134,15 +157,24 @@ export function getElevation(points: (TrackPoint | Waypoint | Coordinates)[], EL
|
||||
const p00 = png.getPixel(_x, _y);
|
||||
const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1));
|
||||
const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y);
|
||||
const p11 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y + (_y + 1 == tileSize ? 0 : 1));
|
||||
const p11 = png.getPixel(
|
||||
_x + (_x + 1 == tileSize ? 0 : 1),
|
||||
_y + (_y + 1 == tileSize ? 0 : 1)
|
||||
);
|
||||
|
||||
let ele00 = -10000 + ((p00[0] * 256 * 256 + p00[1] * 256 + p00[2]) * 0.1);
|
||||
let ele01 = -10000 + ((p01[0] * 256 * 256 + p01[1] * 256 + p01[2]) * 0.1);
|
||||
let ele10 = -10000 + ((p10[0] * 256 * 256 + p10[1] * 256 + p10[2]) * 0.1);
|
||||
let ele11 = -10000 + ((p11[0] * 256 * 256 + p11[1] * 256 + p11[2]) * 0.1);
|
||||
let ele00 = -10000 + (p00[0] * 256 * 256 + p00[1] * 256 + p00[2]) * 0.1;
|
||||
let ele01 = -10000 + (p01[0] * 256 * 256 + p01[1] * 256 + p01[2]) * 0.1;
|
||||
let ele10 = -10000 + (p10[0] * 256 * 256 + p10[1] * 256 + p10[2]) * 0.1;
|
||||
let ele11 = -10000 + (p11[0] * 256 * 256 + p11[1] * 256 + p11[2]) * 0.1;
|
||||
|
||||
return ele00 * (1 - dx) * (1 - dy) + ele01 * (1 - dx) * dy + ele10 * dx * (1 - dy) + ele11 * dx * dy;
|
||||
}));
|
||||
return (
|
||||
ele00 * (1 - dx) * (1 - dy) +
|
||||
ele01 * (1 - dx) * dy +
|
||||
ele10 * dx * (1 - dy) +
|
||||
ele11 * dx * dy
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let previousCursors: string[] = [];
|
||||
@ -226,7 +258,7 @@ export function getURLForLanguage(lang: string | null | undefined, path: string)
|
||||
function getDateFormatter(locale: string) {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium'
|
||||
timeStyle: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,9 @@ export async function load({ params }) {
|
||||
for (let guide of Object.keys(guides)) {
|
||||
guideTitles[guide] = (await getModule(language, guide)).metadata.title;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
if ($page.url.searchParams.has('embed')) {
|
||||
// convert old embedding options to new format and redirect to new embed page
|
||||
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)))}`;
|
||||
}
|
||||
});
|
||||
|
||||
@ -11,7 +11,10 @@
|
||||
<p class="text-xl -mt-6">{$_('page_not_found')}</p>
|
||||
<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">
|
||||
<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" />
|
||||
{$_('homepage.home')}
|
||||
</Button>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
PencilRuler,
|
||||
PenLine,
|
||||
Route,
|
||||
Scale
|
||||
Scale,
|
||||
} from 'lucide-svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
@ -49,7 +49,9 @@
|
||||
<div class="-mt-12 sm:mt-0">
|
||||
<div class="px-12 w-full flex flex-col items-center">
|
||||
<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">
|
||||
{$_('metadata.description')}
|
||||
</div>
|
||||
@ -81,7 +83,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h1>
|
||||
<Route size="24" class="mr-1 inline-block align-baseline" />
|
||||
@ -95,7 +99,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h1>
|
||||
<PencilRuler size="24" class="mr-1 inline-block align-baseline" />
|
||||
@ -167,7 +173,9 @@
|
||||
<ChartArea size="24" class="mr-1 inline-block align-baseline" />
|
||||
{$_('homepage.data_visualization')}
|
||||
</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 class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
@ -189,7 +197,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h1>
|
||||
<Scale size="24" class="mr-1 inline-block align-baseline" />
|
||||
@ -228,7 +238,12 @@
|
||||
<DocsContainer module={fundingModule.default} />
|
||||
{/await}
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
@ -248,7 +263,9 @@
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
❤️ {$_('homepage.supported_by')}
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
bottomPanelSize,
|
||||
rightPanelSize,
|
||||
additionalDatasets,
|
||||
elevationFill
|
||||
elevationFill,
|
||||
} = settings;
|
||||
|
||||
onMount(() => {
|
||||
@ -109,7 +109,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if $elevationProfile}
|
||||
<Resizer orientation="row" bind:after={$bottomPanelSize} minAfter={100} maxAfter={300} />
|
||||
<Resizer
|
||||
orientation="row"
|
||||
bind:after={$bottomPanelSize}
|
||||
minAfter={100}
|
||||
maxAfter={300}
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import Embedding from '$lib/components/embedding/Embedding.svelte';
|
||||
import {
|
||||
getMergedEmbeddingOptions,
|
||||
type EmbeddingOptions
|
||||
type EmbeddingOptions,
|
||||
} from '$lib/components/embedding/Embedding';
|
||||
|
||||
let embeddingOptions: EmbeddingOptions | undefined = undefined;
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
</script>
|
||||
|
||||
<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}
|
||||
<Button
|
||||
variant="link"
|
||||
|
||||
@ -30,7 +30,11 @@
|
||||
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"
|
||||
>
|
||||
<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}`]}
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
@ -59,8 +59,8 @@
|
||||
{#if $locale === 'en'}
|
||||
<Button
|
||||
variant="link"
|
||||
href="https://github.com/gpxstudio/gpx.studio/edit/dev/website/src/lib/docs/en/{$page.params
|
||||
.guide}.mdx"
|
||||
href="https://github.com/gpxstudio/gpx.studio/edit/dev/website/src/lib/docs/en/{$page
|
||||
.params.guide}.mdx"
|
||||
target="_blank"
|
||||
class="p-0 h-6 ml-auto text-link"
|
||||
>
|
||||
|
||||
@ -4,7 +4,7 @@ import { mdsvex } from 'mdsvex';
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.mdx']
|
||||
extensions: ['.mdx'],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@ -16,7 +16,7 @@ const config = {
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
precompress: false,
|
||||
strict: true
|
||||
strict: true,
|
||||
}),
|
||||
paths: {
|
||||
base: process.argv.includes('dev') ? '' : process.env.BASE_PATH,
|
||||
@ -25,8 +25,8 @@ const config = {
|
||||
prerender: {
|
||||
entries: ['/', '/404'],
|
||||
crawl: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -1,64 +1,64 @@
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
safelist: ["dark"],
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
safelist: ['dark'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border) / <alpha-value>)",
|
||||
input: "hsl(var(--input) / <alpha-value>)",
|
||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||
background: "hsl(var(--background) / <alpha-value>)",
|
||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||
border: 'hsl(var(--border) / <alpha-value>)',
|
||||
input: 'hsl(var(--input) / <alpha-value>)',
|
||||
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||
background: 'hsl(var(--background) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
||||
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--card-foreground) / <alpha-value>)',
|
||||
},
|
||||
support: "rgb(var(--support))",
|
||||
link: "rgb(var(--link))"
|
||||
support: 'rgb(var(--support))',
|
||||
link: 'rgb(var(--link))',
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
sans: [...fontFamily.sans],
|
||||
},
|
||||
screens: {
|
||||
"xs": "540px",
|
||||
}
|
||||
xs: '540px',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
dvh: 'height: 100dvh',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { enhancedImages } from '@sveltejs/enhanced-img';
|
||||
import { defineConfig } from 'vite';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [nodePolyfills({
|
||||
plugins: [
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true,
|
||||
},
|
||||
}), enhancedImages(), sveltekit()]
|
||||
}),
|
||||
enhancedImages(),
|
||||
sveltekit(),
|
||||
],
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user