mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-03-16 09:42:59 +00:00
Compare commits
127 Commits
drop-to-de
...
228ad1044e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228ad1044e | ||
|
|
a9ea0e223d | ||
|
|
37e8237f78 | ||
|
|
40deb19837 | ||
|
|
a9deb681e0 | ||
|
|
018e638ae3 | ||
|
|
967d271667 | ||
|
|
4e92a16d8d | ||
|
|
b4bcda12c2 | ||
|
|
e5bb902cd4 | ||
|
|
a3817fd5cd | ||
|
|
3a6338e9fb | ||
|
|
34d2342bdb | ||
|
|
5623a6b662 | ||
|
|
de0cc63d53 | ||
|
|
cb4892ace3 | ||
|
|
3cd17d7409 | ||
|
|
2848402e97 | ||
|
|
7e8f1acf67 | ||
|
|
8aa8a77260 | ||
|
|
ea4e078c92 | ||
|
|
21261f732f | ||
|
|
4e9d65089c | ||
|
|
e4f6dfbf78 | ||
|
|
9083784ffd | ||
|
|
6c4c4dbac9 | ||
|
|
ea0abf15ce | ||
|
|
71cccc5bdc | ||
|
|
4964928ac0 | ||
|
|
7fde60a267 | ||
|
|
306ed2ae0e | ||
|
|
a7cfe36b2e | ||
|
|
f4879a9e8a | ||
|
|
c8e09fcd90 | ||
|
|
c5f20d323c | ||
|
|
e3dcdf2f41 | ||
|
|
82d8b5d61e | ||
|
|
47692656e4 | ||
|
|
b5bf06b37a | ||
|
|
bc3b1e5f7c | ||
|
|
63eae15191 | ||
|
|
848b6dcef3 | ||
|
|
dfcdd71057 | ||
|
|
7368945bf3 | ||
|
|
de52203e89 | ||
|
|
62d1a3e01f | ||
|
|
68b4ecadf5 | ||
|
|
7831774703 | ||
|
|
52984d4b70 | ||
|
|
0b457f9a1e | ||
|
|
01cfd448f0 | ||
|
|
c189ebd8ca | ||
|
|
b35d11c9ed | ||
|
|
5fa5908072 | ||
|
|
a89f2754d3 | ||
|
|
453ae55db0 | ||
|
|
c1a5bdd7ae | ||
|
|
d19e702084 | ||
|
|
e02a22eaea | ||
|
|
63f3d63518 | ||
|
|
0d03ebfe96 | ||
|
|
074da855c1 | ||
|
|
9a028b9d5d | ||
|
|
a502980a39 | ||
|
|
1a1f6e5131 | ||
|
|
a67501e6de | ||
|
|
1f4164aca3 | ||
|
|
25e05f9855 | ||
|
|
1555799533 | ||
|
|
93d5211d27 | ||
|
|
de38fea917 | ||
|
|
3a3bc1c0db | ||
|
|
b5871d974e | ||
|
|
2985c3201b | ||
|
|
cf2fcd8221 | ||
|
|
2cb21a43c5 | ||
|
|
bae0a3f93b | ||
|
|
a853c45ec7 | ||
|
|
6cb6c88cd1 | ||
|
|
077f2b4435 | ||
|
|
8c3c4860f8 | ||
|
|
143592f724 | ||
|
|
dc404706c5 | ||
|
|
7a80e9e104 | ||
|
|
d7aae81c41 | ||
|
|
745c7e8470 | ||
|
|
52623350bd | ||
|
|
7d2c030ebd | ||
|
|
b841326e19 | ||
|
|
23c41f18de | ||
|
|
44e11e1a51 | ||
|
|
798d8e7a14 | ||
|
|
29748cf114 | ||
|
|
f5794b1355 | ||
|
|
4ada271ad3 | ||
|
|
1bda957778 | ||
|
|
95328db1ee | ||
|
|
65cbf5e751 | ||
|
|
60f24f8757 | ||
|
|
d1e4588813 | ||
|
|
711825f5a3 | ||
|
|
d823f44558 | ||
|
|
193c77e51a | ||
|
|
45f6c405c0 | ||
|
|
78e1ea1e59 | ||
|
|
ca51e1d788 | ||
|
|
72b0b5a706 | ||
|
|
4d1de97ba5 | ||
|
|
04769002d0 | ||
|
|
8190284148 | ||
|
|
a668b4be62 | ||
|
|
87534468d2 | ||
|
|
d7a02f714a | ||
|
|
0c16ddd534 | ||
|
|
a96f989199 | ||
|
|
35c7c9d965 | ||
|
|
efa05b93ce | ||
|
|
0e298cd0e4 | ||
|
|
3ef98b2110 | ||
|
|
fbf93ed6f9 | ||
|
|
1bd56b6505 | ||
|
|
acf0750ccb | ||
|
|
48eaa344e4 | ||
|
|
3262dec7d3 | ||
|
|
572d206c2c | ||
|
|
11934e5825 | ||
|
|
5cca106d18 |
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
|
||||||
1617
gpx/package-lock.json
generated
1617
gpx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,16 +12,20 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-xml-parser": "^4.5.0",
|
"fast-xml-parser": "^4.5.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1"
|
||||||
"ts-node": "^10.9.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"@types/node": "^20.16.10",
|
"@types/node": "^20.16.10",
|
||||||
|
"@typescript-eslint/parser": "^8.22.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"postinstall": "npm run build"
|
"postinstall": "npm run build",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1035
gpx/src/gpx.ts
1035
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 { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||||
export { parseGPX, buildGPX } from './io';
|
export { parseGPX, buildGPX } from './io';
|
||||||
export * from './simplify';
|
export * from './simplify';
|
||||||
|
|
||||||
|
|||||||
112
gpx/src/io.ts
112
gpx/src/io.ts
@@ -1,25 +1,68 @@
|
|||||||
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||||
import { GPXFileType } from "./types";
|
import { GPXFileType } from './types';
|
||||||
import { GPXFile } from "./gpx";
|
import { GPXFile } from './gpx';
|
||||||
|
|
||||||
|
const attributesWithNamespace = {
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
const floatPatterns = [
|
||||||
|
/[-+]?\d*\.\d+$/, // decimal
|
||||||
|
/[-+]?\d+$/, // integer
|
||||||
|
];
|
||||||
|
function safeParseFloat(value: string): number {
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
if (!isNaN(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
for (const pattern of floatPatterns) {
|
||||||
|
const match = value.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
return parseFloat(match[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseGPX(gpxData: string): GPXFile {
|
export function parseGPX(gpxData: string): GPXFile {
|
||||||
const parser = new XMLParser({
|
const parser = new XMLParser({
|
||||||
ignoreAttributes: false,
|
ignoreAttributes: false,
|
||||||
attributeNamePrefix: "",
|
attributeNamePrefix: '',
|
||||||
attributesGroupName: 'attributes',
|
attributesGroupName: 'attributes',
|
||||||
|
removeNSPrefix: true,
|
||||||
isArray(name: string) {
|
isArray(name: string) {
|
||||||
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt';
|
return (
|
||||||
|
name === 'trk' ||
|
||||||
|
name === 'trkseg' ||
|
||||||
|
name === 'trkpt' ||
|
||||||
|
name === 'wpt' ||
|
||||||
|
name === 'rte' ||
|
||||||
|
name === 'rtept' ||
|
||||||
|
name === 'gpxx:rpt'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
attributeValueProcessor(attrName, attrValue, jPath) {
|
attributeValueProcessor(attrName, attrValue, jPath) {
|
||||||
if (attrName === 'lat' || attrName === 'lon') {
|
if (attrName === 'lat' || attrName === 'lon') {
|
||||||
return parseFloat(attrValue);
|
return safeParseFloat(attrValue);
|
||||||
}
|
}
|
||||||
return attrValue;
|
return attrValue;
|
||||||
},
|
},
|
||||||
transformTagName(tagName: string) {
|
transformTagName(tagName: string) {
|
||||||
if (tagName === 'power') {
|
if (attributesWithNamespace[tagName]) {
|
||||||
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor
|
return attributesWithNamespace[tagName];
|
||||||
return 'gpxpx:PowerExtension';
|
|
||||||
}
|
}
|
||||||
return tagName;
|
return tagName;
|
||||||
},
|
},
|
||||||
@@ -27,22 +70,29 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
|
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
|
||||||
if (isLeafNode) {
|
if (isLeafNode) {
|
||||||
if (tagName === 'ele') {
|
if (tagName === 'ele') {
|
||||||
return parseFloat(tagValue);
|
return safeParseFloat(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === 'time') {
|
if (tagName === 'time') {
|
||||||
return new Date(tagValue);
|
return new Date(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') {
|
if (
|
||||||
return parseFloat(tagValue);
|
tagName === 'gpxtpx:atemp' ||
|
||||||
|
tagName === 'gpxtpx:hr' ||
|
||||||
|
tagName === 'gpxtpx:cad' ||
|
||||||
|
tagName === 'gpxpx:PowerInWatts' ||
|
||||||
|
tagName === 'gpx_style:opacity' ||
|
||||||
|
tagName === 'gpx_style:width'
|
||||||
|
) {
|
||||||
|
return safeParseFloat(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === 'gpxpx:PowerExtension') {
|
if (tagName === 'gpxpx:PowerExtension') {
|
||||||
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
||||||
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
||||||
return {
|
return {
|
||||||
'gpxpx:PowerInWatts': parseFloat(tagValue)
|
'gpxpx:PowerInWatts': safeParseFloat(tagValue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,35 +104,42 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (parsed.metadata === "") {
|
if (parsed.metadata === '') {
|
||||||
parsed.metadata = {};
|
parsed.metadata = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GPXFile(parsed);
|
return new GPXFile(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildGPX(file: GPXFile, exclude: string[] = []): string {
|
export function buildGPX(file: GPXFile, exclude: string[]): string {
|
||||||
const gpx = file.toGPXFileType(exclude);
|
const gpx = file.toGPXFileType(exclude);
|
||||||
|
|
||||||
|
let lastDate = undefined;
|
||||||
const builder = new XMLBuilder({
|
const builder = new XMLBuilder({
|
||||||
format: true,
|
format: true,
|
||||||
ignoreAttributes: false,
|
ignoreAttributes: false,
|
||||||
attributeNamePrefix: "",
|
attributeNamePrefix: '',
|
||||||
attributesGroupName: 'attributes',
|
attributesGroupName: 'attributes',
|
||||||
suppressEmptyNode: true,
|
suppressEmptyNode: true,
|
||||||
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
|
tagValueProcessor: (tagName: string, tagValue: unknown): string | undefined => {
|
||||||
if (tagValue instanceof Date) {
|
if (tagValue instanceof Date) {
|
||||||
|
if (isNaN(tagValue.getTime())) {
|
||||||
|
return lastDate?.toISOString();
|
||||||
|
}
|
||||||
|
lastDate = tagValue;
|
||||||
return tagValue.toISOString();
|
return tagValue.toISOString();
|
||||||
}
|
}
|
||||||
return tagValue.toString();
|
return tagValue.toString();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
gpx.attributes.creator = gpx.attributes.creator ?? 'https://gpx.studio';
|
if (!gpx.attributes) gpx.attributes = {};
|
||||||
|
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
|
||||||
gpx.attributes['version'] = '1.1';
|
gpx.attributes['version'] = '1.1';
|
||||||
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
||||||
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
||||||
gpx.attributes['xsi:schemaLocation'] = 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
gpx.attributes['xsi:schemaLocation'] =
|
||||||
|
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
||||||
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
||||||
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
||||||
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
||||||
@@ -93,19 +150,24 @@ export function buildGPX(file: GPXFile, exclude: string[] = []): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder.build({
|
return builder.build({
|
||||||
"?xml": {
|
'?xml': {
|
||||||
attributes: {
|
attributes: {
|
||||||
version: "1.0",
|
version: '1.0',
|
||||||
encoding: "UTF-8",
|
encoding: 'UTF-8',
|
||||||
}
|
|
||||||
},
|
},
|
||||||
gpx: removeEmptyElements(gpx)
|
},
|
||||||
|
gpx: removeEmptyElements(gpx),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) {
|
if (
|
||||||
|
obj[key] === null ||
|
||||||
|
obj[key] === undefined ||
|
||||||
|
obj[key] === '' ||
|
||||||
|
(Array.isArray(obj[key]) && obj[key].length === 0)
|
||||||
|
) {
|
||||||
delete obj[key];
|
delete obj[key];
|
||||||
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
||||||
removeEmptyElements(obj[key]);
|
removeEmptyElements(obj[key]);
|
||||||
|
|||||||
@@ -1,33 +1,48 @@
|
|||||||
import { TrackPoint } from "./gpx";
|
import { TrackPoint } from './gpx';
|
||||||
import { Coordinates } from "./types";
|
import { Coordinates } from './types';
|
||||||
|
|
||||||
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
|
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
|
|
||||||
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] {
|
export function ramerDouglasPeucker(
|
||||||
|
points: TrackPoint[],
|
||||||
|
epsilon: number = 50,
|
||||||
|
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
|
||||||
|
): SimplifiedTrackPoint[] {
|
||||||
if (points.length == 0) {
|
if (points.length == 0) {
|
||||||
return [];
|
return [];
|
||||||
} else if (points.length == 1) {
|
} else if (points.length == 1) {
|
||||||
return [{
|
return [
|
||||||
point: points[0]
|
{
|
||||||
}];
|
point: points[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
let simplified = [{
|
let simplified = [
|
||||||
point: points[0]
|
{
|
||||||
}];
|
point: points[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
|
||||||
simplified.push({
|
simplified.push({
|
||||||
point: points[points.length - 1]
|
point: points[points.length - 1],
|
||||||
});
|
});
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
|
function ramerDouglasPeuckerRecursive(
|
||||||
|
points: TrackPoint[],
|
||||||
|
epsilon: number,
|
||||||
|
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
simplified: SimplifiedTrackPoint[]
|
||||||
|
) {
|
||||||
let largest = {
|
let largest = {
|
||||||
index: 0,
|
index: 0,
|
||||||
distance: 0
|
distance: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = start + 1; i < end; i++) {
|
for (let i = start + 1; i < end; i++) {
|
||||||
@@ -45,8 +60,16 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, mea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number {
|
export function crossarcDistance(
|
||||||
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
point1: TrackPoint,
|
||||||
|
point2: TrackPoint,
|
||||||
|
point3: TrackPoint | Coordinates
|
||||||
|
): number {
|
||||||
|
return crossarc(
|
||||||
|
point1.getCoordinates(),
|
||||||
|
point2.getCoordinates(),
|
||||||
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
||||||
@@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
// Is relative bearing obtuse?
|
||||||
if (diff > (Math.PI / 2)) {
|
if (diff > Math.PI / 2) {
|
||||||
return dis13;
|
return dis13;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
|||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Is p4 beyond the arc?
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||||
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
let dis14 =
|
||||||
|
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||||
if (dis14 > dis12) {
|
if (dis14 > dis12) {
|
||||||
return distance(lat2, lon2, lat3, lon3);
|
return distance(lat2, lon2, lat3, lon3);
|
||||||
} else {
|
} else {
|
||||||
@@ -93,18 +117,32 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
|
|||||||
|
|
||||||
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
// Finds the distance between two lat / lon points.
|
// Finds the distance between two lat / lon points.
|
||||||
return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius;
|
return (
|
||||||
|
Math.acos(
|
||||||
|
Math.sin(latA) * Math.sin(latB) +
|
||||||
|
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||||
|
) * earthRadius
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
// Finds the bearing from one lat / lon point to another.
|
// Finds the bearing from one lat / lon point to another.
|
||||||
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
|
return Math.atan2(
|
||||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
|
Math.sin(lonB - lonA) * Math.cos(latB),
|
||||||
|
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectedPoint(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): Coordinates {
|
export function projectedPoint(
|
||||||
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
point1: TrackPoint,
|
||||||
|
point2: TrackPoint,
|
||||||
|
point3: TrackPoint | Coordinates
|
||||||
|
): Coordinates {
|
||||||
|
return projected(
|
||||||
|
point1.getCoordinates(),
|
||||||
|
point2.getCoordinates(),
|
||||||
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||||
@@ -132,7 +170,7 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
// Is relative bearing obtuse?
|
||||||
if (diff > (Math.PI / 2)) {
|
if (diff > Math.PI / 2) {
|
||||||
return coord1;
|
return coord1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,14 +179,22 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
|
|||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Is p4 beyond the arc?
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||||
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
let dis14 =
|
||||||
|
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||||
if (dis14 > dis12) {
|
if (dis14 > dis12) {
|
||||||
return coord2;
|
return coord2;
|
||||||
} else {
|
} else {
|
||||||
// Determine the closest point (p4) on the great circle
|
// Determine the closest point (p4) on the great circle
|
||||||
const f = dis14 / earthRadius;
|
const f = dis14 / earthRadius;
|
||||||
const lat4 = Math.asin(Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12));
|
const lat4 = Math.asin(
|
||||||
const lon4 = lon1 + Math.atan2(Math.sin(bear12) * Math.sin(f) * Math.cos(lat1), Math.cos(f) - Math.sin(lat1) * Math.sin(lat4));
|
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
|
||||||
|
);
|
||||||
|
const lon4 =
|
||||||
|
lon1 +
|
||||||
|
Math.atan2(
|
||||||
|
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
|
||||||
|
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
|
||||||
|
);
|
||||||
|
|
||||||
return { lat: lat4 / rad, lon: lon4 / rad };
|
return { lat: lat4 / rad, lon: lon4 / rad };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export type TrackExtensions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type LineStyleExtension = {
|
export type LineStyleExtension = {
|
||||||
color?: string;
|
'gpx_style:color'?: string;
|
||||||
opacity?: number;
|
'gpx_style:opacity'?: number;
|
||||||
weight?: number;
|
'gpx_style:width'?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackSegmentType = {
|
export type TrackSegmentType = {
|
||||||
@@ -92,14 +92,12 @@ export type TrackPointExtension = {
|
|||||||
'gpxtpx:atemp'?: number;
|
'gpxtpx:atemp'?: number;
|
||||||
'gpxtpx:hr'?: number;
|
'gpxtpx:hr'?: number;
|
||||||
'gpxtpx:cad'?: number;
|
'gpxtpx:cad'?: number;
|
||||||
'gpxtpx:Extensions'?: {
|
'gpxtpx:Extensions'?: Record<string, string>;
|
||||||
surface?: string;
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export type PowerExtension = {
|
export type PowerExtension = {
|
||||||
'gpxpx:PowerInWatts'?: number;
|
'gpxpx:PowerInWatts'?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Author = {
|
export type Author = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -116,12 +114,12 @@ export type RouteType = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
extensions?: TrackExtensions;
|
extensions?: TrackExtensions;
|
||||||
rtept: WaypointType[];
|
rtept: WaypointType[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type RoutePointExtension = {
|
export type RoutePointExtension = {
|
||||||
'gpxx:rpt'?: GPXXRoutePoint[];
|
'gpxx:rpt'?: GPXXRoutePoint[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GPXXRoutePoint = {
|
export type GPXXRoutePoint = {
|
||||||
attributes: Coordinates;
|
attributes: Coordinates;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
<type>Cycling</type>
|
<type>Cycling</type>
|
||||||
<extensions>
|
<extensions>
|
||||||
<gpx_style:line>
|
<gpx_style:line>
|
||||||
<color>#2d3ee9</color>
|
<gpx_style:color>2d3ee9</gpx_style:color>
|
||||||
<opacity>0.5</opacity>
|
<gpx_style:opacity>0.5</gpx_style:opacity>
|
||||||
<weight>6</weight>
|
<gpx_style:width>6</gpx_style:width>
|
||||||
</gpx_style:line>
|
</gpx_style:line>
|
||||||
</extensions>
|
</extensions>
|
||||||
<trkseg>
|
<trkseg>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
"target": "ES2015",
|
"target": "ES2015",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
@@ -5,27 +5,27 @@ module.exports = {
|
|||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:svelte/recommended',
|
'plugin:svelte/recommended',
|
||||||
'prettier'
|
'prettier',
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
extraFileExtensions: ['.svelte']
|
extraFileExtensions: ['.svelte'],
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2017: true,
|
es2017: true,
|
||||||
node: true
|
node: true,
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.svelte'],
|
files: ['*.svelte'],
|
||||||
parser: 'svelte-eslint-parser',
|
parser: 'svelte-eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@typescript-eslint/parser'
|
parser: '@typescript-eslint/parser',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
src/lib/components/ui
|
||||||
|
*.mdx
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
1944
website/package-lock.json
generated
1944
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"prebuild": "npx tsx src/lib/pwa-manifest.ts",
|
||||||
"postbuild": "npx tsx src/lib/sitemap.ts",
|
"postbuild": "npx tsx src/lib/sitemap.ts",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@types/eslint": "^8.56.12",
|
"@types/eslint": "^8.56.12",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/mapbox__tilebelt": "^1.0.4",
|
"@types/mapbox__tilebelt": "^1.0.4",
|
||||||
"@types/mapbox-gl": "^3.4.0",
|
"@types/mapbox-gl": "^3.4.0",
|
||||||
"@types/node": "^20.16.10",
|
"@types/node": "^20.16.10",
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
"eslint-plugin-svelte": "^2.44.1",
|
"eslint-plugin-svelte": "^2.44.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"glob": "^10.4.5",
|
"glob": "^10.4.5",
|
||||||
"mdsvex": "^0.11.2",
|
"mdsvex": "^0.12.6",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.7",
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
@@ -61,17 +62,18 @@
|
|||||||
"chartjs-plugin-zoom": "^2.0.1",
|
"chartjs-plugin-zoom": "^2.0.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-static": "^0.427.0",
|
"jszip": "^3.10.1",
|
||||||
"lucide-svelte": "^0.427.0",
|
"lucide-static": "^0.460.0",
|
||||||
"mapbox-gl": "^3.7.0",
|
"lucide-svelte": "^0.460.1",
|
||||||
|
"mapbox-gl": "^3.11.1",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
"mode-watcher": "^0.3.1",
|
"mode-watcher": "^0.3.1",
|
||||||
"png.js": "^0.2.1",
|
"png.js": "^0.2.1",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"sortablejs": "^1.15.3",
|
"sortablejs": "^1.15.3",
|
||||||
"svelte-i18n": "^4.0.0",
|
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-variants": "^0.2.1"
|
"tailwind-variants": "^0.2.1"
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ export default {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -38,31 +38,18 @@ export async function handle({ event, resolve }) {
|
|||||||
<meta name="twitter:url" content="https://gpx.studio/" />
|
<meta name="twitter:url" content="https://gpx.studio/" />
|
||||||
<meta name="twitter:site" content="@gpxstudio" />
|
<meta name="twitter:site" content="@gpxstudio" />
|
||||||
<meta name="twitter:creator" content="@gpxstudio" />
|
<meta name="twitter:creator" content="@gpxstudio" />
|
||||||
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />`;
|
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />
|
||||||
|
<link rel="manifest" href="/${language}.manifest.webmanifest" />`;
|
||||||
|
|
||||||
for (let lang of Object.keys(languages)) {
|
for (let lang of Object.keys(languages)) {
|
||||||
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" />
|
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" />
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringsHTML = page === 'app' ? stringsToHTML(strings) : '';
|
|
||||||
|
|
||||||
const response = await resolve(event, {
|
const response = await resolve(event, {
|
||||||
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag).replace('</body>', `<div class="fixed -z-10 text-transparent">${stringsHTML}</div></body>`)
|
transformPageChunk: ({ html }) =>
|
||||||
|
html.replace('<html>', htmlTag).replace('<head>', headTag),
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringsToHTML(dictionary, strings = new Set(), root = true) {
|
|
||||||
Object.values(dictionary).forEach((value) => {
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
stringsToHTML(value, strings, false);
|
|
||||||
} else {
|
|
||||||
strings.add(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (root) {
|
|
||||||
return Array.from(strings).map((string) => `<p>${string}</p>`).join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
171
website/src/lib/assets/colors.ts
Normal file
171
website/src/lib/assets/colors.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSurfaceColor(surface: string): string {
|
||||||
|
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.fillStyle = backgroundColor;
|
||||||
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
|
||||||
|
const halfSize = size / 2;
|
||||||
|
const halfLineWidth = lineWidth / 2;
|
||||||
|
if (sacScaleColor) {
|
||||||
|
ctx.strokeStyle = sacScaleColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
|
||||||
|
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
|
||||||
|
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
if (mtbScaleColor) {
|
||||||
|
ctx.strokeStyle = mtbScaleColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(halfSize - halfLineWidth, size + halfLineWidth);
|
||||||
|
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
|
||||||
|
ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx?.createPattern(canvas, 'repeat') || backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns: Record<string, string | CanvasPattern> = {};
|
||||||
|
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('-')}`;
|
||||||
|
if (!patterns[patternId]) {
|
||||||
|
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
|
||||||
|
}
|
||||||
|
return patterns[patternId];
|
||||||
|
}
|
||||||
|
return backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSlope = 20;
|
||||||
|
export function getSlopeColor(slope: number): string {
|
||||||
|
if (slope > maxSlope) {
|
||||||
|
slope = maxSlope;
|
||||||
|
} else if (slope < -maxSlope) {
|
||||||
|
slope = -maxSlope;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = slope / maxSlope;
|
||||||
|
v = 1 / (1 + Math.exp(-6 * v));
|
||||||
|
v = v - 0.5;
|
||||||
|
|
||||||
|
let hue = ((0.5 - v) * 120).toString(10);
|
||||||
|
let lightness = 90 - Math.abs(v) * 70;
|
||||||
|
|
||||||
|
return `hsl(${hue},70%,${lightness}%)`;
|
||||||
|
}
|
||||||
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
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.0 MiB |
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
|||||||
export const surfaceColors: { [key: string]: string } = {
|
|
||||||
'missing': '#d1d1d1',
|
|
||||||
'paved': '#8c8c8c',
|
|
||||||
'unpaved': '#6b443a',
|
|
||||||
'asphalt': '#8c8c8c',
|
|
||||||
'concrete': '#8c8c8c',
|
|
||||||
'chipseal': '#8c8c8c',
|
|
||||||
'cobblestone': '#ffd991',
|
|
||||||
'unhewn_cobblestone': '#ffd991',
|
|
||||||
'paving_stones': '#8c8c8c',
|
|
||||||
'stepping_stones': '#c7b2db',
|
|
||||||
'sett': '#ffd991',
|
|
||||||
'metal': '#8c8c8c',
|
|
||||||
'wood': '#6b443a',
|
|
||||||
'compacted': '#ffffa8',
|
|
||||||
'fine_gravel': '#ffffa8',
|
|
||||||
'gravel': '#ffffa8',
|
|
||||||
'pebblestone': '#ffffa8',
|
|
||||||
'rock': '#ffd991',
|
|
||||||
'dirt': '#ffffa8',
|
|
||||||
'ground': '#6b443a',
|
|
||||||
'earth': '#6b443a',
|
|
||||||
'snow': '#bdfffc',
|
|
||||||
'ice': '#bdfffc',
|
|
||||||
'salt': '#b6c0f2',
|
|
||||||
'mud': '#6b443a',
|
|
||||||
'sand': '#ffffc4',
|
|
||||||
'woodchips': '#6b443a',
|
|
||||||
'grass': '#61b55c',
|
|
||||||
'grass_paver': '#61b55c'
|
|
||||||
}
|
|
||||||
@@ -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 } from "lucide-svelte";
|
import {
|
||||||
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg } from "lucide-static";
|
Landmark,
|
||||||
import type { ComponentType } from "svelte";
|
Icon,
|
||||||
|
Shell,
|
||||||
|
Bike,
|
||||||
|
Building,
|
||||||
|
Tent,
|
||||||
|
Car,
|
||||||
|
Wrench,
|
||||||
|
ShoppingBasket,
|
||||||
|
Droplet,
|
||||||
|
DoorOpen,
|
||||||
|
Trees,
|
||||||
|
Fuel,
|
||||||
|
Home,
|
||||||
|
Info,
|
||||||
|
TreeDeciduous,
|
||||||
|
CircleParking,
|
||||||
|
Cross,
|
||||||
|
Utensils,
|
||||||
|
Construction,
|
||||||
|
BrickWall,
|
||||||
|
ShowerHead,
|
||||||
|
Mountain,
|
||||||
|
Phone,
|
||||||
|
TrainFront,
|
||||||
|
Bed,
|
||||||
|
Binoculars,
|
||||||
|
TriangleAlert,
|
||||||
|
Anchor,
|
||||||
|
Toilet,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import {
|
||||||
|
Landmark as LandmarkSvg,
|
||||||
|
Shell as ShellSvg,
|
||||||
|
Bike as BikeSvg,
|
||||||
|
Building as BuildingSvg,
|
||||||
|
Tent as TentSvg,
|
||||||
|
Car as CarSvg,
|
||||||
|
Wrench as WrenchSvg,
|
||||||
|
ShoppingBasket as ShoppingBasketSvg,
|
||||||
|
Droplet as DropletSvg,
|
||||||
|
DoorOpen as DoorOpenSvg,
|
||||||
|
Trees as TreesSvg,
|
||||||
|
Fuel as FuelSvg,
|
||||||
|
Home as HomeSvg,
|
||||||
|
Info as InfoSvg,
|
||||||
|
TreeDeciduous as TreeDeciduousSvg,
|
||||||
|
CircleParking as CircleParkingSvg,
|
||||||
|
Cross as CrossSvg,
|
||||||
|
Utensils as UtensilsSvg,
|
||||||
|
Construction as ConstructionSvg,
|
||||||
|
BrickWall as BrickWallSvg,
|
||||||
|
ShowerHead as ShowerHeadSvg,
|
||||||
|
Mountain as MountainSvg,
|
||||||
|
Phone as PhoneSvg,
|
||||||
|
TrainFront as TrainFrontSvg,
|
||||||
|
Bed as BedSvg,
|
||||||
|
Binoculars as BinocularsSvg,
|
||||||
|
TriangleAlert as TriangleAlertSvg,
|
||||||
|
Anchor as AnchorSvg,
|
||||||
|
Toilet as ToiletSvg,
|
||||||
|
} from 'lucide-static';
|
||||||
|
import type { ComponentType } from 'svelte';
|
||||||
|
|
||||||
export type Symbol = {
|
export type Symbol = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
|
|||||||
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
||||||
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
||||||
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
|
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
|
||||||
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
convenience_store: {
|
||||||
|
value: 'Convenience Store',
|
||||||
|
icon: ShoppingBasket,
|
||||||
|
iconSvg: ShoppingBasketSvg,
|
||||||
|
},
|
||||||
crossing: { value: 'Crossing' },
|
crossing: { value: 'Crossing' },
|
||||||
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
department_store: {
|
||||||
|
value: 'Department Store',
|
||||||
|
icon: ShoppingBasket,
|
||||||
|
iconSvg: ShoppingBasketSvg,
|
||||||
|
},
|
||||||
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
|
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
|
||||||
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
||||||
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
||||||
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
||||||
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
||||||
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
|
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
|
||||||
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg },
|
ground_transportation: {
|
||||||
|
value: 'Ground Transportation',
|
||||||
|
icon: TrainFront,
|
||||||
|
iconSvg: TrainFrontSvg,
|
||||||
|
},
|
||||||
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
|
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
|
||||||
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
||||||
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
||||||
@@ -39,7 +112,7 @@ export const symbols: { [key: string]: Symbol } = {
|
|||||||
picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
|
picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
|
||||||
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
|
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
|
||||||
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg },
|
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg },
|
||||||
restroom: { value: 'Restroom' },
|
restroom: { value: 'Restroom', icon: Toilet, iconSvg: ToiletSvg },
|
||||||
road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
|
road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
|
||||||
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
|
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
|
||||||
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
|
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
|
||||||
@@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
|
|||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
return Object.keys(symbols).find(key => symbols[key].value === value);
|
return Object.keys(symbols).find((key) => symbols[key].value === value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import docsearch from '@docsearch/js';
|
import docsearch from '@docsearch/js';
|
||||||
import '@docsearch/css';
|
import '@docsearch/css';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { _, locale, waitLocale } from 'svelte-i18n';
|
import { _, locale, isLoadingLocale } from '$lib/i18n';
|
||||||
|
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
indexName: 'gpx',
|
indexName: 'gpx',
|
||||||
container: '#docsearch',
|
container: '#docsearch',
|
||||||
searchParameters: {
|
searchParameters: {
|
||||||
facetFilters: ['lang:' + ($locale ?? 'en')]
|
facetFilters: ['lang:' + $locale],
|
||||||
},
|
},
|
||||||
placeholder: $_('docs.search.search'),
|
placeholder: $_('docs.search.search'),
|
||||||
disableUserPersonalization: true,
|
disableUserPersonalization: true,
|
||||||
translations: {
|
translations: {
|
||||||
button: {
|
button: {
|
||||||
buttonText: $_('docs.search.search'),
|
buttonText: $_('docs.search.search'),
|
||||||
buttonAriaLabel: $_('docs.search.search')
|
buttonAriaLabel: $_('docs.search.search'),
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
searchBox: {
|
searchBox: {
|
||||||
@@ -28,19 +28,19 @@
|
|||||||
resetButtonAriaLabel: $_('docs.search.clear'),
|
resetButtonAriaLabel: $_('docs.search.clear'),
|
||||||
cancelButtonText: $_('docs.search.cancel'),
|
cancelButtonText: $_('docs.search.cancel'),
|
||||||
cancelButtonAriaLabel: $_('docs.search.cancel'),
|
cancelButtonAriaLabel: $_('docs.search.cancel'),
|
||||||
searchInputLabel: $_('docs.search.search')
|
searchInputLabel: $_('docs.search.search'),
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
selectText: $_('docs.search.to_select'),
|
selectText: $_('docs.search.to_select'),
|
||||||
navigateText: $_('docs.search.to_navigate'),
|
navigateText: $_('docs.search.to_navigate'),
|
||||||
closeText: $_('docs.search.to_close')
|
closeText: $_('docs.search.to_close'),
|
||||||
},
|
},
|
||||||
noResultsScreen: {
|
noResultsScreen: {
|
||||||
noResultsText: $_('docs.search.no_results'),
|
noResultsText: $_('docs.search.no_results'),
|
||||||
suggestedQueryText: $_('docs.search.no_results_suggestion')
|
suggestedQueryText: $_('docs.search.no_results_suggestion'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
mounted = true;
|
mounted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (mounted && $locale) {
|
$: if (mounted && $locale && !$isLoadingLocale) {
|
||||||
waitLocale().then(initDocsearch);
|
initDocsearch();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
import type { Builder } from 'bits-ui';
|
||||||
|
|
||||||
export let variant:
|
export let variant:
|
||||||
| 'default'
|
| 'default'
|
||||||
@@ -12,11 +13,12 @@
|
|||||||
| undefined = 'default';
|
| undefined = 'default';
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||||
|
export let builders: Builder[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild let:builder>
|
<Tooltip.Trigger asChild let:builder>
|
||||||
<Button builders={[builder]} {variant} {...$$restProps}>
|
<Button builders={[...builders, builder]} {variant} {...$$restProps} on:click>
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|||||||
18
website/src/lib/components/CoordinatesPopup.svelte
Normal file
18
website/src/lib/components/CoordinatesPopup.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { trackpointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
||||||
|
import { TrackPoint } from 'gpx';
|
||||||
|
|
||||||
|
$: if ($map) {
|
||||||
|
$map.on('contextmenu', (e) => {
|
||||||
|
trackpointPopup?.setItem({
|
||||||
|
item: new TrackPoint({
|
||||||
|
attributes: {
|
||||||
|
lat: e.lngLat.lat,
|
||||||
|
lon: e.lngLat.lng,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import * as ToggleGroup from '$lib/components/ui/toggle-group';
|
import * as ToggleGroup from '$lib/components/ui/toggle-group';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
@@ -12,12 +13,15 @@
|
|||||||
Orbit,
|
Orbit,
|
||||||
SquareActivity,
|
SquareActivity,
|
||||||
Thermometer,
|
Thermometer,
|
||||||
Zap
|
Zap,
|
||||||
|
Circle,
|
||||||
|
Check,
|
||||||
|
ChartNoAxesColumn,
|
||||||
|
Construction,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { surfaceColors } from '$lib/assets/surfaces';
|
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, df } from '$lib/i18n';
|
||||||
import {
|
import {
|
||||||
getCadenceUnits,
|
|
||||||
getCadenceWithUnits,
|
getCadenceWithUnits,
|
||||||
getConvertedDistance,
|
getConvertedDistance,
|
||||||
getConvertedElevation,
|
getConvertedElevation,
|
||||||
@@ -26,45 +30,25 @@
|
|||||||
getDistanceUnits,
|
getDistanceUnits,
|
||||||
getDistanceWithUnits,
|
getDistanceWithUnits,
|
||||||
getElevationWithUnits,
|
getElevationWithUnits,
|
||||||
getHeartRateUnits,
|
|
||||||
getHeartRateWithUnits,
|
getHeartRateWithUnits,
|
||||||
getPowerUnits,
|
|
||||||
getPowerWithUnits,
|
getPowerWithUnits,
|
||||||
getTemperatureUnits,
|
|
||||||
getTemperatureWithUnits,
|
getTemperatureWithUnits,
|
||||||
getVelocityUnits,
|
|
||||||
getVelocityWithUnits,
|
getVelocityWithUnits,
|
||||||
secondsToHHMMSS
|
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import { DateFormatter } from '@internationalized/date';
|
|
||||||
import type { GPXStatistics } from 'gpx';
|
import type { GPXStatistics } from 'gpx';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
export let gpxStatistics: Writable<GPXStatistics>;
|
export let gpxStatistics: Writable<GPXStatistics>;
|
||||||
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||||
export let panelSize: number;
|
|
||||||
export let additionalDatasets: string[];
|
export let additionalDatasets: string[];
|
||||||
export let elevationFill: 'slope' | 'surface' | undefined;
|
export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
|
||||||
export let showControls: boolean = true;
|
export let showControls: boolean = true;
|
||||||
|
|
||||||
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||||
|
|
||||||
let df: DateFormatter;
|
|
||||||
|
|
||||||
$: if ($locale) {
|
|
||||||
df = new DateFormatter($locale, {
|
|
||||||
dateStyle: 'medium',
|
|
||||||
timeStyle: 'medium'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let showAdditionalScales = true;
|
|
||||||
let updateShowAdditionalScales = () => {
|
|
||||||
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
|
|
||||||
};
|
|
||||||
let overlay: HTMLCanvasElement;
|
let overlay: HTMLCanvasElement;
|
||||||
let chart: Chart;
|
let chart: Chart;
|
||||||
|
|
||||||
@@ -83,42 +67,41 @@
|
|||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number, index: number, ticks: { value: number }[]) {
|
callback: function (value: number) {
|
||||||
if (index === ticks.length - 1) {
|
|
||||||
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
|
|
||||||
}
|
|
||||||
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||||
}
|
},
|
||||||
}
|
align: 'inner',
|
||||||
|
maxRotation: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number) {
|
callback: function (value: number) {
|
||||||
return getElevationWithUnits(value, false);
|
return getElevationWithUnits(value, false);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
datasets: {
|
datasets: {
|
||||||
line: {
|
line: {
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
cubicInterpolationMode: 'monotone'
|
cubicInterpolationMode: 'monotone',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
interaction: {
|
interaction: {
|
||||||
mode: 'nearest',
|
mode: 'nearest',
|
||||||
axis: 'x',
|
axis: 'x',
|
||||||
intersect: false
|
intersect: false,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false,
|
||||||
},
|
},
|
||||||
decimation: {
|
decimation: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: () => !dragging && !panning,
|
enabled: () => !dragging && !panning,
|
||||||
@@ -157,13 +140,20 @@
|
|||||||
let slope = {
|
let slope = {
|
||||||
at: point.slope.at.toFixed(1),
|
at: point.slope.at.toFixed(1),
|
||||||
segment: point.slope.segment.toFixed(1),
|
segment: point.slope.segment.toFixed(1),
|
||||||
length: getDistanceWithUnits(point.slope.length)
|
length: getDistanceWithUnits(point.slope.length),
|
||||||
};
|
};
|
||||||
let surface = point.surface ? point.surface : '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 = [
|
let labels = [
|
||||||
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
||||||
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`
|
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (elevationFill === 'surface') {
|
if (elevationFill === 'surface') {
|
||||||
@@ -172,13 +162,26 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elevationFill === 'highway') {
|
||||||
|
labels.push(
|
||||||
|
` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${
|
||||||
|
sacScale
|
||||||
|
? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
if (mtbScale) {
|
||||||
|
labels.push(` ${$_('toolbar.routing.mtb_scale')}: ${mtbScale}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (point.time) {
|
if (point.time) {
|
||||||
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
|
labels.push(` ${$_('quantities.time')}: ${$df.format(point.time)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: {
|
pan: {
|
||||||
@@ -192,18 +195,19 @@
|
|||||||
},
|
},
|
||||||
onPanComplete: function () {
|
onPanComplete: function () {
|
||||||
panning = false;
|
panning = false;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
wheel: {
|
wheel: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
},
|
},
|
||||||
mode: 'x',
|
mode: 'x',
|
||||||
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
|
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
|
||||||
if (
|
if (
|
||||||
event.deltaY < 0 &&
|
event.deltaY < 0 &&
|
||||||
Math.abs(
|
Math.abs(
|
||||||
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange -
|
chart.getInitialScaleBounds().x.max /
|
||||||
|
chart.options.plugins.zoom.limits.x.minRange -
|
||||||
chart.getZoomLevel()
|
chart.getZoomLevel()
|
||||||
) < 0.01
|
) < 0.01
|
||||||
) {
|
) {
|
||||||
@@ -212,86 +216,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$slicedGPXStatistics = undefined;
|
$slicedGPXStatistics = undefined;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
limits: {
|
limits: {
|
||||||
x: {
|
x: {
|
||||||
min: 'original',
|
min: 'original',
|
||||||
max: 'original',
|
max: 'original',
|
||||||
minRange: 1
|
minRange: 1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
stacked: false,
|
stacked: false,
|
||||||
onResize: function () {
|
onResize: function () {
|
||||||
updateOverlay();
|
updateOverlay();
|
||||||
updateShowAdditionalScales();
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let datasets: {
|
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||||
[key: string]: {
|
datasets.forEach((id) => {
|
||||||
id: string;
|
|
||||||
getLabel: () => string;
|
|
||||||
getUnits: () => string;
|
|
||||||
};
|
|
||||||
} = {
|
|
||||||
speed: {
|
|
||||||
id: 'speed',
|
|
||||||
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
|
|
||||||
getUnits: () => getVelocityUnits()
|
|
||||||
},
|
|
||||||
hr: {
|
|
||||||
id: 'hr',
|
|
||||||
getLabel: () => $_('quantities.heartrate'),
|
|
||||||
getUnits: () => getHeartRateUnits()
|
|
||||||
},
|
|
||||||
cad: {
|
|
||||||
id: 'cad',
|
|
||||||
getLabel: () => $_('quantities.cadence'),
|
|
||||||
getUnits: () => getCadenceUnits()
|
|
||||||
},
|
|
||||||
atemp: {
|
|
||||||
id: 'atemp',
|
|
||||||
getLabel: () => $_('quantities.temperature'),
|
|
||||||
getUnits: () => getTemperatureUnits()
|
|
||||||
},
|
|
||||||
power: {
|
|
||||||
id: 'power',
|
|
||||||
getLabel: () => $_('quantities.power'),
|
|
||||||
getUnits: () => getPowerUnits()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let [id, dataset] of Object.entries(datasets)) {
|
|
||||||
options.scales[`y${id}`] = {
|
options.scales[`y${id}`] = {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
|
|
||||||
padding: {
|
|
||||||
top: 6,
|
|
||||||
bottom: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
grid: {
|
||||||
display: false
|
display: false,
|
||||||
},
|
},
|
||||||
reverse: () => id === 'speed' && $velocityUnits === 'pace',
|
reverse: () => id === 'speed' && $velocityUnits === 'pace',
|
||||||
display: false
|
display: false,
|
||||||
};
|
|
||||||
}
|
|
||||||
options.scales.yspeed['ticks'] = {
|
|
||||||
callback: function (value: number) {
|
|
||||||
if ($velocityUnits === 'speed') {
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return secondsToHHMMSS(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
|
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
|
||||||
@@ -299,7 +252,7 @@
|
|||||||
chart = new Chart(canvas, {
|
chart = new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: []
|
datasets: [],
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -312,20 +265,18 @@
|
|||||||
marker.remove();
|
marker.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map marker to show on hover
|
// Map marker to show on hover
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
||||||
marker = new mapboxgl.Marker({
|
marker = new mapboxgl.Marker({
|
||||||
element
|
element,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateShowAdditionalScales();
|
|
||||||
|
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
function getIndex(evt) {
|
function getIndex(evt) {
|
||||||
@@ -333,7 +284,7 @@
|
|||||||
evt,
|
evt,
|
||||||
'x',
|
'x',
|
||||||
{
|
{
|
||||||
intersect: false
|
intersect: false,
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -376,9 +327,12 @@
|
|||||||
startIndex = endIndex;
|
startIndex = endIndex;
|
||||||
} else if (startIndex !== endIndex) {
|
} else if (startIndex !== endIndex) {
|
||||||
$slicedGPXStatistics = [
|
$slicedGPXStatistics = [
|
||||||
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
|
$gpxStatistics.slice(
|
||||||
Math.min(startIndex, endIndex),
|
Math.min(startIndex, endIndex),
|
||||||
Math.max(startIndex, endIndex)
|
Math.max(startIndex, endIndex)
|
||||||
|
),
|
||||||
|
Math.min(startIndex, endIndex),
|
||||||
|
Math.max(startIndex, endIndex),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,126 +366,111 @@
|
|||||||
slope: {
|
slope: {
|
||||||
at: data.local.slope.at[index],
|
at: data.local.slope.at[index],
|
||||||
segment: data.local.slope.segment[index],
|
segment: data.local.slope.segment[index],
|
||||||
length: data.local.slope.length[index]
|
length: data.local.slope.length[index],
|
||||||
},
|
},
|
||||||
surface: point.getSurface(),
|
extensions: point.getExtensions(),
|
||||||
coordinates: point.getCoordinates(),
|
coordinates: point.getCoordinates(),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
fill: 'start',
|
fill: 'start',
|
||||||
order: 1
|
order: 1,
|
||||||
};
|
};
|
||||||
chart.data.datasets[1] = {
|
chart.data.datasets[1] = {
|
||||||
label: datasets.speed.getLabel(),
|
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedVelocity(data.local.speed[index]),
|
y: getConvertedVelocity(data.local.speed[index]),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: `y${datasets.speed.id}`,
|
yAxisID: 'yspeed',
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
chart.data.datasets[2] = {
|
chart.data.datasets[2] = {
|
||||||
label: datasets.hr.getLabel(),
|
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getHeartRate(),
|
y: point.getHeartRate(),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: `y${datasets.hr.id}`,
|
yAxisID: 'yhr',
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
chart.data.datasets[3] = {
|
chart.data.datasets[3] = {
|
||||||
label: datasets.cad.getLabel(),
|
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getCadence(),
|
y: point.getCadence(),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: `y${datasets.cad.id}`,
|
yAxisID: 'ycad',
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
chart.data.datasets[4] = {
|
chart.data.datasets[4] = {
|
||||||
label: datasets.atemp.getLabel(),
|
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: getConvertedTemperature(point.getTemperature()),
|
y: getConvertedTemperature(point.getTemperature()),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: `y${datasets.atemp.id}`,
|
yAxisID: 'yatemp',
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
chart.data.datasets[5] = {
|
chart.data.datasets[5] = {
|
||||||
label: datasets.power.getLabel(),
|
|
||||||
data: data.local.points.map((point, index) => {
|
data: data.local.points.map((point, index) => {
|
||||||
return {
|
return {
|
||||||
x: getConvertedDistance(data.local.distance.total[index]),
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
y: point.getPower(),
|
y: point.getPower(),
|
||||||
index: index
|
index: index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: `y${datasets.power.id}`,
|
yAxisID: 'ypower',
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
chart.options.scales.x['min'] = 0;
|
chart.options.scales.x['min'] = 0;
|
||||||
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||||
|
|
||||||
// update units
|
|
||||||
for (let [id, dataset] of Object.entries(datasets)) {
|
|
||||||
chart.options.scales[`y${id}`].title.text =
|
|
||||||
dataset.getLabel() + ' (' + dataset.getUnits() + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxSlope = 20;
|
|
||||||
function slopeFillCallback(context) {
|
function slopeFillCallback(context) {
|
||||||
let slope = context.p0.raw.slope.segment;
|
return getSlopeColor(context.p0.raw.slope.segment);
|
||||||
if (slope > maxSlope) {
|
|
||||||
slope = maxSlope;
|
|
||||||
} else if (slope < -maxSlope) {
|
|
||||||
slope = -maxSlope;
|
|
||||||
}
|
|
||||||
|
|
||||||
let v = slope / maxSlope;
|
|
||||||
v = 1 / (1 + Math.exp(-6 * v));
|
|
||||||
v = v - 0.5;
|
|
||||||
|
|
||||||
let hue = ((0.5 - v) * 120).toString(10);
|
|
||||||
let lightness = 90 - Math.abs(v) * 70;
|
|
||||||
|
|
||||||
return ['hsl(', hue, ',70%,', lightness, '%)'].join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function surfaceFillCallback(context) {
|
function surfaceFillCallback(context) {
|
||||||
let surface = context.p0.raw.surface;
|
return getSurfaceColor(context.p0.raw.extensions.surface);
|
||||||
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
|
}
|
||||||
|
|
||||||
|
function highwayFillCallback(context) {
|
||||||
|
return getHighwayColor(
|
||||||
|
context.p0.raw.extensions.highway,
|
||||||
|
context.p0.raw.extensions.sac_scale,
|
||||||
|
context.p0.raw.extensions.mtb_scale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (chart) {
|
$: if (chart) {
|
||||||
if (elevationFill === 'slope') {
|
if (elevationFill === 'slope') {
|
||||||
chart.data.datasets[0]['segment'] = {
|
chart.data.datasets[0]['segment'] = {
|
||||||
backgroundColor: slopeFillCallback
|
backgroundColor: slopeFillCallback,
|
||||||
};
|
};
|
||||||
} else if (elevationFill === 'surface') {
|
} else if (elevationFill === 'surface') {
|
||||||
chart.data.datasets[0]['segment'] = {
|
chart.data.datasets[0]['segment'] = {
|
||||||
backgroundColor: surfaceFillCallback
|
backgroundColor: surfaceFillCallback,
|
||||||
|
};
|
||||||
|
} else if (elevationFill === 'highway') {
|
||||||
|
chart.data.datasets[0]['segment'] = {
|
||||||
|
backgroundColor: highwayFillCallback,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
chart.data.datasets[0]['segment'] = {};
|
chart.data.datasets[0]['segment'] = {};
|
||||||
@@ -552,12 +491,6 @@
|
|||||||
chart.data.datasets[4].hidden = !includeTemperature;
|
chart.data.datasets[4].hidden = !includeTemperature;
|
||||||
chart.data.datasets[5].hidden = !includePower;
|
chart.data.datasets[5].hidden = !includePower;
|
||||||
}
|
}
|
||||||
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
|
|
||||||
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
|
|
||||||
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
|
|
||||||
chart.options.scales[`y${datasets.atemp.id}`].display =
|
|
||||||
includeTemperature && showAdditionalScales;
|
|
||||||
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
|
|
||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,6 +501,8 @@
|
|||||||
|
|
||||||
overlay.width = canvas.width / window.devicePixelRatio;
|
overlay.width = canvas.width / window.devicePixelRatio;
|
||||||
overlay.height = canvas.height / window.devicePixelRatio;
|
overlay.height = canvas.height / window.devicePixelRatio;
|
||||||
|
overlay.style.width = `${overlay.width}px`;
|
||||||
|
overlay.style.height = `${overlay.height}px`;
|
||||||
|
|
||||||
if ($slicedGPXStatistics) {
|
if ($slicedGPXStatistics) {
|
||||||
let startIndex = $slicedGPXStatistics[1];
|
let startIndex = $slicedGPXStatistics[1];
|
||||||
@@ -591,7 +526,7 @@
|
|||||||
startPixel,
|
startPixel,
|
||||||
chart.chartArea.top,
|
chart.chartArea.top,
|
||||||
endPixel - startPixel,
|
endPixel - startPixel,
|
||||||
chart.chartArea.bottom - chart.chartArea.top
|
chart.chartArea.height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (overlay) {
|
} else if (overlay) {
|
||||||
@@ -611,75 +546,141 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
|
<div class="h-full grow min-w-0 relative py-2">
|
||||||
<div class="grow h-full min-w-0 relative">
|
|
||||||
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
|
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
|
||||||
<canvas bind:this={canvas} class="w-full h-full"></canvas>
|
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
|
||||||
</div>
|
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
<div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
|
<div class="absolute bottom-10 right-1.5">
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild let:builder>
|
||||||
|
<ButtonWithTooltip
|
||||||
|
label={$_('chart.settings')}
|
||||||
|
builders={[builder]}
|
||||||
|
variant="outline"
|
||||||
|
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
|
||||||
|
>
|
||||||
|
<ChartNoAxesColumn size="18" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
class="w-fit p-0 flex flex-col divide-y"
|
||||||
|
side="top"
|
||||||
|
sideOffset={-32}
|
||||||
|
>
|
||||||
<ToggleGroup.Root
|
<ToggleGroup.Root
|
||||||
class="{panelSize > 158
|
class="flex flex-col items-start gap-0 p-1"
|
||||||
? 'flex-col'
|
|
||||||
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
|
|
||||||
type="single"
|
type="single"
|
||||||
bind:value={elevationFill}
|
bind:value={elevationFill}
|
||||||
>
|
>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope" aria-label={$_('chart.show_slope')}>
|
<ToggleGroup.Item
|
||||||
<Tooltip side="left" label={$_('chart.show_slope')}>
|
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"
|
||||||
<TriangleRight size="15" />
|
value="slope"
|
||||||
</Tooltip>
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if elevationFill === 'slope'}
|
||||||
|
<Circle class="h-1.5 w-1.5 fill-current text-current" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<TriangleRight size="15" class="mr-1" />
|
||||||
|
{$_('quantities.slope')}
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface" aria-label={$_('chart.show_surface')}>
|
<ToggleGroup.Item
|
||||||
<Tooltip side="left" label={$_('chart.show_surface')}>
|
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"
|
||||||
<BrickWall size="15" />
|
value="surface"
|
||||||
</Tooltip>
|
variant="outline"
|
||||||
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if elevationFill === 'surface'}
|
||||||
|
<Circle class="h-1.5 w-1.5 fill-current text-current" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<BrickWall size="15" class="mr-1" />
|
||||||
|
{$_('quantities.surface')}
|
||||||
|
</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"
|
||||||
|
value="highway"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if elevationFill === 'highway'}
|
||||||
|
<Circle class="h-1.5 w-1.5 fill-current text-current" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Construction size="15" class="mr-1" />
|
||||||
|
{$_('quantities.highway')}
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
</ToggleGroup.Root>
|
</ToggleGroup.Root>
|
||||||
<ToggleGroup.Root
|
<ToggleGroup.Root
|
||||||
class="{panelSize > 158
|
class="flex flex-col items-start gap-0 p-1"
|
||||||
? 'flex-col'
|
|
||||||
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
|
|
||||||
type="multiple"
|
type="multiple"
|
||||||
bind:value={additionalDatasets}
|
bind:value={additionalDatasets}
|
||||||
>
|
>
|
||||||
<ToggleGroup.Item
|
<ToggleGroup.Item
|
||||||
class="p-0 w-5 h-5"
|
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"
|
||||||
value="speed"
|
value="speed"
|
||||||
aria-label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
|
||||||
>
|
>
|
||||||
<Tooltip
|
<div class="w-6 flex justify-center items-center">
|
||||||
side="left"
|
{#if additionalDatasets.includes('speed')}
|
||||||
label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
<Check size="14" />
|
||||||
>
|
{/if}
|
||||||
<Zap size="15" />
|
</div>
|
||||||
</Tooltip>
|
<Zap size="15" class="mr-1" />
|
||||||
</ToggleGroup.Item>
|
{$velocityUnits === 'speed'
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr" aria-label={$_('chart.show_heartrate')}>
|
? $_('quantities.speed')
|
||||||
<Tooltip side="left" label={$_('chart.show_heartrate')}>
|
: $_('quantities.pace')}
|
||||||
<HeartPulse size="15" />
|
|
||||||
</Tooltip>
|
|
||||||
</ToggleGroup.Item>
|
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad" aria-label={$_('chart.show_cadence')}>
|
|
||||||
<Tooltip side="left" label={$_('chart.show_cadence')}>
|
|
||||||
<Orbit size="15" />
|
|
||||||
</Tooltip>
|
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item
|
<ToggleGroup.Item
|
||||||
class="p-0 w-5 h-5"
|
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"
|
||||||
value="atemp"
|
value="hr"
|
||||||
aria-label={$_('chart.show_temperature')}
|
|
||||||
>
|
>
|
||||||
<Tooltip side="left" label={$_('chart.show_temperature')}>
|
<div class="w-6 flex justify-center items-center">
|
||||||
<Thermometer size="15" />
|
{#if additionalDatasets.includes('hr')}
|
||||||
</Tooltip>
|
<Check size="14" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<HeartPulse size="15" class="mr-1" />
|
||||||
|
{$_('quantities.heartrate')}
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="power" aria-label={$_('chart.show_power')}>
|
<ToggleGroup.Item
|
||||||
<Tooltip side="left" label={$_('chart.show_power')}>
|
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"
|
||||||
<SquareActivity size="15" />
|
value="cad"
|
||||||
</Tooltip>
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if additionalDatasets.includes('cad')}
|
||||||
|
<Check size="14" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Orbit size="15" class="mr-1" />
|
||||||
|
{$_('quantities.cadence')}
|
||||||
|
</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"
|
||||||
|
value="atemp"
|
||||||
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if additionalDatasets.includes('atemp')}
|
||||||
|
<Check size="14" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Thermometer size="15" class="mr-1" />
|
||||||
|
{$_('quantities.temperature')}
|
||||||
|
</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"
|
||||||
|
value="power"
|
||||||
|
>
|
||||||
|
<div class="w-6 flex justify-center items-center">
|
||||||
|
{#if additionalDatasets.includes('power')}
|
||||||
|
<Check size="14" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<SquareActivity size="15" class="mr-1" />
|
||||||
|
{$_('quantities.power')}
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
</ToggleGroup.Root>
|
</ToggleGroup.Root>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
exportSelectedFiles,
|
exportSelectedFiles,
|
||||||
ExportState,
|
ExportState,
|
||||||
exportState,
|
exportState,
|
||||||
gpxStatistics
|
gpxStatistics,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
Zap,
|
Zap,
|
||||||
BrickWall,
|
Earth,
|
||||||
HeartPulse,
|
HeartPulse,
|
||||||
Orbit,
|
Orbit,
|
||||||
Thermometer,
|
Thermometer,
|
||||||
SquareActivity
|
SquareActivity,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { selection } from './file-list/Selection';
|
import { selection } from './file-list/Selection';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { GPXStatistics } from 'gpx';
|
import { GPXStatistics } from 'gpx';
|
||||||
@@ -31,19 +31,19 @@
|
|||||||
let open = false;
|
let open = false;
|
||||||
let exportOptions: Record<string, boolean> = {
|
let exportOptions: Record<string, boolean> = {
|
||||||
time: true,
|
time: true,
|
||||||
surface: true,
|
|
||||||
hr: true,
|
hr: true,
|
||||||
cad: true,
|
cad: true,
|
||||||
atemp: true,
|
atemp: true,
|
||||||
power: true
|
power: true,
|
||||||
|
extensions: true,
|
||||||
};
|
};
|
||||||
let hide: Record<string, boolean> = {
|
let hide: Record<string, boolean> = {
|
||||||
time: false,
|
time: false,
|
||||||
surface: false,
|
|
||||||
hr: false,
|
hr: false,
|
||||||
cad: false,
|
cad: false,
|
||||||
atemp: false,
|
atemp: false,
|
||||||
power: false
|
power: false,
|
||||||
|
extensions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if ($exportState !== ExportState.NONE) {
|
$: if ($exportState !== ExportState.NONE) {
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
hide.time = statistics.global.time.total === 0;
|
hide.time = statistics.global.time.total === 0;
|
||||||
hide.surface = !Object.keys(statistics.global.surface).some((key) => key !== 'unknown');
|
|
||||||
hide.hr = statistics.global.hr.count === 0;
|
hide.hr = statistics.global.hr.count === 0;
|
||||||
hide.cad = statistics.global.cad.count === 0;
|
hide.cad = statistics.global.cad.count === 0;
|
||||||
hide.atemp = statistics.global.atemp.count === 0;
|
hide.atemp = statistics.global.atemp.count === 0;
|
||||||
hide.power = statistics.global.power.count === 0;
|
hide.power = statistics.global.power.count === 0;
|
||||||
|
hide.extensions = Object.keys(statistics.global.extensions).length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
|
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
|
||||||
@@ -121,7 +121,9 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some((v) => !v)
|
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some(
|
||||||
|
(v) => !v
|
||||||
|
)
|
||||||
? ''
|
? ''
|
||||||
: 'hidden'}"
|
: 'hidden'}"
|
||||||
>
|
>
|
||||||
@@ -144,11 +146,13 @@
|
|||||||
{$_('quantities.time')}
|
{$_('quantities.time')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
|
<div
|
||||||
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
|
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
|
||||||
<Label for="export-surface" class="flex flex-row items-center gap-1">
|
>
|
||||||
<BrickWall size="16" />
|
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
|
||||||
{$_('quantities.surface')}
|
<Label for="export-extensions" class="flex flex-row items-center gap-1">
|
||||||
|
<Earth size="16" />
|
||||||
|
{$_('quantities.osm_extensions')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
|
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
|
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte';
|
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import type { GPXStatistics } from 'gpx';
|
import type { GPXStatistics } from 'gpx';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
class="h-full {orientation === 'vertical'
|
class="h-full {orientation === 'vertical'
|
||||||
? 'min-w-44 sm:min-w-52 text-sm sm:text-base'
|
? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
|
||||||
: 'w-full'} border-none shadow-none"
|
: 'w-full'} border-none shadow-none"
|
||||||
>
|
>
|
||||||
<Card.Content
|
<Card.Content
|
||||||
@@ -38,28 +38,32 @@
|
|||||||
>
|
>
|
||||||
<Tooltip label={$_('quantities.distance')}>
|
<Tooltip label={$_('quantities.distance')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Ruler size="18" class="mr-1" />
|
<Ruler size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.distance.total} type="distance" />
|
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={$_('quantities.elevation_gain_loss')}>
|
<Tooltip label={$_('quantities.elevation_gain_loss')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<MoveUpRight size="18" class="mr-1" />
|
<MoveUpRight size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||||
<MoveDownRight size="18" class="mx-1" />
|
<MoveDownRight size="16" class="mx-1" />
|
||||||
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
|
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
|
||||||
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
label="{$velocityUnits === 'speed'
|
||||||
'quantities.moving'
|
? $_('quantities.speed')
|
||||||
)} / {$_('quantities.total')})"
|
: $_('quantities.pace')} ({$_('quantities.moving')} / {$_('quantities.total')})"
|
||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Zap size="18" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
|
<WithUnits
|
||||||
|
value={statistics.global.speed.moving}
|
||||||
|
type="speed"
|
||||||
|
showUnits={false}
|
||||||
|
/>
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
@@ -68,10 +72,12 @@
|
|||||||
{#if panelSize > 160 || orientation === 'horizontal'}
|
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
||||||
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})"
|
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_(
|
||||||
|
'quantities.total'
|
||||||
|
)})"
|
||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Timer size="18" class="mr-1" />
|
<Timer size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.time.moving} type="time" />
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.time.total} type="time" />
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CircleHelp } from 'lucide-svelte';
|
import { CircleHelp } from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let link: string | undefined = undefined;
|
export let link: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { Languages } from 'lucide-svelte';
|
import { Languages } from 'lucide-svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
|
|
||||||
let selected = {
|
let selected = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if ($locale) {
|
$: if ($locale) {
|
||||||
selected = {
|
selected = {
|
||||||
value: $locale,
|
value: $locale,
|
||||||
label: languages[$locale]
|
label: languages[$locale],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
@@ -22,16 +22,17 @@
|
|||||||
mapboxgl.accessToken = accessToken;
|
mapboxgl.accessToken = accessToken;
|
||||||
|
|
||||||
let webgl2Supported = true;
|
let webgl2Supported = true;
|
||||||
|
let embeddedApp = false;
|
||||||
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
linear: true,
|
linear: true,
|
||||||
easing: () => 1
|
easing: () => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
|
const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
|
||||||
settings;
|
settings;
|
||||||
let scaleControl = new mapboxgl.ScaleControl({
|
let scaleControl = new mapboxgl.ScaleControl({
|
||||||
unit: $distanceUnits
|
unit: $distanceUnits,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -40,6 +41,10 @@
|
|||||||
webgl2Supported = false;
|
webgl2Supported = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (window.top !== window.self && !$page.route.id?.includes('embed')) {
|
||||||
|
embeddedApp = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let language = $page.params.language;
|
let language = $page.params.language;
|
||||||
if (language === 'zh') {
|
if (language === 'zh') {
|
||||||
@@ -65,12 +70,12 @@
|
|||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||||
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
|
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
url: ''
|
url: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'overlays',
|
id: 'overlays',
|
||||||
@@ -78,17 +83,18 @@
|
|||||||
data: {
|
data: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {},
|
sources: {},
|
||||||
layers: []
|
layers: [],
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
projection: 'globe',
|
||||||
zoom: 0,
|
zoom: 0,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
language,
|
language,
|
||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
logoPosition: 'bottom-right',
|
logoPosition: 'bottom-right',
|
||||||
boxZoom: false
|
boxZoom: false,
|
||||||
});
|
});
|
||||||
newMap.on('load', () => {
|
newMap.on('load', () => {
|
||||||
$map = newMap; // only set the store after the map has loaded
|
$map = newMap; // only set the store after the map has loaded
|
||||||
@@ -98,13 +104,13 @@
|
|||||||
|
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.AttributionControl({
|
new mapboxgl.AttributionControl({
|
||||||
compact: true
|
compact: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.NavigationControl({
|
new mapboxgl.NavigationControl({
|
||||||
visualizePitch: true
|
visualizePitch: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -128,12 +134,12 @@
|
|||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [result.lon, result.lat]
|
coordinates: [result.lon, result.lat],
|
||||||
},
|
},
|
||||||
place_name: result.display_name
|
place_name: result.display_name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
let onKeyDown = geocoder._onKeyDown;
|
let onKeyDown = geocoder._onKeyDown;
|
||||||
geocoder._onKeyDown = (e: KeyboardEvent) => {
|
geocoder._onKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -151,11 +157,11 @@
|
|||||||
newMap.addControl(
|
newMap.addControl(
|
||||||
new mapboxgl.GeolocateControl({
|
new mapboxgl.GeolocateControl({
|
||||||
positionOptions: {
|
positionOptions: {
|
||||||
enableHighAccuracy: true
|
enableHighAccuracy: true,
|
||||||
},
|
},
|
||||||
fitBoundsOptions,
|
fitBoundsOptions,
|
||||||
trackUserLocation: true,
|
trackUserLocation: true,
|
||||||
showUserHeading: true
|
showUserHeading: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,25 +173,25 @@
|
|||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
tileSize: 512,
|
tileSize: 512,
|
||||||
maxzoom: 14
|
maxzoom: 14,
|
||||||
});
|
});
|
||||||
if (newMap.getPitch() > 0) {
|
if (newMap.getPitch() > 0) {
|
||||||
newMap.setTerrain({
|
newMap.setTerrain({
|
||||||
source: 'mapbox-dem',
|
source: 'mapbox-dem',
|
||||||
exaggeration: 1
|
exaggeration: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
newMap.setFog({
|
newMap.setFog({
|
||||||
color: 'rgb(186, 210, 235)',
|
color: 'rgb(186, 210, 235)',
|
||||||
'high-color': 'rgb(36, 92, 223)',
|
'high-color': 'rgb(36, 92, 223)',
|
||||||
'horizon-blend': 0.1,
|
'horizon-blend': 0.1,
|
||||||
'space-color': 'rgb(156, 240, 255)'
|
'space-color': 'rgb(156, 240, 255)',
|
||||||
});
|
});
|
||||||
newMap.on('pitch', () => {
|
newMap.on('pitch', () => {
|
||||||
if (newMap.getPitch() > 0) {
|
if (newMap.getPitch() > 0) {
|
||||||
newMap.setTerrain({
|
newMap.setTerrain({
|
||||||
source: 'mapbox-dem',
|
source: 'mapbox-dem',
|
||||||
exaggeration: 1
|
exaggeration: 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newMap.setTerrain(null);
|
newMap.setTerrain(null);
|
||||||
@@ -201,23 +207,30 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (
|
$: if ($map && (!$treeFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)) {
|
||||||
$map &&
|
|
||||||
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
|
|
||||||
) {
|
|
||||||
$map.resize();
|
$map.resize();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<div {...$$restProps}>
|
||||||
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
|
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
|
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported &&
|
||||||
|
!embeddedApp
|
||||||
|
? 'hidden'
|
||||||
|
: ''} {embeddedApp ? 'z-30' : ''}"
|
||||||
>
|
>
|
||||||
|
{#if !webgl2Supported}
|
||||||
<p>{$_('webgl2_required')}</p>
|
<p>{$_('webgl2_required')}</p>
|
||||||
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
||||||
{$_('enable_webgl2')}
|
{$_('enable_webgl2')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{:else if embeddedApp}
|
||||||
|
<p>The app cannot be embedded in an iframe.</p>
|
||||||
|
<Button href="https://gpx.studio/help/integration" target="_blank">
|
||||||
|
Learn how to create a map for your website
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,7 +347,7 @@
|
|||||||
|
|
||||||
div :global(.mapboxgl-popup) {
|
div :global(.mapboxgl-popup) {
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@apply z-20;
|
@apply z-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-content) {
|
div :global(.mapboxgl-popup-content) {
|
||||||
|
|||||||
25
website/src/lib/components/MapPopup.svelte
Normal file
25
website/src/lib/components/MapPopup.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<svelte:options accessors />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { TrackPoint, Waypoint } from 'gpx';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import WaypointPopup from '$lib/components/gpx-layer/WaypointPopup.svelte';
|
||||||
|
import TrackpointPopup from '$lib/components/gpx-layer/TrackpointPopup.svelte';
|
||||||
|
import OverpassPopup from '$lib/components/layer-control/OverpassPopup.svelte';
|
||||||
|
import type { PopupItem } from './MapPopup';
|
||||||
|
|
||||||
|
export let item: Writable<PopupItem | null>;
|
||||||
|
export let container: HTMLDivElement | null = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={container}>
|
||||||
|
{#if $item}
|
||||||
|
{#if $item.item instanceof Waypoint}
|
||||||
|
<WaypointPopup waypoint={$item} />
|
||||||
|
{:else if $item.item instanceof TrackPoint}
|
||||||
|
<TrackpointPopup trackpoint={$item} />
|
||||||
|
{:else}
|
||||||
|
<OverpassPopup poi={$item} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
82
website/src/lib/components/MapPopup.ts
Normal file
82
website/src/lib/components/MapPopup.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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;
|
||||||
|
fileId?: string;
|
||||||
|
hide?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MapPopup {
|
||||||
|
map: mapboxgl.Map;
|
||||||
|
popup: mapboxgl.Popup;
|
||||||
|
item: Writable<PopupItem | null> = writable(null);
|
||||||
|
maybeHideBinded = this.maybeHide.bind(this);
|
||||||
|
|
||||||
|
constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
|
||||||
|
this.map = map;
|
||||||
|
this.popup = new mapboxgl.Popup(options);
|
||||||
|
|
||||||
|
let component = new MapPopupComponent({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
item: this.item,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
tick().then(() => this.popup.setDOMContent(component.container));
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(item: PopupItem | null) {
|
||||||
|
if (item) item.hide = () => this.hide();
|
||||||
|
this.item.set(item);
|
||||||
|
if (item === null) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
tick().then(() => this.show());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
const i = get(this.item);
|
||||||
|
if (i === null) {
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.popup.setLngLat(this.getCoordinates()).addTo(this.map);
|
||||||
|
this.map.on('mousemove', this.maybeHideBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeHide(e: mapboxgl.MapMouseEvent) {
|
||||||
|
const i = get(this.item);
|
||||||
|
if (i === null) {
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.map.project(this.getCoordinates()).dist(this.map.project(e.lngLat)) > 60) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.popup.remove();
|
||||||
|
this.map.off('mousemove', this.maybeHideBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.popup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoordinates() {
|
||||||
|
const i = get(this.item);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
Layers,
|
Layers,
|
||||||
GalleryVertical,
|
ListTree,
|
||||||
Languages,
|
Languages,
|
||||||
Settings,
|
Settings,
|
||||||
Info,
|
Info,
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
FileX,
|
FileX,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
ChartArea,
|
ChartArea,
|
||||||
Maximize
|
Maximize,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
editStyle,
|
editStyle,
|
||||||
exportState,
|
exportState,
|
||||||
ExportState,
|
ExportState,
|
||||||
centerMapOnSelection
|
centerMapOnSelection,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
copied,
|
copied,
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
cutSelection,
|
cutSelection,
|
||||||
pasteSelection,
|
pasteSelection,
|
||||||
selectAll,
|
selectAll,
|
||||||
selection
|
selection,
|
||||||
} from '$lib/components/file-list/Selection';
|
} from '$lib/components/file-list/Selection';
|
||||||
import { derived } from 'svelte/store';
|
import { derived } from 'svelte/store';
|
||||||
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
import { allowedPastes, ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
import { allowedPastes, ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
||||||
import Export from '$lib/components/Export.svelte';
|
import Export from '$lib/components/Export.svelte';
|
||||||
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
velocityUnits,
|
velocityUnits,
|
||||||
temperatureUnits,
|
temperatureUnits,
|
||||||
elevationProfile,
|
elevationProfile,
|
||||||
verticalFileView,
|
treeFileView,
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
distanceMarkers,
|
distanceMarkers,
|
||||||
directionMarkers,
|
directionMarkers,
|
||||||
streetViewSource,
|
streetViewSource,
|
||||||
routing
|
routing,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
|
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<div
|
<div
|
||||||
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
|
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
|
||||||
>
|
>
|
||||||
<a href="./" target="_blank" class="shrink-0">
|
<a href={getURLForLanguage($locale, '/')} target="_blank" class="shrink-0">
|
||||||
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
|
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
|
||||||
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
|
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
|
||||||
</a>
|
</a>
|
||||||
@@ -151,18 +151,27 @@
|
|||||||
<Shortcut key="O" ctrl={true} />
|
<Shortcut key="O" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={dbUtils.duplicateSelection} disabled={$selection.size == 0}>
|
<Menubar.Item
|
||||||
|
on:click={dbUtils.duplicateSelection}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
<Copy size="16" class="mr-1" />
|
<Copy size="16" class="mr-1" />
|
||||||
{$_('menu.duplicate')}
|
{$_('menu.duplicate')}
|
||||||
<Shortcut key="D" ctrl={true} />
|
<Shortcut key="D" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={dbUtils.deleteSelectedFiles} disabled={$selection.size == 0}>
|
<Menubar.Item
|
||||||
|
on:click={dbUtils.deleteSelectedFiles}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
<FileX size="16" class="mr-1" />
|
<FileX size="16" class="mr-1" />
|
||||||
{$_('menu.close')}
|
{$_('menu.close')}
|
||||||
<Shortcut key="⌫" ctrl={true} />
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item on:click={dbUtils.deleteAllFiles} disabled={$fileObservers.size == 0}>
|
<Menubar.Item
|
||||||
|
on:click={dbUtils.deleteAllFiles}
|
||||||
|
disabled={$fileObservers.size == 0}
|
||||||
|
>
|
||||||
<FileX size="16" class="mr-1" />
|
<FileX size="16" class="mr-1" />
|
||||||
{$_('menu.close_all')}
|
{$_('menu.close_all')}
|
||||||
<Shortcut key="⌫" ctrl={true} shift={true} />
|
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||||
@@ -207,7 +216,11 @@
|
|||||||
disabled={$selection.size !== 1 ||
|
disabled={$selection.size !== 1 ||
|
||||||
!$selection
|
!$selection
|
||||||
.getSelected()
|
.getSelected()
|
||||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
.every(
|
||||||
|
(item) =>
|
||||||
|
item instanceof ListFileItem ||
|
||||||
|
item instanceof ListTrackItem
|
||||||
|
)}
|
||||||
on:click={() => ($editMetadata = true)}
|
on:click={() => ($editMetadata = true)}
|
||||||
>
|
>
|
||||||
<Info size="16" class="mr-1" />
|
<Info size="16" class="mr-1" />
|
||||||
@@ -218,7 +231,11 @@
|
|||||||
disabled={$selection.size === 0 ||
|
disabled={$selection.size === 0 ||
|
||||||
!$selection
|
!$selection
|
||||||
.getSelected()
|
.getSelected()
|
||||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
.every(
|
||||||
|
(item) =>
|
||||||
|
item instanceof ListFileItem ||
|
||||||
|
item instanceof ListTrackItem
|
||||||
|
)}
|
||||||
on:click={() => ($editStyle = true)}
|
on:click={() => ($editStyle = true)}
|
||||||
>
|
>
|
||||||
<PaintBucket size="16" class="mr-1" />
|
<PaintBucket size="16" class="mr-1" />
|
||||||
@@ -243,17 +260,20 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<Shortcut key="H" ctrl={true} />
|
<Shortcut key="H" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{#if $verticalFileView}
|
{#if $treeFileView}
|
||||||
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
|
on:click={() =>
|
||||||
|
dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
|
||||||
disabled={$selection.size !== 1}
|
disabled={$selection.size !== 1}
|
||||||
>
|
>
|
||||||
<Plus size="16" class="mr-1" />
|
<Plus size="16" class="mr-1" />
|
||||||
{$_('menu.new_track')}
|
{$_('menu.new_track')}
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)}
|
{:else if $selection
|
||||||
|
.getSelected()
|
||||||
|
.some((item) => item instanceof ListTrackItem)}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -284,7 +304,7 @@
|
|||||||
{$_('menu.center')}
|
{$_('menu.center')}
|
||||||
<Shortcut key="⏎" ctrl={true} />
|
<Shortcut key="⏎" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{#if $verticalFileView}
|
{#if $treeFileView}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
||||||
<ClipboardCopy size="16" class="mr-1" />
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
@@ -300,7 +320,9 @@
|
|||||||
disabled={$copied === undefined ||
|
disabled={$copied === undefined ||
|
||||||
$copied.length === 0 ||
|
$copied.length === 0 ||
|
||||||
($selection.size > 0 &&
|
($selection.size > 0 &&
|
||||||
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))}
|
!allowedPastes[$copied[0].level].includes(
|
||||||
|
$selection.getSelected().pop()?.level
|
||||||
|
))}
|
||||||
on:click={pasteSelection}
|
on:click={pasteSelection}
|
||||||
>
|
>
|
||||||
<ClipboardPaste size="16" class="mr-1" />
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
@@ -309,7 +331,10 @@
|
|||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{/if}
|
{/if}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}>
|
<Menubar.Item
|
||||||
|
on:click={dbUtils.deleteSelection}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('menu.delete')}
|
{$_('menu.delete')}
|
||||||
<Shortcut key="⌫" ctrl={true} />
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
@@ -327,24 +352,32 @@
|
|||||||
{$_('menu.elevation_profile')}
|
{$_('menu.elevation_profile')}
|
||||||
<Shortcut key="P" ctrl={true} />
|
<Shortcut key="P" ctrl={true} />
|
||||||
</Menubar.CheckboxItem>
|
</Menubar.CheckboxItem>
|
||||||
<Menubar.CheckboxItem bind:checked={$verticalFileView}>
|
<Menubar.CheckboxItem bind:checked={$treeFileView}>
|
||||||
<GalleryVertical size="16" class="mr-1" />
|
<ListTree size="16" class="mr-1" />
|
||||||
{$_('menu.vertical_file_view')}
|
{$_('menu.tree_file_view')}
|
||||||
<Shortcut key="L" ctrl={true} />
|
<Shortcut key="L" ctrl={true} />
|
||||||
</Menubar.CheckboxItem>
|
</Menubar.CheckboxItem>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item inset on:click={switchBasemaps}>
|
<Menubar.Item inset on:click={switchBasemaps}>
|
||||||
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut key="F1" />
|
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut
|
||||||
|
key="F1"
|
||||||
|
/>
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item inset on:click={toggleOverlays}>
|
<Menubar.Item inset on:click={toggleOverlays}>
|
||||||
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut key="F2" />
|
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut
|
||||||
|
key="F2"
|
||||||
|
/>
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.CheckboxItem bind:checked={$distanceMarkers}>
|
<Menubar.CheckboxItem bind:checked={$distanceMarkers}>
|
||||||
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut key="F3" />
|
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut
|
||||||
|
key="F3"
|
||||||
|
/>
|
||||||
</Menubar.CheckboxItem>
|
</Menubar.CheckboxItem>
|
||||||
<Menubar.CheckboxItem bind:checked={$directionMarkers}>
|
<Menubar.CheckboxItem bind:checked={$directionMarkers}>
|
||||||
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut key="F4" />
|
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut
|
||||||
|
key="F4"
|
||||||
|
/>
|
||||||
</Menubar.CheckboxItem>
|
</Menubar.CheckboxItem>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item inset on:click={toggle3D}>
|
<Menubar.Item inset on:click={toggle3D}>
|
||||||
@@ -368,9 +401,15 @@
|
|||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
||||||
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="metric"
|
||||||
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
|
>{$_('menu.metric')}</Menubar.RadioItem
|
||||||
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem>
|
>
|
||||||
|
<Menubar.RadioItem value="imperial"
|
||||||
|
>{$_('menu.imperial')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="nautical"
|
||||||
|
>{$_('menu.nautical')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
@@ -380,8 +419,12 @@
|
|||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
||||||
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="speed"
|
||||||
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem>
|
>{$_('quantities.speed')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="pace"
|
||||||
|
>{$_('quantities.pace')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
@@ -391,8 +434,12 @@
|
|||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$temperatureUnits}>
|
<Menubar.RadioGroup bind:value={$temperatureUnits}>
|
||||||
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="celsius"
|
||||||
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem>
|
>{$_('menu.celsius')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="fahrenheit"
|
||||||
|
>{$_('menu.fahrenheit')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
@@ -403,7 +450,7 @@
|
|||||||
{$_('menu.language')}
|
{$_('menu.language')}
|
||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$locale}>
|
<Menubar.RadioGroup value={$locale}>
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
<a href={getURLForLanguage(lang, '/app')}>
|
<a href={getURLForLanguage(lang, '/app')}>
|
||||||
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
||||||
@@ -428,8 +475,11 @@
|
|||||||
setMode(value);
|
setMode(value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="light"
|
||||||
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem>
|
>{$_('menu.light')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
@@ -441,8 +491,12 @@
|
|||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$streetViewSource}>
|
<Menubar.RadioGroup bind:value={$streetViewSource}>
|
||||||
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="mapillary"
|
||||||
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem>
|
>{$_('menu.mapillary')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="google"
|
||||||
|
>{$_('menu.google')}</Menubar.RadioItem
|
||||||
|
>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
@@ -567,7 +621,7 @@
|
|||||||
$elevationProfile = !$elevationProfile;
|
$elevationProfile = !$elevationProfile;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
|
||||||
$verticalFileView = !$verticalFileView;
|
$treeFileView = !$treeFileView;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
||||||
if ($allHidden) {
|
if ($allHidden) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Moon, Sun } from 'lucide-svelte';
|
import { Moon, Sun } from 'lucide-svelte';
|
||||||
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let size = '20';
|
export let size = '20';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
||||||
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||||
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
|
|
||||||
const handleMouseMove = (event: PointerEvent) => {
|
const handleMouseMove = (event: PointerEvent) => {
|
||||||
const newAfter =
|
const newAfter =
|
||||||
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY);
|
startAfter +
|
||||||
|
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
|
||||||
if (newAfter >= minAfter && newAfter <= maxAfter) {
|
if (newAfter >= minAfter && newAfter <= maxAfter) {
|
||||||
after = newAfter;
|
after = newAfter;
|
||||||
} else if (newAfter < minAfter && after !== minAfter) {
|
} else if (newAfter < minAfter && after !== minAfter) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { isMac, isSafari } from '$lib/utils';
|
import { isMac, isSafari } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let key: string | undefined = undefined;
|
export let key: string | undefined = undefined;
|
||||||
export let shift: boolean = false;
|
export let shift: boolean = false;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
getDistanceUnits,
|
getDistanceUnits,
|
||||||
getElevationUnits,
|
getElevationUnits,
|
||||||
getVelocityUnits,
|
getVelocityUnits,
|
||||||
secondsToHHMMSS
|
secondsToHHMMSS,
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let value: number;
|
export let value: number;
|
||||||
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
|
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
|
|
||||||
export let module;
|
export let module;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -43,6 +41,7 @@
|
|||||||
:global(.markdown > a) {
|
:global(.markdown > a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
|
@apply contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown p > a) {
|
:global(.markdown p > a) {
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
class="w-full max-w-3xl"
|
class="w-full max-w-3xl"
|
||||||
/>
|
/>
|
||||||
{:else if src === 'tools/split'}
|
{:else if src === 'tools/split'}
|
||||||
<enhanced:img src="/src/lib/assets/img/docs/tools/split.png" {alt} class="w-full max-w-3xl" />
|
<enhanced:img
|
||||||
|
src="/src/lib/assets/img/docs/tools/split.png"
|
||||||
|
{alt}
|
||||||
|
class="w-full max-w-3xl"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
||||||
|
|||||||
@@ -1,39 +1,64 @@
|
|||||||
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
|
import {
|
||||||
import type { ComponentType } from "svelte";
|
File,
|
||||||
|
FilePen,
|
||||||
|
View,
|
||||||
|
type Icon,
|
||||||
|
Settings,
|
||||||
|
Pencil,
|
||||||
|
MapPin,
|
||||||
|
Scissors,
|
||||||
|
CalendarClock,
|
||||||
|
Group,
|
||||||
|
Ungroup,
|
||||||
|
Filter,
|
||||||
|
SquareDashedMousePointer,
|
||||||
|
MountainSnow,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import type { ComponentType } from 'svelte';
|
||||||
|
|
||||||
export const guides: Record<string, string[]> = {
|
export const guides: Record<string, string[]> = {
|
||||||
'getting-started': [],
|
'getting-started': [],
|
||||||
menu: ['file', 'edit', 'view', 'settings'],
|
menu: ['file', 'edit', 'view', 'settings'],
|
||||||
'files-and-stats': [],
|
'files-and-stats': [],
|
||||||
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'],
|
toolbar: [
|
||||||
|
'routing',
|
||||||
|
'poi',
|
||||||
|
'scissors',
|
||||||
|
'time',
|
||||||
|
'merge',
|
||||||
|
'extract',
|
||||||
|
'elevation',
|
||||||
|
'minify',
|
||||||
|
'clean',
|
||||||
|
],
|
||||||
'map-controls': [],
|
'map-controls': [],
|
||||||
'gpx': [],
|
gpx: [],
|
||||||
'integration': [],
|
integration: [],
|
||||||
'faq': [],
|
faq: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
||||||
"getting-started": "🚀",
|
'getting-started': '🚀',
|
||||||
"menu": "📂 ⚙️",
|
menu: '📂 ⚙️',
|
||||||
"file": File,
|
file: File,
|
||||||
"edit": FilePen,
|
edit: FilePen,
|
||||||
"view": View,
|
view: View,
|
||||||
"settings": Settings,
|
settings: Settings,
|
||||||
"files-and-stats": "🗂 📈",
|
'files-and-stats': '🗂 📈',
|
||||||
"toolbar": "🧰",
|
toolbar: '🧰',
|
||||||
"routing": Pencil,
|
routing: Pencil,
|
||||||
"poi": MapPin,
|
poi: MapPin,
|
||||||
"scissors": Scissors,
|
scissors: Scissors,
|
||||||
"time": CalendarClock,
|
time: CalendarClock,
|
||||||
"merge": Group,
|
merge: Group,
|
||||||
"extract": Ungroup,
|
extract: Ungroup,
|
||||||
"elevation": MountainSnow,
|
elevation: MountainSnow,
|
||||||
"minify": Filter,
|
minify: Filter,
|
||||||
"clean": SquareDashedMousePointer,
|
clean: SquareDashedMousePointer,
|
||||||
"map-controls": "🗺",
|
'map-controls': '🗺',
|
||||||
"gpx": "💾",
|
gpx: '💾',
|
||||||
"integration": "{ 👩💻 }",
|
integration: '{ 👩💻 }',
|
||||||
"faq": "🔮",
|
faq: '🔮',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPreviousGuide(currentGuide: string): string | undefined {
|
export function getPreviousGuide(currentGuide: string): string | undefined {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
embedding,
|
embedding,
|
||||||
loadFile,
|
loadFile,
|
||||||
map,
|
map,
|
||||||
updateGPXData
|
updateGPXData,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
|
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
import {
|
import {
|
||||||
allowedEmbeddingBasemaps,
|
allowedEmbeddingBasemaps,
|
||||||
getFilesFromEmbeddingOptions,
|
getFilesFromEmbeddingOptions,
|
||||||
type EmbeddingOptions
|
type EmbeddingOptions,
|
||||||
} from './Embedding';
|
} from './Embedding';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
temperatureUnits,
|
temperatureUnits,
|
||||||
fileOrder,
|
fileOrder,
|
||||||
distanceMarkers,
|
distanceMarkers,
|
||||||
directionMarkers
|
directionMarkers,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
export let useHash = true;
|
export let useHash = true;
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
distanceUnits: 'metric',
|
distanceUnits: 'metric',
|
||||||
velocityUnits: 'speed',
|
velocityUnits: 'speed',
|
||||||
temperatureUnits: 'celsius',
|
temperatureUnits: 'celsius',
|
||||||
theme: 'system'
|
theme: 'system',
|
||||||
};
|
};
|
||||||
|
|
||||||
function applyOptions() {
|
function applyOptions() {
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
let bounds = {
|
let bounds = {
|
||||||
southWest: {
|
southWest: {
|
||||||
lat: 90,
|
lat: 90,
|
||||||
lon: 180
|
lon: 180,
|
||||||
},
|
},
|
||||||
northEast: {
|
northEast: {
|
||||||
lat: -90,
|
lat: -90,
|
||||||
lon: -180
|
lon: -180,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fileObservers.update(($fileObservers) => {
|
fileObservers.update(($fileObservers) => {
|
||||||
@@ -96,12 +96,13 @@
|
|||||||
id,
|
id,
|
||||||
readable({
|
readable({
|
||||||
file,
|
file,
|
||||||
statistics
|
statistics,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
ids.push(id);
|
ids.push(id);
|
||||||
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
|
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global
|
||||||
|
.bounds;
|
||||||
|
|
||||||
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
|
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
|
||||||
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
|
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
|
||||||
@@ -130,12 +131,12 @@
|
|||||||
bounds.southWest.lon,
|
bounds.southWest.lon,
|
||||||
bounds.southWest.lat,
|
bounds.southWest.lat,
|
||||||
bounds.northEast.lon,
|
bounds.northEast.lon,
|
||||||
bounds.northEast.lat
|
bounds.northEast.lat,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
padding: 80,
|
padding: 80,
|
||||||
linear: true,
|
linear: true,
|
||||||
easing: () => 1
|
easing: () => 1,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
|
if (
|
||||||
|
options.basemap !== $currentBasemap &&
|
||||||
|
allowedEmbeddingBasemaps.includes(options.basemap)
|
||||||
|
) {
|
||||||
$currentBasemap = options.basemap;
|
$currentBasemap = options.basemap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,12 +261,10 @@
|
|||||||
options.elevation.hr ? 'hr' : null,
|
options.elevation.hr ? 'hr' : null,
|
||||||
options.elevation.cad ? 'cad' : null,
|
options.elevation.cad ? 'cad' : null,
|
||||||
options.elevation.temp ? 'temp' : null,
|
options.elevation.temp ? 'temp' : null,
|
||||||
options.elevation.power ? 'power' : null
|
options.elevation.power ? 'power' : null,
|
||||||
].filter((dataset) => dataset !== null)}
|
].filter((dataset) => dataset !== null)}
|
||||||
elevationFill={options.elevation.fill}
|
elevationFill={options.elevation.fill}
|
||||||
panelSize={options.elevation.height}
|
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
class="py-2"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
height: number;
|
height: number;
|
||||||
controls: boolean;
|
controls: boolean;
|
||||||
fill: 'slope' | 'surface' | undefined;
|
fill: 'slope' | 'surface' | 'highway' | undefined;
|
||||||
speed: boolean;
|
speed: boolean;
|
||||||
hr: boolean;
|
hr: boolean;
|
||||||
cad: boolean;
|
cad: boolean;
|
||||||
@@ -39,14 +39,14 @@ export const defaultEmbeddingOptions = {
|
|||||||
hr: false,
|
hr: false,
|
||||||
cad: false,
|
cad: false,
|
||||||
temp: false,
|
temp: false,
|
||||||
power: false
|
power: false,
|
||||||
},
|
},
|
||||||
distanceMarkers: false,
|
distanceMarkers: false,
|
||||||
directionMarkers: false,
|
directionMarkers: false,
|
||||||
distanceUnits: 'metric',
|
distanceUnits: 'metric',
|
||||||
velocityUnits: 'speed',
|
velocityUnits: 'speed',
|
||||||
temperatureUnits: 'celsius',
|
temperatureUnits: 'celsius',
|
||||||
theme: 'system'
|
theme: 'system',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
||||||
@@ -59,7 +59,11 @@ export function getMergedEmbeddingOptions(
|
|||||||
): EmbeddingOptions {
|
): EmbeddingOptions {
|
||||||
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
for (const key in options) {
|
for (const key in options) {
|
||||||
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
|
if (
|
||||||
|
typeof options[key] === 'object' &&
|
||||||
|
options[key] !== null &&
|
||||||
|
!Array.isArray(options[key])
|
||||||
|
) {
|
||||||
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
|
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
|
||||||
} else {
|
} else {
|
||||||
mergedOptions[key] = options[key];
|
mergedOptions[key] = options[key];
|
||||||
@@ -79,7 +83,10 @@ export function getCleanedEmbeddingOptions(
|
|||||||
cleanedOptions[key] !== null &&
|
cleanedOptions[key] !== null &&
|
||||||
!Array.isArray(cleanedOptions[key])
|
!Array.isArray(cleanedOptions[key])
|
||||||
) {
|
) {
|
||||||
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
|
cleanedOptions[key] = getCleanedEmbeddingOptions(
|
||||||
|
cleanedOptions[key],
|
||||||
|
defaultOptions[key]
|
||||||
|
);
|
||||||
if (Object.keys(cleanedOptions[key]).length === 0) {
|
if (Object.keys(cleanedOptions[key]).length === 0) {
|
||||||
delete cleanedOptions[key];
|
delete cleanedOptions[key];
|
||||||
}
|
}
|
||||||
@@ -141,7 +148,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
|||||||
}
|
}
|
||||||
if (options.has('slope')) {
|
if (options.has('slope')) {
|
||||||
newOptions.elevation = {
|
newOptions.elevation = {
|
||||||
fill: 'slope'
|
fill: 'slope',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return newOptions;
|
return newOptions;
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
SquareActivity,
|
SquareActivity,
|
||||||
Coins,
|
Coins,
|
||||||
Milestone,
|
Milestone,
|
||||||
Video
|
Video,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import {
|
import {
|
||||||
allowedEmbeddingBasemaps,
|
allowedEmbeddingBasemaps,
|
||||||
getCleanedEmbeddingOptions,
|
getCleanedEmbeddingOptions,
|
||||||
getDefaultEmbeddingOptions
|
getDefaultEmbeddingOptions,
|
||||||
} from './Embedding';
|
} from './Embedding';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import Embedding from './Embedding.svelte';
|
import Embedding from './Embedding.svelte';
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
let options = getDefaultEmbeddingOptions();
|
let options = getDefaultEmbeddingOptions();
|
||||||
options.token = 'YOUR_MAPBOX_TOKEN';
|
options.token = 'YOUR_MAPBOX_TOKEN';
|
||||||
options.files = [
|
options.files = [
|
||||||
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
|
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx',
|
||||||
];
|
];
|
||||||
|
|
||||||
let files = options.files[0];
|
let files = options.files[0];
|
||||||
@@ -130,7 +130,11 @@
|
|||||||
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
|
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
|
||||||
<Label class="flex flex-row items-center gap-2">
|
<Label class="flex flex-row items-center gap-2">
|
||||||
{$_('embedding.height')}
|
{$_('embedding.height')}
|
||||||
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" />
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={options.elevation.height}
|
||||||
|
class="h-8 w-20"
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<span class="shrink-0">
|
<span class="shrink-0">
|
||||||
@@ -142,7 +146,11 @@
|
|||||||
let value = selected?.value;
|
let value = selected?.value;
|
||||||
if (value === 'none') {
|
if (value === 'none') {
|
||||||
options.elevation.fill = undefined;
|
options.elevation.fill = undefined;
|
||||||
} else if (value === 'slope' || value === 'surface') {
|
} else if (
|
||||||
|
value === 'slope' ||
|
||||||
|
value === 'surface' ||
|
||||||
|
value === 'highway'
|
||||||
|
) {
|
||||||
options.elevation.fill = value;
|
options.elevation.fill = value;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -152,7 +160,10 @@
|
|||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
||||||
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
|
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item
|
||||||
|
>
|
||||||
|
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item
|
||||||
|
>
|
||||||
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
@@ -165,35 +176,35 @@
|
|||||||
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
|
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
|
||||||
<Label for="show-speed" class="flex flex-row items-center gap-1">
|
<Label for="show-speed" class="flex flex-row items-center gap-1">
|
||||||
<Zap size="16" />
|
<Zap size="16" />
|
||||||
{$_('chart.show_speed')}
|
{$_('quantities.speed')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
|
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
|
||||||
<Label for="show-hr" class="flex flex-row items-center gap-1">
|
<Label for="show-hr" class="flex flex-row items-center gap-1">
|
||||||
<HeartPulse size="16" />
|
<HeartPulse size="16" />
|
||||||
{$_('chart.show_heartrate')}
|
{$_('quantities.heartrate')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
|
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
|
||||||
<Label for="show-cad" class="flex flex-row items-center gap-1">
|
<Label for="show-cad" class="flex flex-row items-center gap-1">
|
||||||
<Orbit size="16" />
|
<Orbit size="16" />
|
||||||
{$_('chart.show_cadence')}
|
{$_('quantities.cadence')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
|
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
|
||||||
<Label for="show-temp" class="flex flex-row items-center gap-1">
|
<Label for="show-temp" class="flex flex-row items-center gap-1">
|
||||||
<Thermometer size="16" />
|
<Thermometer size="16" />
|
||||||
{$_('chart.show_temperature')}
|
{$_('quantities.temperature')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Checkbox id="show-power" bind:checked={options.elevation.power} />
|
<Checkbox id="show-power" bind:checked={options.elevation.power} />
|
||||||
<Label for="show-power" class="flex flex-row items-center gap-1">
|
<Label for="show-power" class="flex flex-row items-center gap-1">
|
||||||
<SquareActivity size="16" />
|
<SquareActivity size="16" />
|
||||||
{$_('chart.show_power')}
|
{$_('quantities.power')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +328,8 @@
|
|||||||
<Label>
|
<Label>
|
||||||
{$_('embedding.code')}
|
{$_('embedding.code')}
|
||||||
</Label>
|
</Label>
|
||||||
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
<pre
|
||||||
|
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
||||||
<code class="language-html">
|
<code class="language-html">
|
||||||
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
|
|
||||||
export let files: string[];
|
export let files: string[];
|
||||||
export let ids: string[];
|
export let ids: string[];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { copied, pasteSelection, selectAll, selection } from './Selection';
|
import { copied, pasteSelection, selectAll, selection } from './Selection';
|
||||||
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
|
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { createFile } from '$lib/stores';
|
import { createFile } from '$lib/stores';
|
||||||
|
|
||||||
export let orientation: 'vertical' | 'horizontal';
|
export let orientation: 'vertical' | 'horizontal';
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
setContext('orientation', orientation);
|
setContext('orientation', orientation);
|
||||||
setContext('recursive', recursive);
|
setContext('recursive', recursive);
|
||||||
|
|
||||||
const { verticalFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
verticalFileView.subscribe(($vertical) => {
|
treeFileView.subscribe(($vertical) => {
|
||||||
if ($vertical) {
|
if ($vertical) {
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.forEach((item) => {
|
$selection.forEach((item) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { dbUtils, getFile } from "$lib/db";
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { freeze } from "immer";
|
import { freeze } from 'immer';
|
||||||
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
|
||||||
import { selection } from "./Selection";
|
import { selection } from './Selection';
|
||||||
import { newGPXFile } from "$lib/stores";
|
import { newGPXFile } from '$lib/stores';
|
||||||
|
|
||||||
export enum ListLevel {
|
export enum ListLevel {
|
||||||
ROOT,
|
ROOT,
|
||||||
@@ -10,7 +10,7 @@ export enum ListLevel {
|
|||||||
TRACK,
|
TRACK,
|
||||||
SEGMENT,
|
SEGMENT,
|
||||||
WAYPOINTS,
|
WAYPOINTS,
|
||||||
WAYPOINT
|
WAYPOINT,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||||
@@ -19,7 +19,7 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
|||||||
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||||
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
||||||
@@ -28,7 +28,7 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
|||||||
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
|
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
|
||||||
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||||
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class ListItem {
|
export abstract class ListItem {
|
||||||
@@ -322,7 +322,13 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
|
export function moveItems(
|
||||||
|
fromParent: ListItem,
|
||||||
|
toParent: ListItem,
|
||||||
|
fromItems: ListItem[],
|
||||||
|
toItems: ListItem[],
|
||||||
|
remove: boolean = true
|
||||||
|
) {
|
||||||
if (fromItems.length === 0) {
|
if (fromItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -338,11 +344,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
context.push(file.clone());
|
context.push(file.clone());
|
||||||
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||||
context.push(file.trk[item.getTrackIndex()].clone());
|
context.push(file.trk[item.getTrackIndex()].clone());
|
||||||
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
|
} else if (
|
||||||
|
item instanceof ListTrackSegmentItem &&
|
||||||
|
item.getTrackIndex() < file.trk.length &&
|
||||||
|
item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length
|
||||||
|
) {
|
||||||
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
||||||
} else if (item instanceof ListWaypointsItem) {
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
context.push(file.wpt.map((wpt) => wpt.clone()));
|
context.push(file.wpt.map((wpt) => wpt.clone()));
|
||||||
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
|
} else if (
|
||||||
|
item instanceof ListWaypointItem &&
|
||||||
|
item.getWaypointIndex() < file.wpt.length
|
||||||
|
) {
|
||||||
context.push(file.wpt[item.getWaypointIndex()].clone());
|
context.push(file.wpt[item.getWaypointIndex()].clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +372,12 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
if (item instanceof ListTrackItem) {
|
if (item instanceof ListTrackItem) {
|
||||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
|
file.replaceTrackSegments(
|
||||||
|
item.getTrackIndex(),
|
||||||
|
item.getSegmentIndex(),
|
||||||
|
item.getSegmentIndex(),
|
||||||
|
[]
|
||||||
|
);
|
||||||
} else if (item instanceof ListWaypointsItem) {
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
||||||
} else if (item instanceof ListWaypointItem) {
|
} else if (item instanceof ListWaypointItem) {
|
||||||
@@ -371,25 +389,43 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
toItems.forEach((item, i) => {
|
toItems.forEach((item, i) => {
|
||||||
if (item instanceof ListTrackItem) {
|
if (item instanceof ListTrackItem) {
|
||||||
if (context[i] instanceof Track) {
|
if (context[i] instanceof Track) {
|
||||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
|
||||||
|
context[i],
|
||||||
|
]);
|
||||||
} else if (context[i] instanceof TrackSegment) {
|
} else if (context[i] instanceof TrackSegment) {
|
||||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
|
||||||
trkseg: [context[i]]
|
new Track({
|
||||||
})]);
|
trkseg: [context[i]],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
|
} else if (
|
||||||
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
|
item instanceof ListTrackSegmentItem &&
|
||||||
|
context[i] instanceof TrackSegment
|
||||||
|
) {
|
||||||
|
file.replaceTrackSegments(
|
||||||
|
item.getTrackIndex(),
|
||||||
|
item.getSegmentIndex(),
|
||||||
|
item.getSegmentIndex() - 1,
|
||||||
|
[context[i]]
|
||||||
|
);
|
||||||
} else if (item instanceof ListWaypointsItem) {
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
|
if (
|
||||||
|
Array.isArray(context[i]) &&
|
||||||
|
context[i].length > 0 &&
|
||||||
|
context[i][0] instanceof Waypoint
|
||||||
|
) {
|
||||||
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
|
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
|
||||||
} else if (context[i] instanceof Waypoint) {
|
} else if (context[i] instanceof Waypoint) {
|
||||||
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
|
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
|
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
|
||||||
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
|
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [
|
||||||
|
context[i],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (fromParent instanceof ListRootItem) {
|
if (fromParent instanceof ListRootItem) {
|
||||||
@@ -400,7 +436,10 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
callbacks.splice(0, 1);
|
callbacks.splice(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
dbUtils.applyEachToFilesAndGlobal(
|
||||||
|
files,
|
||||||
|
callbacks,
|
||||||
|
(files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
toItems.forEach((item, i) => {
|
toItems.forEach((item, i) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (context[i] instanceof GPXFile) {
|
if (context[i] instanceof GPXFile) {
|
||||||
@@ -421,14 +460,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
} else if (context[i] instanceof TrackSegment) {
|
} else if (context[i] instanceof TrackSegment) {
|
||||||
let newFile = newGPXFile();
|
let newFile = newGPXFile();
|
||||||
newFile._data.id = item.getFileId();
|
newFile._data.id = item.getFileId();
|
||||||
newFile.replaceTracks(0, 0, [new Track({
|
newFile.replaceTracks(0, 0, [
|
||||||
trkseg: [context[i]]
|
new Track({
|
||||||
})]);
|
trkseg: [context[i]],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
files.set(item.getFileId(), freeze(newFile));
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, context);
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
TrackSegment,
|
TrackSegment,
|
||||||
Waypoint,
|
Waypoint,
|
||||||
type AnyGPXTreeElement,
|
type AnyGPXTreeElement,
|
||||||
type GPXTreeElement
|
type GPXTreeElement,
|
||||||
} from 'gpx';
|
} from 'gpx';
|
||||||
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||||
import { settings, type GPXFileWithStatistics } from '$lib/db';
|
import { settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
ListWaypointsItem,
|
ListWaypointsItem,
|
||||||
type ListItem,
|
type ListItem,
|
||||||
type ListTrackItem
|
type ListTrackItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
@@ -39,19 +39,20 @@
|
|||||||
node instanceof GPXFile && item instanceof ListFileItem
|
node instanceof GPXFile && item instanceof ListFileItem
|
||||||
? node.metadata.name
|
? node.metadata.name
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
|
? (node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
|
||||||
: node instanceof TrackSegment
|
: node instanceof TrackSegment
|
||||||
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||||
: node instanceof Waypoint
|
: node instanceof Waypoint
|
||||||
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
|
? (node.name ??
|
||||||
|
`${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
|
||||||
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||||
? $_('gpx.waypoints')
|
? $_('gpx.waypoints')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const { verticalFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
function openIfSelectedChild() {
|
function openIfSelectedChild() {
|
||||||
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) {
|
if (collapsible && get(treeFileView) && $selection.hasAnyChildren(item, false)) {
|
||||||
collapsible.openNode();
|
collapsible.openNode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||||
buildGPX,
|
|
||||||
GPXFile,
|
|
||||||
Track,
|
|
||||||
Waypoint,
|
|
||||||
type AnyGPXTreeElement,
|
|
||||||
type GPXTreeElement
|
|
||||||
} from 'gpx';
|
|
||||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { getFile, getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
@@ -26,11 +19,11 @@
|
|||||||
ListWaypointsItem,
|
ListWaypointsItem,
|
||||||
allowedMoves,
|
allowedMoves,
|
||||||
moveItems,
|
moveItems,
|
||||||
type ListItem
|
type ListItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { isMac } from '$lib/utils';
|
import { isMac } from '$lib/utils';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||||
@@ -85,8 +78,13 @@
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
e.originalEvent &&
|
e.originalEvent &&
|
||||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
|
!(
|
||||||
($selection.size > 1 || !$selection.has(item.extend(getRealId(changed[0]))))
|
e.originalEvent.ctrlKey ||
|
||||||
|
e.originalEvent.metaKey ||
|
||||||
|
e.originalEvent.shiftKey
|
||||||
|
) &&
|
||||||
|
($selection.size > 1 ||
|
||||||
|
!$selection.has(item.extend(getRealId(changed[0]))))
|
||||||
) {
|
) {
|
||||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
@@ -115,7 +113,7 @@
|
|||||||
Sortable.utils.select(element);
|
Sortable.utils.select(element);
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'nearest'
|
block: 'nearest',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Sortable.utils.deselect(element);
|
Sortable.utils.deselect(element);
|
||||||
@@ -157,7 +155,7 @@
|
|||||||
group: {
|
group: {
|
||||||
name: sortableLevel,
|
name: sortableLevel,
|
||||||
pull: allowedMoves[sortableLevel],
|
pull: allowedMoves[sortableLevel],
|
||||||
put: true
|
put: true,
|
||||||
},
|
},
|
||||||
direction: orientation,
|
direction: orientation,
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
@@ -199,7 +197,9 @@
|
|||||||
fromItems = [fromItem.extend('waypoints')];
|
fromItems = [fromItem.extend('waypoints')];
|
||||||
} else {
|
} else {
|
||||||
let oldIndices: number[] =
|
let oldIndices: number[] =
|
||||||
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
e.oldIndicies.length > 0
|
||||||
|
? e.oldIndicies.map((i) => i.index)
|
||||||
|
: [e.oldIndex];
|
||||||
oldIndices = oldIndices.filter((i) => i >= 0);
|
oldIndices = oldIndices.filter((i) => i >= 0);
|
||||||
oldIndices.sort((a, b) => a - b);
|
oldIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
@@ -214,7 +214,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newIndices: number[] =
|
let newIndices: number[] =
|
||||||
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
e.newIndicies.length > 0
|
||||||
|
? e.newIndicies.map((i) => i.index)
|
||||||
|
: [e.newIndex];
|
||||||
newIndices = newIndices.filter((i) => i >= 0);
|
newIndices = newIndices.filter((i) => i >= 0);
|
||||||
newIndices.sort((a, b) => a - b);
|
newIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
@@ -232,31 +234,15 @@
|
|||||||
moveItems(fromItem, toItem, fromItems, toItems);
|
moveItems(fromItem, toItem, fromItems, toItems);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setData: function (dataTransfer: DataTransfer, dragEl: HTMLElement) {
|
|
||||||
if (sortableLevel === ListLevel.FILE) {
|
|
||||||
const fileId = dragEl.getAttribute('data-id');
|
|
||||||
const file = fileId ? getFile(fileId) : null;
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
const data = buildGPX(file);
|
|
||||||
dataTransfer.setData(
|
|
||||||
'DownloadURL',
|
|
||||||
`application/gpx+xml:${file.metadata.name}.gpx:data:text/octet-stream;charset=utf-8,${encodeURIComponent(data)}`
|
|
||||||
);
|
|
||||||
dataTransfer.dropEffect = 'copy';
|
|
||||||
dataTransfer.effectAllowed = 'copy';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
Object.defineProperty(sortable, '_item', {
|
Object.defineProperty(sortable, '_item', {
|
||||||
value: item,
|
value: item,
|
||||||
writable: true
|
writable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(sortable, '_waypointRoot', {
|
Object.defineProperty(sortable, '_waypointRoot', {
|
||||||
value: waypointRoot,
|
value: waypointRoot,
|
||||||
writable: true
|
writable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
Maximize,
|
Maximize,
|
||||||
Scissors,
|
Scissors,
|
||||||
FileStack,
|
FileStack,
|
||||||
FileX
|
FileX,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
allowedPastes,
|
allowedPastes,
|
||||||
type ListItem
|
type ListItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import {
|
import {
|
||||||
copied,
|
copied,
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
pasteSelection,
|
pasteSelection,
|
||||||
selectAll,
|
selectAll,
|
||||||
selectItem,
|
selectItem,
|
||||||
selection
|
selection,
|
||||||
} from './Selection';
|
} from './Selection';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
@@ -47,19 +47,14 @@
|
|||||||
embedding,
|
embedding,
|
||||||
centerMapOnSelection,
|
centerMapOnSelection,
|
||||||
gpxLayers,
|
gpxLayers,
|
||||||
map
|
map,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
GPXTreeElement,
|
import { _ } from '$lib/i18n';
|
||||||
Track,
|
|
||||||
TrackSegment,
|
|
||||||
type AnyGPXTreeElement,
|
|
||||||
Waypoint,
|
|
||||||
GPXFile
|
|
||||||
} from 'gpx';
|
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import MetadataDialog from './MetadataDialog.svelte';
|
import MetadataDialog from './MetadataDialog.svelte';
|
||||||
import StyleDialog from './StyleDialog.svelte';
|
import StyleDialog from './StyleDialog.svelte';
|
||||||
|
import { waypointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
||||||
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
@@ -75,13 +70,14 @@
|
|||||||
nodeColors = [];
|
nodeColors = [];
|
||||||
|
|
||||||
if (node instanceof GPXFile) {
|
if (node instanceof GPXFile) {
|
||||||
let style = node.getStyle();
|
let defaultColor = undefined;
|
||||||
|
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
style.color.push(layer.layerColor);
|
defaultColor = layer.layerColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let style = node.getStyle(defaultColor);
|
||||||
style.color.forEach((c) => {
|
style.color.forEach((c) => {
|
||||||
if (!nodeColors.includes(c)) {
|
if (!nodeColors.includes(c)) {
|
||||||
nodeColors.push(c);
|
nodeColors.push(c);
|
||||||
@@ -90,8 +86,8 @@
|
|||||||
} else if (node instanceof Track) {
|
} else if (node instanceof Track) {
|
||||||
let style = node.getStyle();
|
let style = node.getStyle();
|
||||||
if (style) {
|
if (style) {
|
||||||
if (style.color && !nodeColors.includes(style.color)) {
|
if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
|
||||||
nodeColors.push(style.color);
|
nodeColors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeColors.length === 0) {
|
if (nodeColors.length === 0) {
|
||||||
@@ -103,6 +99,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
|
||||||
|
|
||||||
let openEditMetadata: boolean = false;
|
let openEditMetadata: boolean = false;
|
||||||
let openEditStyle: boolean = false;
|
let openEditStyle: boolean = false;
|
||||||
|
|
||||||
@@ -179,7 +177,10 @@
|
|||||||
if (layer && file) {
|
if (layer && file) {
|
||||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||||
if (waypoint) {
|
if (waypoint) {
|
||||||
layer.showWaypointPopup(waypoint);
|
waypointPopup?.setItem({
|
||||||
|
item: waypoint,
|
||||||
|
fileId: item.getFileId(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +189,7 @@
|
|||||||
if (item instanceof ListWaypointItem) {
|
if (item instanceof ListWaypointItem) {
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
layer.hideWaypointPopup();
|
waypointPopup?.setItem(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -196,16 +197,30 @@
|
|||||||
{#if item.level === ListLevel.SEGMENT}
|
{#if item.level === ListLevel.SEGMENT}
|
||||||
<Waypoints size="16" class="mr-1 shrink-0" />
|
<Waypoints size="16" class="mr-1 shrink-0" />
|
||||||
{:else if item.level === ListLevel.WAYPOINT}
|
{:else if item.level === ListLevel.WAYPOINT}
|
||||||
|
{#if symbolKey && symbols[symbolKey].icon}
|
||||||
|
<svelte:component
|
||||||
|
this={symbols[symbolKey].icon}
|
||||||
|
size="16"
|
||||||
|
class="mr-1 shrink-0"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<MapPin size="16" class="mr-1 shrink-0" />
|
<MapPin size="16" class="mr-1 shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}">
|
{/if}
|
||||||
|
<span
|
||||||
|
class="grow select-none truncate {orientation === 'vertical'
|
||||||
|
? 'last:mr-2'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{#if hidden}
|
{#if hidden}
|
||||||
<EyeOff
|
<EyeOff
|
||||||
size="12"
|
size="12"
|
||||||
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
|
class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
|
||||||
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
|
? 'mr-2'
|
||||||
|
: ''} {item.level === ListLevel.SEGMENT ||
|
||||||
|
item.level === ListLevel.WAYPOINT
|
||||||
? 'mr-3'
|
? 'mr-3'
|
||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { Save } from 'lucide-svelte';
|
import { Save } from 'lucide-svelte';
|
||||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { editMetadata } from '$lib/stores';
|
import { editMetadata } from '$lib/stores';
|
||||||
|
|
||||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||||
@@ -17,15 +17,15 @@
|
|||||||
|
|
||||||
let name: string =
|
let name: string =
|
||||||
node instanceof GPXFile
|
node instanceof GPXFile
|
||||||
? node.metadata.name ?? ''
|
? (node.metadata.name ?? '')
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.name ?? ''
|
? (node.name ?? '')
|
||||||
: '';
|
: '';
|
||||||
let description: string =
|
let description: string =
|
||||||
node instanceof GPXFile
|
node instanceof GPXFile
|
||||||
? node.metadata.desc ?? ''
|
? (node.metadata.desc ?? '')
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.desc ?? ''
|
? (node.desc ?? '')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$: if (!open) {
|
$: if (!open) {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { get, writable } from "svelte/store";
|
import { get, writable } from 'svelte/store';
|
||||||
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
|
import {
|
||||||
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
|
ListFileItem,
|
||||||
|
ListItem,
|
||||||
|
ListRootItem,
|
||||||
|
ListTrackItem,
|
||||||
|
ListTrackSegmentItem,
|
||||||
|
ListWaypointItem,
|
||||||
|
ListLevel,
|
||||||
|
sortItems,
|
||||||
|
ListWaypointsItem,
|
||||||
|
moveItems,
|
||||||
|
} from './FileList';
|
||||||
|
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
|
||||||
|
|
||||||
export class SelectionTreeType {
|
export class SelectionTreeType {
|
||||||
item: ListItem;
|
item: ListItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
children: {
|
children: {
|
||||||
[key: string | number]: SelectionTreeType
|
[key: string | number]: SelectionTreeType;
|
||||||
};
|
};
|
||||||
size: number = 0;
|
size: number = 0;
|
||||||
|
|
||||||
@@ -67,7 +78,11 @@ export class SelectionTreeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasAnyParent(item: ListItem, self: boolean = true): boolean {
|
hasAnyParent(item: ListItem, self: boolean = true): boolean {
|
||||||
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
|
if (
|
||||||
|
this.selected &&
|
||||||
|
this.item.level <= item.level &&
|
||||||
|
(self || this.item.level < item.level)
|
||||||
|
) {
|
||||||
return this.selected;
|
return this.selected;
|
||||||
}
|
}
|
||||||
let id = item.getIdAtLevel(this.item.level);
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
@@ -80,7 +95,11 @@ export class SelectionTreeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
|
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
|
||||||
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
|
if (
|
||||||
|
this.selected &&
|
||||||
|
this.item.level >= item.level &&
|
||||||
|
(self || this.item.level > item.level)
|
||||||
|
) {
|
||||||
return this.selected;
|
return this.selected;
|
||||||
}
|
}
|
||||||
let id = item.getIdAtLevel(this.item.level);
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
@@ -131,7 +150,7 @@ export class SelectionTreeType {
|
|||||||
delete this.children[id];
|
delete this.children[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
|
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
|
||||||
|
|
||||||
@@ -181,7 +200,10 @@ export function selectAll() {
|
|||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
if (file) {
|
if (file) {
|
||||||
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
|
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
|
||||||
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
|
$selection.set(
|
||||||
|
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListWaypointItem) {
|
} else if (item instanceof ListWaypointItem) {
|
||||||
@@ -205,14 +227,24 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
|
|||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
export function applyToOrderedItemsFromFile(
|
||||||
|
selectedItems: ListItem[],
|
||||||
|
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
|
||||||
|
reverse: boolean = true
|
||||||
|
) {
|
||||||
get(settings.fileOrder).forEach((fileId) => {
|
get(settings.fileOrder).forEach((fileId) => {
|
||||||
let level: ListLevel | undefined = undefined;
|
let level: ListLevel | undefined = undefined;
|
||||||
let items: ListItem[] = [];
|
let items: ListItem[] = [];
|
||||||
selectedItems.forEach((item) => {
|
selectedItems.forEach((item) => {
|
||||||
if (item.getFileId() === fileId) {
|
if (item.getFileId() === fileId) {
|
||||||
level = item.level;
|
level = item.level;
|
||||||
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) {
|
if (
|
||||||
|
item instanceof ListFileItem ||
|
||||||
|
item instanceof ListTrackItem ||
|
||||||
|
item instanceof ListTrackSegmentItem ||
|
||||||
|
item instanceof ListWaypointsItem ||
|
||||||
|
item instanceof ListWaypointItem
|
||||||
|
) {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +257,10 @@ export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
export function applyToOrderedSelectedItemsFromFile(
|
||||||
|
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
|
||||||
|
reverse: boolean = true
|
||||||
|
) {
|
||||||
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
|
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +305,11 @@ export function pasteSelection() {
|
|||||||
let startIndex: number | undefined = undefined;
|
let startIndex: number | undefined = undefined;
|
||||||
|
|
||||||
if (fromItems[0].level === toParent.level) {
|
if (fromItems[0].level === toParent.level) {
|
||||||
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
|
if (
|
||||||
|
toParent instanceof ListTrackItem ||
|
||||||
|
toParent instanceof ListTrackSegmentItem ||
|
||||||
|
toParent instanceof ListWaypointItem
|
||||||
|
) {
|
||||||
startIndex = toParent.getId() + 1;
|
startIndex = toParent.getId() + 1;
|
||||||
}
|
}
|
||||||
toParent = toParent.getParent();
|
toParent = toParent.getParent();
|
||||||
@@ -288,20 +327,41 @@ export function pasteSelection() {
|
|||||||
fromItems.forEach((item, index) => {
|
fromItems.forEach((item, index) => {
|
||||||
if (toParent instanceof ListFileItem) {
|
if (toParent instanceof ListFileItem) {
|
||||||
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||||
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
|
toItems.push(
|
||||||
|
new ListTrackItem(
|
||||||
|
toParent.getFileId(),
|
||||||
|
(startIndex ?? toFile.trk.length) + index
|
||||||
|
)
|
||||||
|
);
|
||||||
} else if (item instanceof ListWaypointsItem) {
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
toItems.push(new ListWaypointsItem(toParent.getFileId()));
|
toItems.push(new ListWaypointsItem(toParent.getFileId()));
|
||||||
} else if (item instanceof ListWaypointItem) {
|
} else if (item instanceof ListWaypointItem) {
|
||||||
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
toItems.push(
|
||||||
|
new ListWaypointItem(
|
||||||
|
toParent.getFileId(),
|
||||||
|
(startIndex ?? toFile.wpt.length) + index
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (toParent instanceof ListTrackItem) {
|
} else if (toParent instanceof ListTrackItem) {
|
||||||
if (item instanceof ListTrackSegmentItem) {
|
if (item instanceof ListTrackSegmentItem) {
|
||||||
let toTrackIndex = toParent.getTrackIndex();
|
let toTrackIndex = toParent.getTrackIndex();
|
||||||
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
|
toItems.push(
|
||||||
|
new ListTrackSegmentItem(
|
||||||
|
toParent.getFileId(),
|
||||||
|
toTrackIndex,
|
||||||
|
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (toParent instanceof ListWaypointsItem) {
|
} else if (toParent instanceof ListWaypointsItem) {
|
||||||
if (item instanceof ListWaypointItem) {
|
if (item instanceof ListWaypointItem) {
|
||||||
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
toItems.push(
|
||||||
|
new ListWaypointItem(
|
||||||
|
toParent.getFileId(),
|
||||||
|
(startIndex ?? toFile.wpt.length) + index
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,25 +9,25 @@
|
|||||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { editStyle, gpxLayers } from '$lib/stores';
|
import { editStyle, gpxLayers } from '$lib/stores';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
const { defaultOpacity, defaultWeight } = settings;
|
const { defaultOpacity, defaultWidth } = settings;
|
||||||
|
|
||||||
let colors: string[] = [];
|
let colors: string[] = [];
|
||||||
let color: string | undefined = undefined;
|
let color: string | undefined = undefined;
|
||||||
let opacity: number[] = [];
|
let opacity: number[] = [];
|
||||||
let weight: number[] = [];
|
let width: number[] = [];
|
||||||
let colorChanged = false;
|
let colorChanged = false;
|
||||||
let opacityChanged = false;
|
let opacityChanged = false;
|
||||||
let weightChanged = false;
|
let widthChanged = false;
|
||||||
|
|
||||||
function setStyleInputs() {
|
function setStyleInputs() {
|
||||||
colors = [];
|
colors = [];
|
||||||
opacity = [];
|
opacity = [];
|
||||||
weight = [];
|
width = [];
|
||||||
|
|
||||||
$selection.forEach((item) => {
|
$selection.forEach((item) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
@@ -47,9 +47,9 @@
|
|||||||
opacity.push(o);
|
opacity.push(o);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
style.weight.forEach((w) => {
|
style.width.forEach((w) => {
|
||||||
if (!weight.includes(w)) {
|
if (!width.includes(w)) {
|
||||||
weight.push(w);
|
width.push(w);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -60,14 +60,20 @@
|
|||||||
let track = file.trk[item.getTrackIndex()];
|
let track = file.trk[item.getTrackIndex()];
|
||||||
let style = track.getStyle();
|
let style = track.getStyle();
|
||||||
if (style) {
|
if (style) {
|
||||||
if (style.color && !colors.includes(style.color)) {
|
if (
|
||||||
colors.push(style.color);
|
style['gpx_style:color'] &&
|
||||||
|
!colors.includes(style['gpx_style:color'])
|
||||||
|
) {
|
||||||
|
colors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
if (style.opacity && !opacity.includes(style.opacity)) {
|
if (
|
||||||
opacity.push(style.opacity);
|
style['gpx_style:opacity'] &&
|
||||||
|
!opacity.includes(style['gpx_style:opacity'])
|
||||||
|
) {
|
||||||
|
opacity.push(style['gpx_style:opacity']);
|
||||||
}
|
}
|
||||||
if (style.weight && !weight.includes(style.weight)) {
|
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
|
||||||
weight.push(style.weight);
|
width.push(style['gpx_style:width']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!colors.includes(layer.layerColor)) {
|
if (!colors.includes(layer.layerColor)) {
|
||||||
@@ -79,11 +85,11 @@
|
|||||||
|
|
||||||
color = colors[0];
|
color = colors[0];
|
||||||
opacity = [opacity[0] ?? $defaultOpacity];
|
opacity = [opacity[0] ?? $defaultOpacity];
|
||||||
weight = [weight[0] ?? $defaultWeight];
|
width = [width[0] ?? $defaultWidth];
|
||||||
|
|
||||||
colorChanged = false;
|
colorChanged = false;
|
||||||
opacityChanged = false;
|
opacityChanged = false;
|
||||||
weightChanged = false;
|
widthChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selection && open) {
|
$: if ($selection && open) {
|
||||||
@@ -123,37 +129,37 @@
|
|||||||
{$_('menu.style.width')}
|
{$_('menu.style.width')}
|
||||||
<div class="w-40 p-2">
|
<div class="w-40 p-2">
|
||||||
<Slider
|
<Slider
|
||||||
bind:value={weight}
|
bind:value={width}
|
||||||
id="weight"
|
id="width"
|
||||||
min={1}
|
min={1}
|
||||||
max={10}
|
max={10}
|
||||||
step={1}
|
step={1}
|
||||||
onValueChange={() => (weightChanged = true)}
|
onValueChange={() => (widthChanged = true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!colorChanged && !opacityChanged && !weightChanged}
|
disabled={!colorChanged && !opacityChanged && !widthChanged}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let style = {};
|
let style = {};
|
||||||
if (colorChanged) {
|
if (colorChanged) {
|
||||||
style.color = color;
|
style['gpx_style:color'] = color;
|
||||||
}
|
}
|
||||||
if (opacityChanged) {
|
if (opacityChanged) {
|
||||||
style.opacity = opacity[0];
|
style['gpx_style:opacity'] = opacity[0];
|
||||||
}
|
}
|
||||||
if (weightChanged) {
|
if (widthChanged) {
|
||||||
style.weight = weight[0];
|
style['gpx_style:width'] = width[0];
|
||||||
}
|
}
|
||||||
dbUtils.setStyleToSelection(style);
|
dbUtils.setStyleToSelection(style);
|
||||||
|
|
||||||
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
|
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
|
||||||
if (style.opacity) {
|
if (style['gpx_style:opacity']) {
|
||||||
$defaultOpacity = style.opacity;
|
$defaultOpacity = style['gpx_style:opacity'];
|
||||||
}
|
}
|
||||||
if (style.weight) {
|
if (style['gpx_style:width']) {
|
||||||
$defaultWeight = style.weight;
|
$defaultWidth = style['gpx_style:width'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
website/src/lib/components/gpx-layer/CopyCoordinates.svelte
Normal file
23
website/src/lib/components/gpx-layer/CopyCoordinates.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { ClipboardCopy } from 'lucide-svelte';
|
||||||
|
import { _ } from '$lib/i18n';
|
||||||
|
import type { Coordinates } from 'gpx';
|
||||||
|
|
||||||
|
export let coordinates: Coordinates;
|
||||||
|
export let onCopy: () => void = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="w-full px-2 py-1 h-8 justify-start {$$props.class}"
|
||||||
|
variant="outline"
|
||||||
|
on:click={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${coordinates.lat.toFixed(6)}, ${coordinates.lon.toFixed(6)}`
|
||||||
|
);
|
||||||
|
onCopy();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
{$_('menu.copy_coordinates')}
|
||||||
|
</Button>
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { settings } from '$lib/db';
|
||||||
import { settings } from "$lib/db";
|
import { gpxStatistics } from '$lib/stores';
|
||||||
import { gpxStatistics } from "$lib/stores";
|
import { get } from 'svelte/store';
|
||||||
import { get } from "svelte/store";
|
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
|
const stops = [
|
||||||
|
[100, 0],
|
||||||
|
[50, 7],
|
||||||
|
[25, 8, 10],
|
||||||
|
[10, 10],
|
||||||
|
[5, 11],
|
||||||
|
[1, 13],
|
||||||
|
];
|
||||||
|
|
||||||
export class DistanceMarkers {
|
export class DistanceMarkers {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
@@ -28,41 +36,55 @@ export class DistanceMarkers {
|
|||||||
} else {
|
} else {
|
||||||
this.map.addSource('distance-markers', {
|
this.map.addSource('distance-markers', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: this.getDistanceMarkersGeoJSON()
|
data: this.getDistanceMarkersGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!this.map.getLayer('distance-markers')) {
|
stops.forEach(([d, minzoom, maxzoom]) => {
|
||||||
|
if (!this.map.getLayer(`distance-markers-${d}`)) {
|
||||||
this.map.addLayer({
|
this.map.addLayer({
|
||||||
id: 'distance-markers',
|
id: `distance-markers-${d}`,
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'distance-markers',
|
source: 'distance-markers',
|
||||||
|
filter:
|
||||||
|
d === 5
|
||||||
|
? [
|
||||||
|
'any',
|
||||||
|
['==', ['get', 'level'], 5],
|
||||||
|
['==', ['get', 'level'], 25],
|
||||||
|
]
|
||||||
|
: ['==', ['get', 'level'], d],
|
||||||
|
minzoom: minzoom,
|
||||||
|
maxzoom: maxzoom ?? 24,
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'distance'],
|
'text-field': ['get', 'distance'],
|
||||||
'text-size': 14,
|
'text-size': 14,
|
||||||
'text-font': ['Open Sans Bold'],
|
'text-font': ['Open Sans Bold'],
|
||||||
'text-padding': 20,
|
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-color': 'black',
|
'text-color': 'black',
|
||||||
'text-halo-width': 2,
|
'text-halo-width': 2,
|
||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.map.moveLayer(`distance-markers-${d}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.map.moveLayer('distance-markers');
|
stops.forEach(([d]) => {
|
||||||
|
if (this.map.getLayer(`distance-markers-${d}`)) {
|
||||||
|
this.map.removeLayer(`distance-markers-${d}`);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
if (this.map.getLayer('distance-markers')) {
|
|
||||||
this.map.removeLayer('distance-markers');
|
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
@@ -71,17 +93,28 @@ export class DistanceMarkers {
|
|||||||
let features = [];
|
let features = [];
|
||||||
let currentTargetDistance = 1;
|
let currentTargetDistance = 1;
|
||||||
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
||||||
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
|
if (
|
||||||
|
statistics.local.distance.total[i] >=
|
||||||
|
currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)
|
||||||
|
) {
|
||||||
let distance = currentTargetDistance.toFixed(0);
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
|
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
||||||
|
0, 0,
|
||||||
|
];
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
|
coordinates: [
|
||||||
|
statistics.local.points[i].getLongitude(),
|
||||||
|
statistics.local.points[i].getLatitude(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
distance,
|
distance,
|
||||||
}
|
level,
|
||||||
|
minzoom,
|
||||||
|
},
|
||||||
} as GeoJSON.Feature);
|
} as GeoJSON.Feature);
|
||||||
currentTargetDistance += 1;
|
currentTargetDistance += 1;
|
||||||
}
|
}
|
||||||
@@ -89,7 +122,7 @@ export class DistanceMarkers {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features
|
features,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,28 @@
|
|||||||
import { currentTool, map, Tool } from "$lib/stores";
|
import { currentTool, map, Tool } from '$lib/stores';
|
||||||
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
|
import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
|
||||||
import { get, type Readable } from "svelte/store";
|
import { get, type Readable } from 'svelte/store';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
|
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
|
||||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
|
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
import {
|
||||||
import type { Waypoint } from "gpx";
|
ListTrackSegmentItem,
|
||||||
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
ListWaypointItem,
|
||||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
ListWaypointsItem,
|
||||||
import { MapPin, Square } from "lucide-static";
|
ListTrackItem,
|
||||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
ListFileItem,
|
||||||
|
ListRootItem,
|
||||||
|
} from '$lib/components/file-list/FileList';
|
||||||
|
import {
|
||||||
|
getClosestLinePoint,
|
||||||
|
getElevation,
|
||||||
|
resetCursor,
|
||||||
|
setGrabbingCursor,
|
||||||
|
setPointerCursor,
|
||||||
|
setScissorsCursor,
|
||||||
|
} from '$lib/utils';
|
||||||
|
import { selectedWaypoint } from '$lib/components/toolbar/tools/Waypoint.svelte';
|
||||||
|
import { MapPin, Square } from 'lucide-static';
|
||||||
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000',
|
'#ff0000',
|
||||||
@@ -22,7 +35,7 @@ const colors = [
|
|||||||
'#288228',
|
'#288228',
|
||||||
'#9933ff',
|
'#9933ff',
|
||||||
'#50f0be',
|
'#50f0be',
|
||||||
'#8c645a'
|
'#8c645a',
|
||||||
];
|
];
|
||||||
|
|
||||||
const colorCount: { [key: string]: number } = {};
|
const colorCount: { [key: string]: number } = {};
|
||||||
@@ -46,26 +59,30 @@ function decrementColor(color: string) {
|
|||||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||||
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
${Square
|
${Square.replace('width="24"', 'width="12"')
|
||||||
.replace('width="24"', 'width="12"')
|
|
||||||
.replace('height="24"', 'height="12"')
|
.replace('height="24"', 'height="12"')
|
||||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||||
${MapPin
|
${MapPin.replace('width="24"', '')
|
||||||
.replace('width="24"', '')
|
|
||||||
.replace('height="24"', '')
|
.replace('height="24"', '')
|
||||||
.replace('stroke="currentColor"', '')
|
.replace('stroke="currentColor"', '')
|
||||||
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
||||||
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
|
.replace(
|
||||||
${symbolSvg?.replace('width="24"', 'width="10"')
|
'circle',
|
||||||
|
`circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`
|
||||||
|
)}
|
||||||
|
${
|
||||||
|
symbolSvg
|
||||||
|
?.replace('width="24"', 'width="10"')
|
||||||
.replace('height="24"', 'height="10"')
|
.replace('height="24"', 'height="10"')
|
||||||
.replace('stroke="currentColor"', 'stroke="white"')
|
.replace('stroke="currentColor"', 'stroke="white"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
|
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''
|
||||||
|
}
|
||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
|
const { directionMarkers, treeFileView, defaultOpacity, defaultWidth } = settings;
|
||||||
|
|
||||||
export class GPXLayer {
|
export class GPXLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -80,17 +97,22 @@ export class GPXLayer {
|
|||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
|
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||||
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
|
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(
|
||||||
|
map: mapboxgl.Map,
|
||||||
|
fileId: string,
|
||||||
|
file: Readable<GPXFileWithStatistics | undefined>
|
||||||
|
) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.layerColor = getColor();
|
this.layerColor = getColor();
|
||||||
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
||||||
this.unsubscribe.push(selection.subscribe($selection => {
|
this.unsubscribe.push(
|
||||||
|
selection.subscribe(($selection) => {
|
||||||
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
|
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
|
||||||
if (this.selected || newSelected) {
|
if (this.selected || newSelected) {
|
||||||
this.selected = newSelected;
|
this.selected = newSelected;
|
||||||
@@ -99,17 +121,20 @@ export class GPXLayer {
|
|||||||
if (newSelected) {
|
if (newSelected) {
|
||||||
this.moveToFront();
|
this.moveToFront();
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||||
this.unsubscribe.push(currentTool.subscribe(tool => {
|
this.unsubscribe.push(
|
||||||
|
currentTool.subscribe((tool) => {
|
||||||
if (tool === Tool.WAYPOINT && !this.draggable) {
|
if (tool === Tool.WAYPOINT && !this.draggable) {
|
||||||
this.draggable = true;
|
this.draggable = true;
|
||||||
this.markers.forEach(marker => marker.setDraggable(true));
|
this.markers.forEach((marker) => marker.setDraggable(true));
|
||||||
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||||
this.draggable = false;
|
this.draggable = false;
|
||||||
this.markers.forEach(marker => marker.setDraggable(false));
|
this.markers.forEach((marker) => marker.setDraggable(false));
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
|
|
||||||
this.map.on('style.import.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
@@ -121,7 +146,11 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
|
if (
|
||||||
|
file._data.style &&
|
||||||
|
file._data.style.color &&
|
||||||
|
this.layerColor !== `#${file._data.style.color}`
|
||||||
|
) {
|
||||||
decrementColor(this.layerColor);
|
decrementColor(this.layerColor);
|
||||||
this.layerColor = `#${file._data.style.color}`;
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
}
|
}
|
||||||
@@ -133,7 +162,7 @@ export class GPXLayer {
|
|||||||
} else {
|
} else {
|
||||||
this.map.addSource(this.fileId, {
|
this.map.addSource(this.fileId, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: this.getGeoJSON()
|
data: this.getGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,24 +173,26 @@ export class GPXLayer {
|
|||||||
source: this.fileId,
|
source: this.fileId,
|
||||||
layout: {
|
layout: {
|
||||||
'line-join': 'round',
|
'line-join': 'round',
|
||||||
'line-cap': 'round'
|
'line-cap': 'round',
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['get', 'color'],
|
'line-color': ['get', 'color'],
|
||||||
'line-width': ['get', 'weight'],
|
'line-width': ['get', 'width'],
|
||||||
'line-opacity': ['get', 'opacity']
|
'line-opacity': ['get', 'opacity'],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
this.map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
if (!this.map.getLayer(this.fileId + '-direction')) {
|
if (!this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.addLayer({
|
this.map.addLayer(
|
||||||
|
{
|
||||||
id: this.fileId + '-direction',
|
id: this.fileId + '-direction',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: this.fileId,
|
source: this.fileId,
|
||||||
@@ -179,9 +210,11 @@ export class GPXLayer {
|
|||||||
'text-color': 'white',
|
'text-color': 'white',
|
||||||
'text-opacity': 0.7,
|
'text-opacity': 0.7,
|
||||||
'text-halo-width': 0.2,
|
'text-halo-width': 0.2,
|
||||||
'text-halo-color': 'white'
|
'text-halo-color': 'white',
|
||||||
}
|
},
|
||||||
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
},
|
||||||
|
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
@@ -196,23 +229,53 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
this.map.setFilter(
|
||||||
|
this.fileId,
|
||||||
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
{ validate: false }
|
||||||
|
);
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
this.map.setFilter(
|
||||||
|
this.fileId + '-direction',
|
||||||
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
{ validate: false }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let markerIndex = 0;
|
let markerIndex = 0;
|
||||||
|
|
||||||
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
||||||
file.wpt.forEach((waypoint) => { // Update markers
|
file.wpt.forEach((waypoint) => {
|
||||||
|
// Update markers
|
||||||
let symbolKey = getSymbolKey(waypoint.sym);
|
let symbolKey = getSymbolKey(waypoint.sym);
|
||||||
if (markerIndex < this.markers.length) {
|
if (markerIndex < this.markers.length) {
|
||||||
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
|
||||||
|
symbolKey,
|
||||||
|
this.layerColor
|
||||||
|
);
|
||||||
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
||||||
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
|
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
|
||||||
|
value: waypoint,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||||
@@ -220,15 +283,15 @@ export class GPXLayer {
|
|||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: this.draggable,
|
draggable: this.draggable,
|
||||||
element,
|
element,
|
||||||
anchor: 'bottom'
|
anchor: 'bottom',
|
||||||
}).setLngLat(waypoint.getCoordinates());
|
}).setLngLat(waypoint.getCoordinates());
|
||||||
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||||
let dragEndTimestamp = 0;
|
let dragEndTimestamp = 0;
|
||||||
marker.getElement().addEventListener('mouseover', (e) => {
|
marker.getElement().addEventListener('mousemove', (e) => {
|
||||||
if (marker._isDragging) {
|
if (marker._isDragging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.showWaypointPopup(marker._waypoint);
|
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
marker.getElement().addEventListener('click', (e) => {
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
@@ -242,23 +305,33 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get(verticalFileView)) {
|
if (get(treeFileView)) {
|
||||||
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
|
if (
|
||||||
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
get(selection).hasAnyChildren(
|
||||||
|
new ListWaypointsItem(this.fileId),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addSelectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
selectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (get(currentTool) === Tool.WAYPOINT) {
|
} else if (get(currentTool) === Tool.WAYPOINT) {
|
||||||
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||||
} else {
|
} else {
|
||||||
this.showWaypointPopup(marker._waypoint);
|
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
marker.on('dragstart', () => {
|
marker.on('dragstart', () => {
|
||||||
setGrabbingCursor();
|
setGrabbingCursor();
|
||||||
marker.getElement().style.cursor = 'grabbing';
|
marker.getElement().style.cursor = 'grabbing';
|
||||||
this.hideWaypointPopup();
|
waypointPopup?.hide();
|
||||||
});
|
});
|
||||||
marker.on('dragend', (e) => {
|
marker.on('dragend', (e) => {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
@@ -269,12 +342,12 @@ export class GPXLayer {
|
|||||||
let wpt = file.wpt[marker._waypoint._data.index];
|
let wpt = file.wpt[marker._waypoint._data.index];
|
||||||
wpt.setCoordinates({
|
wpt.setCoordinates({
|
||||||
lat: latLng.lat,
|
lat: latLng.lat,
|
||||||
lon: latLng.lng
|
lon: latLng.lng,
|
||||||
});
|
});
|
||||||
wpt.ele = ele[0];
|
wpt.ele = ele[0];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
dragEndTimestamp = Date.now()
|
dragEndTimestamp = Date.now();
|
||||||
});
|
});
|
||||||
this.markers.push(marker);
|
this.markers.push(marker);
|
||||||
}
|
}
|
||||||
@@ -282,7 +355,8 @@ export class GPXLayer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
while (markerIndex < this.markers.length) { // Remove extra markers
|
while (markerIndex < this.markers.length) {
|
||||||
|
// Remove extra markers
|
||||||
this.markers.pop()?.remove();
|
this.markers.pop()?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +381,7 @@ export class GPXLayer {
|
|||||||
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
this.map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
this.map.off('style.import.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
@@ -334,7 +409,10 @@ export class GPXLayer {
|
|||||||
this.map.moveLayer(this.fileId);
|
this.map.moveLayer(this.fileId);
|
||||||
}
|
}
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
this.map.moveLayer(
|
||||||
|
this.fileId + '-direction',
|
||||||
|
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +420,12 @@ export class GPXLayer {
|
|||||||
let trackIndex = e.features[0].properties.trackIndex;
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
setScissorsCursor();
|
setScissorsCursor();
|
||||||
} else {
|
} else {
|
||||||
setPointerCursor();
|
setPointerCursor();
|
||||||
@@ -353,16 +436,43 @@ export class GPXLayer {
|
|||||||
resetCursor();
|
resetCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layerOnMouseMove(e: any) {
|
||||||
|
if (e.originalEvent.shiftKey) {
|
||||||
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layerOnClick(e: any) {
|
layerOnClick(e: any) {
|
||||||
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
if (
|
||||||
|
get(currentTool) === Tool.ROUTING &&
|
||||||
|
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let trackIndex = e.features[0].properties.trackIndex;
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
dbUtils.split(this.fileId, trackIndex, segmentIndex, {
|
||||||
|
lat: e.lngLat.lat,
|
||||||
|
lon: e.lngLat.lng,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +482,12 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let item = undefined;
|
let item = undefined;
|
||||||
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
|
if (get(treeFileView) && file.getSegments().length > 1) {
|
||||||
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
// Select inner item
|
||||||
|
item =
|
||||||
|
file.children[trackIndex].children.length > 1
|
||||||
|
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
: new ListTrackItem(this.fileId, trackIndex);
|
||||||
} else {
|
} else {
|
||||||
item = new ListFileItem(this.fileId);
|
item = new ListFileItem(this.fileId);
|
||||||
}
|
}
|
||||||
@@ -391,55 +505,19 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showWaypointPopup(waypoint: Waypoint) {
|
|
||||||
if (get(currentPopupWaypoint) !== null) {
|
|
||||||
this.hideWaypointPopup();
|
|
||||||
}
|
|
||||||
let marker = this.markers[waypoint._data.index];
|
|
||||||
if (marker) {
|
|
||||||
currentPopupWaypoint.set([waypoint, this.fileId]);
|
|
||||||
marker.setPopup(waypointPopup);
|
|
||||||
marker.togglePopup();
|
|
||||||
this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeHideWaypointPopup(e: any) {
|
|
||||||
let waypoint = get(currentPopupWaypoint)?.[0];
|
|
||||||
if (waypoint) {
|
|
||||||
let marker = this.markers[waypoint._data.index];
|
|
||||||
if (marker) {
|
|
||||||
if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 100) {
|
|
||||||
this.hideWaypointPopup();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.hideWaypointPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideWaypointPopup() {
|
|
||||||
let waypoint = get(currentPopupWaypoint)?.[0];
|
|
||||||
if (waypoint) {
|
|
||||||
let marker = this.markers[waypoint._data.index];
|
|
||||||
marker?.getPopup()?.remove();
|
|
||||||
currentPopupWaypoint.set(null);
|
|
||||||
this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = file.toGeoJSON();
|
let data = file.toGeoJSON();
|
||||||
|
|
||||||
let trackIndex = 0, segmentIndex = 0;
|
let trackIndex = 0,
|
||||||
|
segmentIndex = 0;
|
||||||
for (let feature of data.features) {
|
for (let feature of data.features) {
|
||||||
if (!feature.properties) {
|
if (!feature.properties) {
|
||||||
feature.properties = {};
|
feature.properties = {};
|
||||||
@@ -447,14 +525,19 @@ export class GPXLayer {
|
|||||||
if (!feature.properties.color) {
|
if (!feature.properties.color) {
|
||||||
feature.properties.color = this.layerColor;
|
feature.properties.color = this.layerColor;
|
||||||
}
|
}
|
||||||
if (!feature.properties.weight) {
|
|
||||||
feature.properties.weight = get(defaultWeight);
|
|
||||||
}
|
|
||||||
if (!feature.properties.opacity) {
|
if (!feature.properties.opacity) {
|
||||||
feature.properties.opacity = get(defaultOpacity);
|
feature.properties.opacity = get(defaultOpacity);
|
||||||
}
|
}
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
|
if (!feature.properties.width) {
|
||||||
feature.properties.weight = feature.properties.weight + 2;
|
feature.properties.width = get(defaultWidth);
|
||||||
|
}
|
||||||
|
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);
|
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
|
||||||
}
|
}
|
||||||
feature.properties.trackIndex = trackIndex;
|
feature.properties.trackIndex = trackIndex;
|
||||||
|
|||||||
44
website/src/lib/components/gpx-layer/GPXLayerPopup.ts
Normal file
44
website/src/lib/components/gpx-layer/GPXLayerPopup.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { dbUtils } from '$lib/db';
|
||||||
|
import { MapPopup } from '$lib/components/MapPopup';
|
||||||
|
|
||||||
|
export let waypointPopup: MapPopup | null = null;
|
||||||
|
export let trackpointPopup: MapPopup | null = null;
|
||||||
|
|
||||||
|
export function createPopups(map: mapboxgl.Map) {
|
||||||
|
removePopups();
|
||||||
|
waypointPopup = new MapPopup(map, {
|
||||||
|
closeButton: false,
|
||||||
|
focusAfterOpen: false,
|
||||||
|
maxWidth: undefined,
|
||||||
|
offset: {
|
||||||
|
top: [0, 0],
|
||||||
|
'top-left': [0, 0],
|
||||||
|
'top-right': [0, 0],
|
||||||
|
bottom: [0, -30],
|
||||||
|
'bottom-left': [0, -30],
|
||||||
|
'bottom-right': [0, -30],
|
||||||
|
left: [10, -15],
|
||||||
|
right: [-10, -15],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
trackpointPopup = new MapPopup(map, {
|
||||||
|
closeButton: false,
|
||||||
|
focusAfterOpen: false,
|
||||||
|
maxWidth: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePopups() {
|
||||||
|
if (waypointPopup !== null) {
|
||||||
|
waypointPopup.remove();
|
||||||
|
waypointPopup = null;
|
||||||
|
}
|
||||||
|
if (trackpointPopup !== null) {
|
||||||
|
trackpointPopup.remove();
|
||||||
|
trackpointPopup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteWaypoint(fileId: string, waypointIndex: number) {
|
||||||
|
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { map, gpxLayers } from '$lib/stores';
|
import { map, gpxLayers } from '$lib/stores';
|
||||||
import { GPXLayer } from './GPXLayer';
|
import { GPXLayer } from './GPXLayer';
|
||||||
import WaypointPopup from './WaypointPopup.svelte';
|
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
import { DistanceMarkers } from './DistanceMarkers';
|
import { DistanceMarkers } from './DistanceMarkers';
|
||||||
import { StartEndMarkers } from './StartEndMarkers';
|
import { StartEndMarkers } from './StartEndMarkers';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
import { createPopups, removePopups } from './GPXLayerPopup';
|
||||||
|
|
||||||
let distanceMarkers: DistanceMarkers | undefined = undefined;
|
let distanceMarkers: DistanceMarkers | undefined = undefined;
|
||||||
let startEndMarkers: StartEndMarkers | undefined = undefined;
|
let startEndMarkers: StartEndMarkers | undefined = undefined;
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
if (startEndMarkers) {
|
if (startEndMarkers) {
|
||||||
startEndMarkers.remove();
|
startEndMarkers.remove();
|
||||||
}
|
}
|
||||||
|
createPopups($map);
|
||||||
distanceMarkers = new DistanceMarkers($map);
|
distanceMarkers = new DistanceMarkers($map);
|
||||||
startEndMarkers = new StartEndMarkers($map);
|
startEndMarkers = new StartEndMarkers($map);
|
||||||
}
|
}
|
||||||
@@ -42,17 +43,14 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
gpxLayers.forEach((layer) => layer.remove());
|
gpxLayers.forEach((layer) => layer.remove());
|
||||||
gpxLayers.clear();
|
gpxLayers.clear();
|
||||||
|
removePopups();
|
||||||
if (distanceMarkers) {
|
if (distanceMarkers) {
|
||||||
distanceMarkers.remove();
|
distanceMarkers.remove();
|
||||||
distanceMarkers = undefined;
|
distanceMarkers = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startEndMarkers) {
|
if (startEndMarkers) {
|
||||||
startEndMarkers.remove();
|
startEndMarkers.remove();
|
||||||
startEndMarkers = undefined;
|
startEndMarkers = undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WaypointPopup />
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores";
|
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { get } from "svelte/store";
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
export class StartEndMarkers {
|
export class StartEndMarkers {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -16,7 +16,8 @@ export class StartEndMarkers {
|
|||||||
let endElement = document.createElement('div');
|
let endElement = document.createElement('div');
|
||||||
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
|
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
|
||||||
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
|
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
|
||||||
endElement.style.background = 'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
|
endElement.style.background =
|
||||||
|
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
|
||||||
|
|
||||||
this.start = new mapboxgl.Marker({ element: startElement });
|
this.start = new mapboxgl.Marker({ element: startElement });
|
||||||
this.end = new mapboxgl.Marker({ element: endElement });
|
this.end = new mapboxgl.Marker({ element: endElement });
|
||||||
@@ -31,7 +32,11 @@ export class StartEndMarkers {
|
|||||||
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
||||||
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
|
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
|
||||||
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
|
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
|
||||||
this.end.setLngLat(statistics.local.points[statistics.local.points.length - 1].getCoordinates()).addTo(this.map);
|
this.end
|
||||||
|
.setLngLat(
|
||||||
|
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
|
||||||
|
)
|
||||||
|
.addTo(this.map);
|
||||||
} else {
|
} else {
|
||||||
this.start.remove();
|
this.start.remove();
|
||||||
this.end.remove();
|
this.end.remove();
|
||||||
@@ -39,7 +44,7 @@ export class StartEndMarkers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
this.start.remove();
|
this.start.remove();
|
||||||
this.end.remove();
|
this.end.remove();
|
||||||
|
|||||||
42
website/src/lib/components/gpx-layer/TrackpointPopup.svelte
Normal file
42
website/src/lib/components/gpx-layer/TrackpointPopup.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { TrackPoint } from 'gpx';
|
||||||
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
|
import CopyCoordinates from '$lib/components/gpx-layer/CopyCoordinates.svelte';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
|
import { Compass, Mountain, Timer } from 'lucide-svelte';
|
||||||
|
import { _, df } from '$lib/i18n';
|
||||||
|
|
||||||
|
export let trackpoint: PopupItem<TrackPoint>;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root class="border-none shadow-md text-base p-2">
|
||||||
|
<Card.Header class="p-0">
|
||||||
|
<Card.Title class="text-md"></Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="flex flex-col p-0 text-xs gap-1">
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<Compass size="14" />
|
||||||
|
{trackpoint.item.getLatitude().toFixed(6)}° {trackpoint.item
|
||||||
|
.getLongitude()
|
||||||
|
.toFixed(6)}°
|
||||||
|
</div>
|
||||||
|
{#if trackpoint.item.ele !== undefined}
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<Mountain size="14" />
|
||||||
|
<WithUnits value={trackpoint.item.ele} type="elevation" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if trackpoint.item.time}
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<Timer size="14" />
|
||||||
|
{$df.format(trackpoint.item.time)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<CopyCoordinates
|
||||||
|
coordinates={trackpoint.item.attributes}
|
||||||
|
onCopy={() => trackpoint.hide?.()}
|
||||||
|
class="mt-0.5"
|
||||||
|
/>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
@@ -2,23 +2,21 @@
|
|||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup';
|
import CopyCoordinates from '$lib/components/gpx-layer/CopyCoordinates.svelte';
|
||||||
|
import { deleteWaypoint } from './GPXLayerPopup';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
|
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { Tool, currentTool } from '$lib/stores';
|
import { Tool, currentTool } from '$lib/stores';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import type { Waypoint } from 'gpx';
|
||||||
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
|
||||||
let popupElement: HTMLDivElement;
|
export let waypoint: PopupItem<Waypoint>;
|
||||||
|
|
||||||
onMount(() => {
|
$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
|
||||||
waypointPopup.setDOMContent(popupElement);
|
|
||||||
popupElement.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
|
|
||||||
|
|
||||||
function sanitize(text: string | undefined): string {
|
function sanitize(text: string | undefined): string {
|
||||||
if (text === undefined) {
|
if (text === undefined) {
|
||||||
@@ -28,28 +26,26 @@
|
|||||||
allowedTags: ['a', 'br', 'img'],
|
allowedTags: ['a', 'br', 'img'],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'target'],
|
a: ['href', 'target'],
|
||||||
img: ['src']
|
img: ['src'],
|
||||||
}
|
},
|
||||||
}).trim();
|
}).trim();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={popupElement} class="hidden">
|
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
|
||||||
{#if $currentPopupWaypoint}
|
|
||||||
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
|
|
||||||
<Card.Header class="p-0">
|
<Card.Header class="p-0">
|
||||||
<Card.Title class="text-md">
|
<Card.Title class="text-md">
|
||||||
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
|
{#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
|
||||||
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
|
<a href={waypoint.item.link.attributes.href} target="_blank">
|
||||||
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
|
{waypoint.item.name ?? waypoint.item.link.attributes.href}
|
||||||
<ExternalLink size="12" class="inline-block mb-1.5" />
|
<ExternalLink size="12" class="inline-block mb-1.5" />
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
|
{waypoint.item.name ?? $_('gpx.waypoint')}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex flex-col p-0 text-sm">
|
<Card.Content class="flex flex-col text-sm p-0">
|
||||||
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
|
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
|
||||||
{#if symbolKey}
|
{#if symbolKey}
|
||||||
<span>
|
<span>
|
||||||
@@ -66,36 +62,38 @@
|
|||||||
</span>
|
</span>
|
||||||
<Dot size="16" />
|
<Dot size="16" />
|
||||||
{/if}
|
{/if}
|
||||||
{$currentPopupWaypoint[0].getLatitude().toFixed(6)}° {$currentPopupWaypoint[0]
|
{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item
|
||||||
.getLongitude()
|
.getLongitude()
|
||||||
.toFixed(6)}°
|
.toFixed(6)}°
|
||||||
{#if $currentPopupWaypoint[0].ele !== undefined}
|
{#if waypoint.item.ele !== undefined}
|
||||||
<Dot size="16" />
|
<Dot size="16" />
|
||||||
<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" />
|
<WithUnits value={waypoint.item.ele} type="elevation" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if $currentPopupWaypoint[0].desc}
|
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
|
||||||
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
|
{#if waypoint.item.desc}
|
||||||
|
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
|
{#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
|
||||||
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
|
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
</ScrollArea>
|
||||||
|
<div class="mt-2 flex flex-col gap-1">
|
||||||
|
<CopyCoordinates coordinates={waypoint.item.attributes} />
|
||||||
{#if $currentTool === Tool.WAYPOINT}
|
{#if $currentTool === Tool.WAYPOINT}
|
||||||
<Button
|
<Button
|
||||||
class="mt-2 w-full px-2 py-1 h-8 justify-start"
|
class="w-full px-2 py-1 h-8 justify-start"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
on:click={() =>
|
on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
|
||||||
deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
|
|
||||||
>
|
>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('menu.delete')}
|
{$_('menu.delete')}
|
||||||
<Shortcut shift={true} click={true} />
|
<Shortcut shift={true} click={true} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(a) {
|
div :global(a) {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { dbUtils } from "$lib/db";
|
|
||||||
import type { Waypoint } from "gpx";
|
|
||||||
import mapboxgl from "mapbox-gl";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
export const currentPopupWaypoint = writable<[Waypoint, string] | null>(null);
|
|
||||||
|
|
||||||
export const waypointPopup = new mapboxgl.Popup({
|
|
||||||
closeButton: false,
|
|
||||||
maxWidth: undefined,
|
|
||||||
offset: {
|
|
||||||
'top': [0, 0],
|
|
||||||
'top-left': [0, 0],
|
|
||||||
'top-right': [0, 0],
|
|
||||||
'bottom': [0, -30],
|
|
||||||
'bottom-left': [0, -30],
|
|
||||||
'bottom-right': [0, -30],
|
|
||||||
'left': [0, 0],
|
|
||||||
'right': [0, 0]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
|
|
||||||
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
Trash2,
|
Trash2,
|
||||||
Move,
|
Move,
|
||||||
Map,
|
Map,
|
||||||
Layers2
|
Layers2,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
currentOverlays,
|
currentOverlays,
|
||||||
previousOverlays,
|
previousOverlays,
|
||||||
customBasemapOrder,
|
customBasemapOrder,
|
||||||
customOverlayOrder
|
customOverlayOrder,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let name: string = '';
|
let name: string = '';
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
overlaySortable = Sortable.create(overlayContainer, {
|
overlaySortable = Sortable.create(overlayContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
basemapSortable.sort($customBasemapOrder);
|
basemapSortable.sort($customBasemapOrder);
|
||||||
@@ -108,6 +108,7 @@
|
|||||||
if (typeof maxZoom === 'string') {
|
if (typeof maxZoom === 'string') {
|
||||||
maxZoom = parseInt(maxZoom);
|
maxZoom = parseInt(maxZoom);
|
||||||
}
|
}
|
||||||
|
let is512 = tileUrls.some((url) => url.includes('512'));
|
||||||
|
|
||||||
let layerId = selectedLayerId ?? getLayerId();
|
let layerId = selectedLayerId ?? getLayerId();
|
||||||
let layer: CustomLayer = {
|
let layer: CustomLayer = {
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
layerType: layerType,
|
layerType: layerType,
|
||||||
resourceType: resourceType,
|
resourceType: resourceType,
|
||||||
value: ''
|
value: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resourceType === 'vector') {
|
if (resourceType === 'vector') {
|
||||||
@@ -129,17 +130,17 @@
|
|||||||
[layerId]: {
|
[layerId]: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: layer.tileUrls,
|
tiles: layer.tileUrls,
|
||||||
tileSize: 256,
|
tileSize: is512 ? 512 : 256,
|
||||||
maxzoom: maxZoom
|
maxzoom: maxZoom,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: layerId,
|
id: layerId,
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: layerId
|
source: layerId,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
$customLayers[layerId] = layer;
|
$customLayers[layerId] = layer;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { customBasemapUpdate, getLayers } from './utils';
|
import { customBasemapUpdate, getLayers } from './utils';
|
||||||
import { OverpassLayer } from './OverpassLayer';
|
import { OverpassLayer } from './OverpassLayer';
|
||||||
import OverpassPopup from './OverpassPopup.svelte';
|
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let overpassLayer: OverpassLayer;
|
let overpassLayer: OverpassLayer;
|
||||||
@@ -27,14 +26,14 @@
|
|||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
selectedOverpassTree,
|
selectedOverpassTree,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities
|
opacities,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
function setStyle() {
|
function setStyle() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||||
? basemaps[$currentBasemap]
|
? basemaps[$currentBasemap]
|
||||||
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
|
: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
|
||||||
$map.removeImport('basemap');
|
$map.removeImport('basemap');
|
||||||
if (typeof basemap === 'string') {
|
if (typeof basemap === 'string') {
|
||||||
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
$map.addImport(
|
$map.addImport(
|
||||||
{
|
{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
data: basemap
|
data: basemap,
|
||||||
},
|
},
|
||||||
'overlays'
|
'overlays'
|
||||||
);
|
);
|
||||||
@@ -71,12 +70,12 @@
|
|||||||
layer.paint['raster-opacity'] = $opacities[id];
|
layer.paint['raster-opacity'] = $opacities[id];
|
||||||
}
|
}
|
||||||
return layer;
|
return layer;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
$map.addImport({
|
$map.addImport({
|
||||||
id,
|
id,
|
||||||
data: overlay
|
data: overlay,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,18 +84,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOverlays() {
|
function updateOverlays() {
|
||||||
if ($map && $currentOverlays) {
|
if ($map && $currentOverlays && $opacities) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
try {
|
try {
|
||||||
let activeOverlays = $map
|
let activeOverlays = $map.getStyle().imports.reduce((acc, i) => {
|
||||||
.getStyle()
|
if (!['basemap', 'overlays', 'glyphs-and-sprite'].includes(i.id)) {
|
||||||
.imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
|
acc[i.id] = i;
|
||||||
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
|
}
|
||||||
toRemove.forEach((i) => {
|
return acc;
|
||||||
$map.removeImport(i.id);
|
}, {});
|
||||||
|
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
|
||||||
|
toRemove.forEach((id) => {
|
||||||
|
$map.removeImport(id);
|
||||||
});
|
});
|
||||||
let toAdd = Object.entries(overlayLayers)
|
let toAdd = Object.entries(overlayLayers)
|
||||||
.filter(([id, selected]) => selected && !activeOverlays.some((j) => j.id === id))
|
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
toAdd.forEach((id) => {
|
toAdd.forEach((id) => {
|
||||||
addOverlay(id);
|
addOverlay(id);
|
||||||
@@ -107,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && $currentOverlays) {
|
$: if ($map && $currentOverlays && $opacities) {
|
||||||
updateOverlays();
|
updateOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +132,9 @@
|
|||||||
});
|
});
|
||||||
currentBasemap.subscribe((value) => {
|
currentBasemap.subscribe((value) => {
|
||||||
// Updates coming from the database, or from the user swapping basemaps
|
// Updates coming from the database, or from the user swapping basemaps
|
||||||
|
if (value !== get(selectedBasemap)) {
|
||||||
selectedBasemap.set(value);
|
selectedBasemap.set(value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
@@ -209,8 +213,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
<OverpassPopup />
|
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
if (open && !cancelEvents && !container.contains(e.target)) {
|
if (open && !cancelEvents && !container.contains(e.target)) {
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
defaultBasemap,
|
defaultBasemap,
|
||||||
overlays,
|
overlays,
|
||||||
overlayTree,
|
overlayTree,
|
||||||
overpassTree
|
overpassTree,
|
||||||
} from '$lib/assets/layers';
|
} from '$lib/assets/layers';
|
||||||
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
|
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import CustomLayers from './CustomLayers.svelte';
|
import CustomLayers from './CustomLayers.svelte';
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
currentBasemap,
|
currentBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities
|
opacities,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
export let open: boolean;
|
export let open: boolean;
|
||||||
@@ -137,7 +137,9 @@
|
|||||||
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
|
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
|
||||||
{#each Object.keys(overlays) as id}
|
{#each Object.keys(overlays) as id}
|
||||||
{#if isSelected($selectedOverlayTree, id)}
|
{#if isSelected($selectedOverlayTree, id)}
|
||||||
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item>
|
<Select.Item value={id}
|
||||||
|
>{$_(`layers.label.${id}`)}</Select.Item
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#each Object.entries($customLayers) as [id, layer]}
|
{#each Object.entries($customLayers) as [id, layer]}
|
||||||
@@ -157,15 +159,22 @@
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
disabled={$selectedOverlay === undefined}
|
disabled={$selectedOverlay === undefined}
|
||||||
onValueChange={() => {
|
onValueChange={(value) => {
|
||||||
if ($selectedOverlay) {
|
if ($selectedOverlay) {
|
||||||
$opacities[$selectedOverlay.value] = $overlayOpacity[0];
|
if (
|
||||||
if ($map) {
|
$map &&
|
||||||
if ($map.getLayer($selectedOverlay.value)) {
|
isSelected(
|
||||||
$map.removeLayer($selectedOverlay.value);
|
$currentOverlays,
|
||||||
$currentOverlays = $currentOverlays;
|
$selectedOverlay.value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
$map.removeImport($selectedOverlay.value);
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$opacities[$selectedOverlay.value] = value[0];
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { type LayerTreeType } from '$lib/assets/layers';
|
import { type LayerTreeType } from '$lib/assets/layers';
|
||||||
import { anySelectedLayer } from './utils';
|
import { anySelectedLayer } from './utils';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { beforeUpdate } from 'svelte';
|
import { beforeUpdate } from 'svelte';
|
||||||
|
|
||||||
@@ -49,7 +49,13 @@
|
|||||||
aria-label={$_(`layers.label.${id}`)}
|
aria-label={$_(`layers.label.${id}`)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
|
<input
|
||||||
|
id="{name}-{id}"
|
||||||
|
type="radio"
|
||||||
|
{name}
|
||||||
|
value={id}
|
||||||
|
bind:group={selected}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||||
{#if $customLayers.hasOwnProperty(id)}
|
{#if $customLayers.hasOwnProperty(id)}
|
||||||
@@ -64,7 +70,13 @@
|
|||||||
<CollapsibleTreeNode {id}>
|
<CollapsibleTreeNode {id}>
|
||||||
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
|
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<svelte:self node={node[id]} {name} bind:selected {multiple} bind:checked={checked[id]} />
|
<svelte:self
|
||||||
|
node={node[id]}
|
||||||
|
{name}
|
||||||
|
bind:selected
|
||||||
|
{multiple}
|
||||||
|
bind:checked={checked[id]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTreeNode>
|
</CollapsibleTreeNode>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import SphericalMercator from "@mapbox/sphericalmercator";
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
import { getLayers } from "./utils";
|
import { getLayers } from './utils';
|
||||||
import mapboxgl from "mapbox-gl";
|
import { get, writable } from 'svelte/store';
|
||||||
import { get, writable } from "svelte/store";
|
import { liveQuery } from 'dexie';
|
||||||
import { liveQuery } from "dexie";
|
import { db, settings } from '$lib/db';
|
||||||
import { db, settings } from "$lib/db";
|
import { overpassQueryData } from '$lib/assets/layers';
|
||||||
import { overpassQueryData } from "$lib/assets/layers";
|
import { MapPopup } from '$lib/components/MapPopup';
|
||||||
|
|
||||||
const {
|
const { currentOverpassQueries } = settings;
|
||||||
currentOverpassQueries
|
|
||||||
} = settings;
|
|
||||||
|
|
||||||
const mercator = new SphericalMercator({
|
const mercator = new SphericalMercator({
|
||||||
size: 256,
|
size: 256,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const overpassPopupPOI = writable<Record<string, any> | null>(null);
|
|
||||||
|
|
||||||
export const overpassPopup = new mapboxgl.Popup({
|
|
||||||
closeButton: false,
|
|
||||||
maxWidth: undefined,
|
|
||||||
offset: 15,
|
|
||||||
});
|
|
||||||
|
|
||||||
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||||
|
|
||||||
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||||
@@ -34,28 +24,36 @@ export class OverpassLayer {
|
|||||||
queryZoom = 12;
|
queryZoom = 12;
|
||||||
expirationTime = 7 * 24 * 3600 * 1000;
|
expirationTime = 7 * 24 * 3600 * 1000;
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
|
popup: MapPopup;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map();
|
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
|
||||||
|
|
||||||
unsubscribes: (() => void)[] = [];
|
unsubscribes: (() => void)[] = [];
|
||||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||||
updateBinded = this.update.bind(this);
|
updateBinded = this.update.bind(this);
|
||||||
onHoverBinded = this.onHover.bind(this);
|
onHoverBinded = this.onHover.bind(this);
|
||||||
maybeHidePopupBinded = this.maybeHidePopup.bind(this);
|
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.popup = new MapPopup(map, {
|
||||||
|
closeButton: false,
|
||||||
|
focusAfterOpen: false,
|
||||||
|
maxWidth: undefined,
|
||||||
|
offset: 15,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.map.on('moveend', this.queryIfNeededBinded);
|
this.map.on('moveend', this.queryIfNeededBinded);
|
||||||
this.map.on('style.import.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
this.unsubscribes.push(
|
||||||
|
currentOverpassQueries.subscribe(() => {
|
||||||
this.updateBinded();
|
this.updateBinded();
|
||||||
this.queryIfNeededBinded();
|
this.queryIfNeededBinded();
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@@ -125,27 +123,12 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onHover(e: any) {
|
onHover(e: any) {
|
||||||
overpassPopupPOI.set({
|
this.popup.setItem({
|
||||||
|
item: {
|
||||||
...e.features[0].properties,
|
...e.features[0].properties,
|
||||||
sym: overpassQueryData[e.features[0].properties.query].symbol ?? ''
|
sym: overpassQueryData[e.features[0].properties.query].symbol ?? '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
overpassPopup.setLngLat(e.features[0].geometry.coordinates);
|
|
||||||
overpassPopup.addTo(this.map);
|
|
||||||
this.map.on('mousemove', this.maybeHidePopupBinded);
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeHidePopup(e: any) {
|
|
||||||
let poi = get(overpassPopupPOI);
|
|
||||||
if (poi && this.map.project([poi.lon, poi.lat]).dist(this.map.project(e.lngLat)) > 100) {
|
|
||||||
this.hideWaypointPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hideWaypointPopup() {
|
|
||||||
overpassPopupPOI.set(null);
|
|
||||||
overpassPopup.remove();
|
|
||||||
|
|
||||||
this.map.off('mousemove', this.maybeHidePopupBinded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query(bbox: [number, number, number, number]) {
|
query(bbox: [number, number, number, number]) {
|
||||||
@@ -163,8 +146,19 @@ export class OverpassLayer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.overpasstiles.where('[x+y]').equals([x, y]).toArray().then((querytiles) => {
|
db.overpasstiles
|
||||||
let missingQueries = queries.filter((query) => !querytiles.some((querytile) => querytile.query === query && time - querytile.time < this.expirationTime));
|
.where('[x+y]')
|
||||||
|
.equals([x, y])
|
||||||
|
.toArray()
|
||||||
|
.then((querytiles) => {
|
||||||
|
let missingQueries = queries.filter(
|
||||||
|
(query) =>
|
||||||
|
!querytiles.some(
|
||||||
|
(querytile) =>
|
||||||
|
querytile.query === query &&
|
||||||
|
time - querytile.time < this.expirationTime
|
||||||
|
)
|
||||||
|
);
|
||||||
if (missingQueries.length > 0) {
|
if (missingQueries.length > 0) {
|
||||||
this.queryTile(x, y, missingQueries);
|
this.queryTile(x, y, missingQueries);
|
||||||
}
|
}
|
||||||
@@ -182,13 +176,16 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
const bounds = mercator.bbox(x, y, this.queryZoom);
|
||||||
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
||||||
.then((response) => {
|
.then(
|
||||||
|
(response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
this.currentQueries.delete(`${x},${y}`);
|
this.currentQueries.delete(`${x},${y}`);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}, () => (this.currentQueries.delete(`${x},${y}`)))
|
},
|
||||||
|
() => this.currentQueries.delete(`${x},${y}`)
|
||||||
|
)
|
||||||
.then((data) => this.storeOverpassData(x, y, queries, data))
|
.then((data) => this.storeOverpassData(x, y, queries, data))
|
||||||
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
||||||
}
|
}
|
||||||
@@ -196,7 +193,7 @@ export class OverpassLayer {
|
|||||||
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
||||||
let time = Date.now();
|
let time = Date.now();
|
||||||
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
||||||
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = [];
|
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
|
||||||
|
|
||||||
if (data.elements === undefined) {
|
if (data.elements === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -212,7 +209,9 @@ export class OverpassLayer {
|
|||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: element.center ? [element.center.lon, element.center.lat] : [element.lon, element.lat],
|
coordinates: element.center
|
||||||
|
? [element.center.lon, element.center.lat]
|
||||||
|
: [element.lon, element.lat],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
id: element.id,
|
id: element.id,
|
||||||
@@ -220,9 +219,10 @@ export class OverpassLayer {
|
|||||||
lon: element.center ? element.center.lon : element.lon,
|
lon: element.center ? element.center.lon : element.lon,
|
||||||
query: query,
|
query: query,
|
||||||
icon: `overpass-${query}`,
|
icon: `overpass-${query}`,
|
||||||
tags: element.tags
|
tags: element.tags,
|
||||||
|
type: element.type,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,11 +245,13 @@ export class OverpassLayer {
|
|||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
this.map.addImage(`overpass-${query}`, icon);
|
this.map.addImage(`overpass-${query}`, icon);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
// Lucide icons are SVG files with a 24x24 viewBox
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||||
icon.src = 'data:image/svg+xml,' + encodeURIComponent(`
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
@@ -281,9 +283,14 @@ function getQuery(query: string) {
|
|||||||
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
||||||
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
||||||
if (arrayEntry !== undefined) {
|
if (arrayEntry !== undefined) {
|
||||||
return arrayEntry[1].map((val) => `nwr${Object.entries(tags)
|
return arrayEntry[1]
|
||||||
|
.map(
|
||||||
|
(val) =>
|
||||||
|
`nwr${Object.entries(tags)
|
||||||
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
||||||
.join('')};`).join('');
|
.join('')};`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
} else {
|
} else {
|
||||||
return `nwr${Object.entries(tags)
|
return `nwr${Object.entries(tags)
|
||||||
.map(([tag, value]) => `[${tag}=${value}]`)
|
.map(([tag, value]) => `[${tag}=${value}]`)
|
||||||
@@ -300,8 +307,9 @@ function belongsToQuery(element: any, query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
||||||
return Object.entries(tags)
|
return Object.entries(tags).every(([tag, value]) =>
|
||||||
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value);
|
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentQueries() {
|
function getCurrentQueries() {
|
||||||
@@ -310,5 +318,7 @@ function getCurrentQueries() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query);
|
return Object.entries(getLayers(currentQueries))
|
||||||
|
.filter(([_, selected]) => selected)
|
||||||
|
.map(([query, _]) => query);
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,52 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
|
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { PencilLine, MapPin } from 'lucide-svelte';
|
import { PencilLine, MapPin } from 'lucide-svelte';
|
||||||
import { onMount } from 'svelte';
|
import { _ } from '$lib/i18n';
|
||||||
import { _ } from 'svelte-i18n';
|
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
import type { WaypointType } from 'gpx';
|
||||||
|
|
||||||
let popupElement: HTMLDivElement;
|
export let poi: PopupItem<any>;
|
||||||
|
|
||||||
onMount(() => {
|
let tags: { [key: string]: string } = {};
|
||||||
overpassPopup.setDOMContent(popupElement);
|
|
||||||
popupElement.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
let tags = {};
|
|
||||||
let name = '';
|
let name = '';
|
||||||
$: if ($overpassPopupPOI) {
|
$: if (poi) {
|
||||||
tags = JSON.parse($overpassPopupPOI.tags);
|
tags = JSON.parse(poi.item.tags);
|
||||||
if (tags.name !== undefined && tags.name !== '') {
|
if (tags.name !== undefined && tags.name !== '') {
|
||||||
name = tags.name;
|
name = tags.name;
|
||||||
} else {
|
} else {
|
||||||
name = $_(`layers.label.${$overpassPopupPOI.query}`);
|
name = $_(`layers.label.${poi.item.query}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addToFile() {
|
||||||
|
const desc = Object.entries(tags)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join('\n');
|
||||||
|
let wpt: WaypointType = {
|
||||||
|
attributes: {
|
||||||
|
lat: poi.item.lat,
|
||||||
|
lon: poi.item.lon,
|
||||||
|
},
|
||||||
|
name: name,
|
||||||
|
desc: desc,
|
||||||
|
cmt: desc,
|
||||||
|
sym: poi.item.sym,
|
||||||
|
};
|
||||||
|
if (tags.website) {
|
||||||
|
wpt.link = {
|
||||||
|
attributes: {
|
||||||
|
href: tags.website,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
dbUtils.addOrUpdateWaypoint(wpt);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={popupElement} class="hidden">
|
|
||||||
{#if $overpassPopupPOI}
|
|
||||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
|
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
|
||||||
<Card.Header class="p-0">
|
<Card.Header class="p-0">
|
||||||
<Card.Title class="text-md">
|
<Card.Title class="text-md">
|
||||||
@@ -36,13 +54,14 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{name}
|
{name}
|
||||||
<div class="text-muted-foreground text-sm font-normal">
|
<div class="text-muted-foreground text-sm font-normal">
|
||||||
{$overpassPopupPOI.lat.toFixed(6)}° {$overpassPopupPOI.lon.toFixed(6)}°
|
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
class="ml-auto p-1.5 h-8"
|
class="ml-auto p-1.5 h-8"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
href="https://www.openstreetmap.org/edit?editor=id&node={$overpassPopupPOI.id}"
|
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
|
||||||
|
'node'}={poi.item.id}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<PencilLine size="16" />
|
<PencilLine size="16" />
|
||||||
@@ -50,18 +69,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
|
||||||
|
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
|
||||||
{#if tags.image || tags['image:0']}
|
{#if tags.image || tags['image:0']}
|
||||||
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
<img src={tags.image ?? tags['image:0']} />
|
<img src={tags.image ?? tags['image:0']} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
|
|
||||||
<div class="grid grid-cols-[auto_auto] gap-x-3">
|
<div class="grid grid-cols-[auto_auto] gap-x-3">
|
||||||
{#each Object.entries(tags) as [key, value]}
|
{#each Object.entries(tags) as [key, value]}
|
||||||
{#if key !== 'name' && !key.includes('image')}
|
{#if key !== 'name' && !key.includes('image')}
|
||||||
<span class="font-mono">{key}</span>
|
<span class="font-mono">{key}</span>
|
||||||
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
{#if key === 'website' || key.startsWith('website:') || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
||||||
<a href={value} target="_blank" class="text-link underline">{value}</a>
|
<a href={value} target="_blank" class="text-link underline">{value}</a>
|
||||||
{:else if key === 'phone' || key === 'contact:phone'}
|
{:else if key === 'phone' || key === 'contact:phone'}
|
||||||
<a href={'tel:' + value} class="text-link underline">{value}</a>
|
<a href={'tel:' + value} class="text-link underline">{value}</a>
|
||||||
@@ -73,30 +93,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
<Button
|
<Button
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={$selection.size === 0}
|
disabled={$selection.size === 0}
|
||||||
on:click={() => {
|
on:click={addToFile}
|
||||||
let desc = Object.entries(tags)
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join('\n');
|
|
||||||
dbUtils.addOrUpdateWaypoint({
|
|
||||||
attributes: {
|
|
||||||
lat: $overpassPopupPOI.lat,
|
|
||||||
lon: $overpassPopupPOI.lon
|
|
||||||
},
|
|
||||||
name: name,
|
|
||||||
desc: desc,
|
|
||||||
cmt: desc,
|
|
||||||
sym: $overpassPopupPOI.sym
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<MapPin size="16" class="mr-1" />
|
<MapPin size="16" class="mr-1" />
|
||||||
{$_('toolbar.waypoint.add')}
|
{$_('toolbar.waypoint.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { LayerTreeType } from "$lib/assets/layers";
|
import type { LayerTreeType } from '$lib/assets/layers';
|
||||||
import { writable } from "svelte/store";
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export function anySelectedLayer(node: LayerTreeType) {
|
export function anySelectedLayer(node: LayerTreeType) {
|
||||||
return Object.keys(node).find((id) => {
|
return (
|
||||||
if (typeof node[id] == "boolean") {
|
Object.keys(node).find((id) => {
|
||||||
|
if (typeof node[id] == 'boolean') {
|
||||||
if (node[id]) {
|
if (node[id]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -13,12 +14,16 @@ export function anySelectedLayer(node: LayerTreeType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}) !== undefined;
|
}) !== undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLayers(node: LayerTreeType, layers: { [key: string]: boolean } = {}): { [key: string]: boolean } {
|
export function getLayers(
|
||||||
|
node: LayerTreeType,
|
||||||
|
layers: { [key: string]: boolean } = {}
|
||||||
|
): { [key: string]: boolean } {
|
||||||
Object.keys(node).forEach((id) => {
|
Object.keys(node).forEach((id) => {
|
||||||
if (typeof node[id] == "boolean") {
|
if (typeof node[id] == 'boolean') {
|
||||||
layers[id] = node[id];
|
layers[id] = node[id];
|
||||||
} else {
|
} else {
|
||||||
getLayers(node[id], layers);
|
getLayers(node[id], layers);
|
||||||
@@ -32,7 +37,7 @@ export function isSelected(node: LayerTreeType, id: string) {
|
|||||||
if (key === id) {
|
if (key === id) {
|
||||||
return node[key];
|
return node[key];
|
||||||
}
|
}
|
||||||
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) {
|
if (typeof node[key] !== 'boolean' && isSelected(node[key], id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -43,7 +48,7 @@ export function toggle(node: LayerTreeType, id: string) {
|
|||||||
Object.keys(node).forEach((key) => {
|
Object.keys(node).forEach((key) => {
|
||||||
if (key === id) {
|
if (key === id) {
|
||||||
node[key] = !node[key];
|
node[key] = !node[key];
|
||||||
} else if (typeof node[key] !== "boolean") {
|
} else if (typeof node[key] !== 'boolean') {
|
||||||
toggle(node[key], id);
|
toggle(node[key], id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resetCursor, setCrosshairCursor } from "$lib/utils";
|
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import type mapboxgl from "mapbox-gl";
|
import type mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
export class GoogleRedirect {
|
export class GoogleRedirect {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
|
||||||
import { Viewer } from 'mapillary-js/dist/mapillary.module';
|
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { resetCursor, setPointerCursor } from "$lib/utils";
|
import { resetCursor, setPointerCursor } from '$lib/utils';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
const mapillarySource = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
tiles: ['https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011'],
|
tiles: [
|
||||||
|
'https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||||
|
],
|
||||||
minzoom: 6,
|
minzoom: 6,
|
||||||
maxzoom: 14,
|
maxzoom: 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapillarySequenceLayer = {
|
const mapillarySequenceLayer: LayerSpecification = {
|
||||||
id: 'mapillary-sequence',
|
id: 'mapillary-sequence',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'mapillary',
|
source: 'mapillary',
|
||||||
@@ -26,7 +29,7 @@ const mapillarySequenceLayer = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapillaryImageLayer = {
|
const mapillaryImageLayer: LayerSpecification = {
|
||||||
id: 'mapillary-image',
|
id: 'mapillary-image',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'mapillary',
|
source: 'mapillary',
|
||||||
@@ -40,35 +43,56 @@ const mapillaryImageLayer = {
|
|||||||
|
|
||||||
export class MapillaryLayer {
|
export class MapillaryLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
popup: mapboxgl.Popup;
|
marker: mapboxgl.Marker;
|
||||||
viewer: Viewer;
|
viewer: Viewer;
|
||||||
|
|
||||||
|
active = false;
|
||||||
|
popupOpen: Writable<boolean>;
|
||||||
|
|
||||||
addBinded = this.add.bind(this);
|
addBinded = this.add.bind(this);
|
||||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, container: HTMLElement) {
|
constructor(map: mapboxgl.Map, container: HTMLElement, popupOpen: Writable<boolean>) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
|
||||||
this.viewer = new Viewer({
|
this.viewer = new Viewer({
|
||||||
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||||
container,
|
container,
|
||||||
});
|
});
|
||||||
container.classList.remove('hidden');
|
|
||||||
|
|
||||||
this.popup = new mapboxgl.Popup({
|
const element = document.createElement('div');
|
||||||
closeButton: false,
|
element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
|
||||||
maxWidth: container.style.width,
|
const dot = document.createElement('div');
|
||||||
}).setDOMContent(container);
|
dot.className = 'mapboxgl-user-location-dot';
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.className = 'mapboxgl-user-location-heading';
|
||||||
|
element.appendChild(dot);
|
||||||
|
element.appendChild(heading);
|
||||||
|
|
||||||
|
this.marker = new mapboxgl.Marker({
|
||||||
|
rotationAlignment: 'map',
|
||||||
|
element,
|
||||||
|
});
|
||||||
|
|
||||||
this.viewer.on('position', async () => {
|
this.viewer.on('position', async () => {
|
||||||
if (this.popup.isOpen()) {
|
if (this.active) {
|
||||||
|
popupOpen.set(true);
|
||||||
let latLng = await this.viewer.getPosition();
|
let latLng = await this.viewer.getPosition();
|
||||||
this.popup.setLngLat(latLng);
|
this.marker.setLngLat(latLng).addTo(this.map);
|
||||||
if (!this.map.getBounds().contains(latLng)) {
|
if (!this.map.getBounds()?.contains(latLng)) {
|
||||||
this.map.panTo(latLng);
|
this.map.panTo(latLng);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.viewer.on('bearing', (e: ViewerBearingEvent) => {
|
||||||
|
if (this.active) {
|
||||||
|
this.marker.setRotation(e.bearing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.popupOpen = popupOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
@@ -101,15 +125,19 @@ export class MapillaryLayer {
|
|||||||
this.map.removeSource('mapillary');
|
this.map.removeSource('mapillary');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popup.remove();
|
this.marker.remove();
|
||||||
|
this.popupOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
closePopup() {
|
closePopup() {
|
||||||
this.popup.remove();
|
this.active = false;
|
||||||
|
this.marker.remove();
|
||||||
|
this.popupOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter(e: mapboxgl.MapLayerMouseEvent) {
|
onMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||||
this.popup.addTo(this.map).setLngLat(e.lngLat);
|
this.active = true;
|
||||||
|
|
||||||
this.viewer.resize();
|
this.viewer.resize();
|
||||||
this.viewer.moveTo(e.features[0].properties.id);
|
this.viewer.moveTo(e.features[0].properties.id);
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,19 @@
|
|||||||
import { GoogleRedirect } from './Google';
|
import { GoogleRedirect } from './Google';
|
||||||
import { map, streetViewEnabled } from '$lib/stores';
|
import { map, streetViewEnabled } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
const { streetViewSource } = settings;
|
const { streetViewSource } = settings;
|
||||||
|
|
||||||
let googleRedirect: GoogleRedirect;
|
let googleRedirect: GoogleRedirect;
|
||||||
let mapillaryLayer: MapillaryLayer;
|
let mapillaryLayer: MapillaryLayer;
|
||||||
|
let mapillaryOpen = writable(false);
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
googleRedirect = new GoogleRedirect($map);
|
googleRedirect = new GoogleRedirect($map);
|
||||||
mapillaryLayer = new MapillaryLayer($map, container);
|
mapillaryLayer = new MapillaryLayer($map, container, mapillaryOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (mapillaryLayer) {
|
$: if (mapillaryLayer) {
|
||||||
@@ -53,7 +55,9 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="hidden relative w-[50vw] h-[40vh] rounded-md border-background border-2"
|
class="{$mapillaryOpen
|
||||||
|
? ''
|
||||||
|
: 'hidden'} !absolute bottom-[44px] right-2.5 z-10 w-[40%] h-[40%] bg-background rounded-md overflow-hidden border-background border-2"
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
MapPin,
|
MapPin,
|
||||||
Filter,
|
Filter,
|
||||||
Scissors,
|
Scissors,
|
||||||
MountainSnow
|
MountainSnow,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
import ToolbarItemMenu from './ToolbarItemMenu.svelte';
|
import ToolbarItemMenu from './ToolbarItemMenu.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
popup = new mapboxgl.Popup({
|
popup = new mapboxgl.Popup({
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
maxWidth: undefined
|
maxWidth: undefined,
|
||||||
});
|
});
|
||||||
popup.setDOMContent(popupElement);
|
popup.setDOMContent(popupElement);
|
||||||
popupElement.classList.remove('hidden');
|
popupElement.classList.remove('hidden');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
enum CleanType {
|
enum CleanType {
|
||||||
INSIDE = 'inside',
|
INSIDE = 'inside',
|
||||||
OUTSIDE = 'outside'
|
OUTSIDE = 'outside',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { Trash2 } from 'lucide-svelte';
|
import { Trash2 } from 'lucide-svelte';
|
||||||
@@ -41,10 +41,10 @@
|
|||||||
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
|
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
|
||||||
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
|
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
|
||||||
[rectangleCoordinates[0].lng, rectangleCoordinates[1].lat],
|
[rectangleCoordinates[0].lng, rectangleCoordinates[1].lat],
|
||||||
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat]
|
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
let source = $map.getSource('rectangle');
|
let source = $map.getSource('rectangle');
|
||||||
if (source) {
|
if (source) {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
} else {
|
} else {
|
||||||
$map.addSource('rectangle', {
|
$map.addSource('rectangle', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: data
|
data: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('rectangle')) {
|
if (!$map.getLayer('rectangle')) {
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
source: 'rectangle',
|
source: 'rectangle',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': 'SteelBlue',
|
'fill-color': 'SteelBlue',
|
||||||
'fill-opacity': 0.5
|
'fill-opacity': 0.5,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,12 +161,12 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
||||||
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
|
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
||||||
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
|
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
cleanType === CleanType.INSIDE,
|
cleanType === CleanType.INSIDE,
|
||||||
deleteTrackpoints,
|
deleteTrackpoints,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { MountainSnow } from 'lucide-svelte';
|
import { MountainSnow } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection = $selection.size > 0;
|
$: validSelection = $selection.size > 0;
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
ListWaypointsItem
|
ListWaypointsItem,
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection =
|
$: validSelection =
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
enum MergeType {
|
enum MergeType {
|
||||||
TRACES = 'traces',
|
TRACES = 'traces',
|
||||||
CONTENTS = 'contents'
|
CONTENTS = 'contents',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -11,15 +11,18 @@
|
|||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { Group } from 'lucide-svelte';
|
import { Group } from 'lucide-svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
import { gpxStatistics } from '$lib/stores';
|
||||||
|
|
||||||
let canMergeTraces = false;
|
let canMergeTraces = false;
|
||||||
let canMergeContents = false;
|
let canMergeContents = false;
|
||||||
|
let removeGaps = false;
|
||||||
|
|
||||||
$: if ($selection.size > 1) {
|
$: if ($selection.size > 1) {
|
||||||
canMergeTraces = true;
|
canMergeTraces = true;
|
||||||
@@ -56,22 +59,31 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<RadioGroup.Root bind:value={mergeType}>
|
<RadioGroup.Root bind:value={mergeType}>
|
||||||
<Label class="flex flex-row items-center gap-2 leading-5">
|
<Label class="flex flex-row items-center gap-1.5 leading-5">
|
||||||
<RadioGroup.Item value={MergeType.TRACES} />
|
<RadioGroup.Item value={MergeType.TRACES} />
|
||||||
{$_('toolbar.merge.merge_traces')}
|
{$_('toolbar.merge.merge_traces')}
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row items-center gap-2 leading-5">
|
<Label class="flex flex-row items-center gap-1.5 leading-5">
|
||||||
<RadioGroup.Item value={MergeType.CONTENTS} />
|
<RadioGroup.Item value={MergeType.CONTENTS} />
|
||||||
{$_('toolbar.merge.merge_contents')}
|
{$_('toolbar.merge.merge_contents')}
|
||||||
</Label>
|
</Label>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
|
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
|
||||||
|
<div class="flex flex-row items-center gap-1.5">
|
||||||
|
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
|
||||||
|
<Label for="remove-gaps">{$_('toolbar.merge.remove_gaps')}</Label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
||||||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
|
dbUtils.mergeSelection(
|
||||||
|
mergeType === MergeType.TRACES,
|
||||||
|
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group size="16" class="mr-1 shrink-0" />
|
<Group size="16" class="mr-1 shrink-0" />
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
import {
|
||||||
|
ListItem,
|
||||||
|
ListRootItem,
|
||||||
|
ListTrackSegmentItem,
|
||||||
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { Filter } from 'lucide-svelte';
|
import { Filter } from 'lucide-svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { dbUtils, fileObservers } from '$lib/db';
|
import { dbUtils, fileObservers } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
@@ -18,10 +22,13 @@
|
|||||||
let sliderValue = [50];
|
let sliderValue = [50];
|
||||||
let maxPoints = 0;
|
let maxPoints = 0;
|
||||||
let currentPoints = 0;
|
let currentPoints = 0;
|
||||||
|
const minTolerance = 0.1;
|
||||||
|
const maxTolerance = 10000;
|
||||||
|
|
||||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
|
|
||||||
$: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000)));
|
$: tolerance =
|
||||||
|
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)));
|
||||||
|
|
||||||
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
|
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
|
||||||
let unsubscribes = new Map<string, () => void>();
|
let unsubscribes = new Map<string, () => void>();
|
||||||
@@ -32,7 +39,7 @@
|
|||||||
|
|
||||||
let data: GeoJSON.FeatureCollection = {
|
let data: GeoJSON.FeatureCollection = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
||||||
@@ -49,10 +56,10 @@
|
|||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: current.map((point) => [
|
coordinates: current.map((point) => [
|
||||||
point.point.getLongitude(),
|
point.point.getLongitude(),
|
||||||
point.point.getLatitude()
|
point.point.getLatitude(),
|
||||||
])
|
]),
|
||||||
},
|
},
|
||||||
properties: {}
|
properties: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +70,7 @@
|
|||||||
} else {
|
} else {
|
||||||
$map.addSource('simplified', {
|
$map.addSource('simplified', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: data
|
data: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('simplified')) {
|
if (!$map.getLayer('simplified')) {
|
||||||
@@ -73,8 +80,8 @@
|
|||||||
source: 'simplified',
|
source: 'simplified',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': 'white',
|
'line-color': 'white',
|
||||||
'line-width': 3
|
'line-width': 3,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$map.moveLayer('simplified');
|
$map.moveLayer('simplified');
|
||||||
@@ -91,17 +98,23 @@
|
|||||||
});
|
});
|
||||||
$fileObservers.forEach((fileStore, fileId) => {
|
$fileObservers.forEach((fileStore, fileId) => {
|
||||||
if (!unsubscribes.has(fileId)) {
|
if (!unsubscribes.has(fileId)) {
|
||||||
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
|
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
|
||||||
([fs, sel]) => {
|
fs,
|
||||||
|
sel,
|
||||||
|
]).subscribe(([fs, sel]) => {
|
||||||
if (fs) {
|
if (fs) {
|
||||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
|
let segmentItem = new ListTrackSegmentItem(
|
||||||
|
fileId,
|
||||||
|
trackIndex,
|
||||||
|
segmentIndex
|
||||||
|
);
|
||||||
if (sel.hasAnyParent(segmentItem)) {
|
if (sel.hasAnyParent(segmentItem)) {
|
||||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||||
simplified.set(segmentItem.getFullId(), [
|
simplified.set(segmentItem.getFullId(), [
|
||||||
segmentItem,
|
segmentItem,
|
||||||
statistics.local.points.length,
|
statistics.local.points.length,
|
||||||
ramerDouglasPeucker(statistics.local.points, 1)
|
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||||
]);
|
]);
|
||||||
update();
|
update();
|
||||||
} else if (simplified.has(segmentItem.getFullId())) {
|
} else if (simplified.has(segmentItem.getFullId())) {
|
||||||
@@ -110,8 +123,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
unsubscribes.set(fileId, unsubscribe);
|
unsubscribes.set(fileId, unsubscribe);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -154,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.tolerance')}</span>
|
<span>{$_('toolbar.reduce.tolerance')}</span>
|
||||||
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
|
<WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
distancePerHourToSecondsPerDistance,
|
distancePerHourToSecondsPerDistance,
|
||||||
getConvertedVelocity,
|
getConvertedVelocity,
|
||||||
milesToKilometers,
|
milesToKilometers,
|
||||||
nauticalMilesToKilometers
|
nauticalMilesToKilometers,
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||||
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListTrackSegmentItem
|
ListTrackSegmentItem,
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
@@ -69,14 +69,14 @@
|
|||||||
endDate = undefined;
|
endDate = undefined;
|
||||||
endTime = undefined;
|
endTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.moving) {
|
if ($gpxStatistics.global.time.moving && $gpxStatistics.global.speed.moving) {
|
||||||
movingTime = $gpxStatistics.global.time.moving;
|
movingTime = $gpxStatistics.global.time.moving;
|
||||||
|
setSpeed($gpxStatistics.global.speed.moving);
|
||||||
|
} else if ($gpxStatistics.global.time.total && $gpxStatistics.global.speed.total) {
|
||||||
|
movingTime = $gpxStatistics.global.time.total;
|
||||||
|
setSpeed($gpxStatistics.global.speed.total);
|
||||||
} else {
|
} else {
|
||||||
movingTime = undefined;
|
movingTime = undefined;
|
||||||
}
|
|
||||||
if ($gpxStatistics.global.speed.moving) {
|
|
||||||
setSpeed($gpxStatistics.global.speed.moving);
|
|
||||||
} else {
|
|
||||||
speed = undefined;
|
speed = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,11 @@
|
|||||||
class="grow whitespace-normal h-fit"
|
class="grow whitespace-normal h-fit"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let effectiveSpeed = getSpeed();
|
let effectiveSpeed = getSpeed();
|
||||||
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
if (
|
||||||
|
startDate === undefined ||
|
||||||
|
startTime === undefined ||
|
||||||
|
effectiveSpeed === undefined
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,13 +329,20 @@
|
|||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
dbUtils.applyToFile(fileId, (file) => {
|
dbUtils.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (artificial) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
|
file.createArtificialTimestamps(
|
||||||
|
getDate(startDate, startTime),
|
||||||
|
movingTime
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
file.changeTimestamps(
|
||||||
|
getDate(startDate, startTime),
|
||||||
|
effectiveSpeed,
|
||||||
|
ratio
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
if (artificial) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate, startTime),
|
getDate(startDate, startTime),
|
||||||
movingTime,
|
movingTime,
|
||||||
@@ -346,7 +357,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
if (artificial) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate, startTime),
|
getDate(startDate, startTime),
|
||||||
movingTime,
|
movingTime,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { Waypoint } from 'gpx';
|
import { Waypoint } from 'gpx';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { ListWaypointItem } from '$lib/components/file-list/FileList';
|
import { ListWaypointItem } from '$lib/components/file-list/FileList';
|
||||||
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
@@ -31,14 +31,14 @@
|
|||||||
|
|
||||||
let selectedSymbol = {
|
let selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { verticalFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
$: canCreate = $selection.size > 0;
|
$: canCreate = $selection.size > 0;
|
||||||
|
|
||||||
$: if ($verticalFileView && $selection) {
|
$: if ($treeFileView && $selection) {
|
||||||
selectedWaypoint.update(() => {
|
selectedWaypoint.update(() => {
|
||||||
if ($selection.size === 1) {
|
if ($selection.size === 1) {
|
||||||
let item = $selection.getSelected()[0];
|
let item = $selection.getSelected()[0];
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
if (symbolKey) {
|
if (symbolKey) {
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: symbol,
|
value: symbol,
|
||||||
label: $_(`gpx.symbol.${symbolKey}`)
|
label: $_(`gpx.symbol.${symbolKey}`),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: symbol,
|
value: symbol,
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
link = '';
|
link = '';
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
@@ -134,13 +134,13 @@
|
|||||||
{
|
{
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: latitude,
|
lat: latitude,
|
||||||
lon: longitude
|
lon: longitude,
|
||||||
},
|
},
|
||||||
name: name.length > 0 ? name : undefined,
|
name: name.length > 0 ? name : undefined,
|
||||||
desc: description.length > 0 ? description : undefined,
|
desc: description.length > 0 ? description : undefined,
|
||||||
cmt: description.length > 0 ? description : undefined,
|
cmt: description.length > 0 ? description : undefined,
|
||||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||||
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
|
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined,
|
||||||
},
|
},
|
||||||
$selectedWaypoint
|
$selectedWaypoint
|
||||||
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
|
||||||
@@ -195,7 +195,11 @@
|
|||||||
/>
|
/>
|
||||||
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
||||||
<Select.Root bind:selected={selectedSymbol}>
|
<Select.Root bind:selected={selectedSymbol}>
|
||||||
<Select.Trigger id="symbol" class="w-full h-8" disabled={!canCreate && !$selectedWaypoint}>
|
<Select.Trigger
|
||||||
|
id="symbol"
|
||||||
|
class="w-full h-8"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
|
>
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
@@ -218,7 +222,12 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
||||||
<Input bind:value={link} id="link" class="h-8" disabled={!canCreate && !$selectedWaypoint} />
|
<Input
|
||||||
|
bind:value={link}
|
||||||
|
id="link"
|
||||||
|
class="h-8"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
|
/>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
RouteOff,
|
RouteOff,
|
||||||
Repeat,
|
Repeat,
|
||||||
SquareArrowUpLeft,
|
SquareArrowUpLeft,
|
||||||
SquareArrowOutDownRight
|
SquareArrowOutDownRight,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
|
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
|
||||||
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
||||||
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
||||||
|
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { RoutingControls } from './RoutingControls';
|
import { RoutingControls } from './RoutingControls';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
ListRootItem,
|
ListRootItem,
|
||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
type ListItem
|
type ListItem,
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
@@ -68,7 +68,10 @@
|
|||||||
// add controls for new files
|
// add controls for new files
|
||||||
$fileObservers.forEach((file, fileId) => {
|
$fileObservers.forEach((file, fileId) => {
|
||||||
if (!routingControls.has(fileId)) {
|
if (!routingControls.has(fileId)) {
|
||||||
routingControls.set(fileId, new RoutingControls($map, fileId, file, popup, popupElement));
|
routingControls.set(
|
||||||
|
fileId,
|
||||||
|
new RoutingControls($map, fileId, file, popup, popupElement)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,9 +85,9 @@
|
|||||||
new TrackPoint({
|
new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: e.lngLat.lat,
|
lat: e.lngLat.lat,
|
||||||
lon: e.lngLat.lng
|
lon: e.lngLat.lng,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
file._data.id = getFileIds(1)[0];
|
file._data.id = getFileIds(1)[0];
|
||||||
dbUtils.add(file);
|
dbUtils.add(file);
|
||||||
@@ -195,7 +198,8 @@
|
|||||||
if (selected[0] instanceof ListFileItem) {
|
if (selected[0] instanceof ListFileItem) {
|
||||||
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
|
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
|
||||||
} else if (selected[0] instanceof ListTrackItem) {
|
} else if (selected[0] instanceof ListTrackItem) {
|
||||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0];
|
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]
|
||||||
|
?.trkpt[0];
|
||||||
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
||||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
||||||
selected[0].getSegmentIndex()
|
selected[0].getSegmentIndex()
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { Coordinates } from "gpx";
|
import type { Coordinates } from 'gpx';
|
||||||
import { TrackPoint, distance } from "gpx";
|
import { TrackPoint, distance } from 'gpx';
|
||||||
import { derived, get, writable } from "svelte/store";
|
import { derived, get, writable } from 'svelte/store';
|
||||||
import { settings } from "$lib/db";
|
import { settings } from '$lib/db';
|
||||||
import { _, isLoading, locale } from "svelte-i18n";
|
import { _, locale, isLoadingLocale } from '$lib/i18n';
|
||||||
import { map } from "$lib/stores";
|
import { getElevation } from '$lib/utils';
|
||||||
import { getElevation } from "$lib/utils";
|
|
||||||
|
|
||||||
const { routing, routingProfile, privateRoads } = settings;
|
const { routing, routingProfile, privateRoads } = settings;
|
||||||
|
|
||||||
@@ -16,22 +15,31 @@ export const brouterProfiles: { [key: string]: string } = {
|
|||||||
foot: 'Hiking-Alpine-SAC6',
|
foot: 'Hiking-Alpine-SAC6',
|
||||||
motorcycle: 'Car-FastEco',
|
motorcycle: 'Car-FastEco',
|
||||||
water: 'river',
|
water: 'river',
|
||||||
railway: 'rail'
|
railway: 'rail',
|
||||||
};
|
};
|
||||||
export const routingProfileSelectItem = writable({
|
export const routingProfileSelectItem = writable({
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => {
|
derived([routingProfile, locale, isLoadingLocale], ([profile, l, i]) => [profile, l, i]).subscribe(
|
||||||
if (!i && profile !== '' && (profile !== get(routingProfileSelectItem).value || get(_)(`toolbar.routing.activities.${profile}`) !== get(routingProfileSelectItem).label) && l !== null) {
|
([profile, l, i]) => {
|
||||||
|
if (
|
||||||
|
!i &&
|
||||||
|
profile !== '' &&
|
||||||
|
(profile !== get(routingProfileSelectItem).value ||
|
||||||
|
get(_)(`toolbar.routing.activities.${profile}`) !==
|
||||||
|
get(routingProfileSelectItem).label) &&
|
||||||
|
l !== null
|
||||||
|
) {
|
||||||
routingProfileSelectItem.update((item) => {
|
routingProfileSelectItem.update((item) => {
|
||||||
item.value = profile;
|
item.value = profile;
|
||||||
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
routingProfileSelectItem.subscribe((item) => {
|
routingProfileSelectItem.subscribe((item) => {
|
||||||
if (item.value !== '' && item.value !== get(routingProfile)) {
|
if (item.value !== '' && item.value !== get(routingProfile)) {
|
||||||
routingProfile.set(item.value);
|
routingProfile.set(item.value);
|
||||||
@@ -46,8 +54,12 @@ export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRoute(points: Coordinates[], brouterProfile: string, privateRoads: boolean): Promise<TrackPoint[]> {
|
async function getRoute(
|
||||||
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
points: Coordinates[],
|
||||||
|
brouterProfile: string,
|
||||||
|
privateRoads: boolean
|
||||||
|
): Promise<TrackPoint[]> {
|
||||||
|
let url = `https://brouter.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
||||||
|
|
||||||
let response = await fetch(url);
|
let response = await fetch(url);
|
||||||
|
|
||||||
@@ -62,71 +74,81 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
|||||||
let coordinates = geojson.features[0].geometry.coordinates;
|
let coordinates = geojson.features[0].geometry.coordinates;
|
||||||
let messages = geojson.features[0].properties.messages;
|
let messages = geojson.features[0].properties.messages;
|
||||||
|
|
||||||
const lngIdx = messages[0].indexOf("Longitude");
|
const lngIdx = messages[0].indexOf('Longitude');
|
||||||
const latIdx = messages[0].indexOf("Latitude");
|
const latIdx = messages[0].indexOf('Latitude');
|
||||||
const tagIdx = messages[0].indexOf("WayTags");
|
const tagIdx = messages[0].indexOf('WayTags');
|
||||||
let messageIdx = 1;
|
let messageIdx = 1;
|
||||||
let surface = messageIdx < messages.length ? getSurface(messages[messageIdx][tagIdx]) : undefined;
|
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
||||||
|
|
||||||
for (let i = 0; i < coordinates.length; i++) {
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
let coord = coordinates[i];
|
let coord = coordinates[i];
|
||||||
route.push(new TrackPoint({
|
route.push(
|
||||||
|
new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: coord[1],
|
lat: coord[1],
|
||||||
lon: coord[0]
|
lon: coord[0],
|
||||||
},
|
},
|
||||||
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0)
|
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (messageIdx < messages.length &&
|
if (
|
||||||
|
messageIdx < messages.length &&
|
||||||
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
|
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
|
||||||
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000
|
||||||
|
) {
|
||||||
messageIdx++;
|
messageIdx++;
|
||||||
|
|
||||||
if (messageIdx == messages.length) surface = undefined;
|
if (messageIdx == messages.length) tags = {};
|
||||||
else surface = getSurface(messages[messageIdx][tagIdx]);
|
else tags = getTags(messages[messageIdx][tagIdx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (surface) {
|
route[route.length - 1].setExtensions(tags);
|
||||||
route[route.length - 1].setSurface(surface);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSurface(message: string): string | undefined {
|
function getTags(message: string): { [key: string]: string } {
|
||||||
const fields = message.split(" ");
|
const fields = message.split(' ');
|
||||||
for (let i = 0; i < fields.length; i++) if (fields[i].startsWith("surface=")) {
|
let tags: { [key: string]: string } = {};
|
||||||
return fields[i].substring(8);
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
let [key, value] = fields[i].split('=');
|
||||||
|
key = key.replace(/:/g, '_');
|
||||||
|
tags[key] = value;
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||||
let route: TrackPoint[] = [];
|
let route: TrackPoint[] = [];
|
||||||
let step = 0.05;
|
let step = 0.05;
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) { // Add intermediate points between each pair of points
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
// Add intermediate points between each pair of points
|
||||||
let dist = distance(points[i], points[i + 1]) / 1000;
|
let dist = distance(points[i], points[i + 1]) / 1000;
|
||||||
for (let d = 0; d < dist; d += step) {
|
for (let d = 0; d < dist; d += step) {
|
||||||
let lat = points[i].lat + d / dist * (points[i + 1].lat - points[i].lat);
|
let lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
|
||||||
let lon = points[i].lon + d / dist * (points[i + 1].lon - points[i].lon);
|
let lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
|
||||||
route.push(new TrackPoint({
|
route.push(
|
||||||
|
new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: lat,
|
lat: lat,
|
||||||
lon: lon
|
lon: lon,
|
||||||
}
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route.push(new TrackPoint({
|
route.push(
|
||||||
|
new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: points[points.length - 1].lat,
|
lat: points[points.length - 1].lat,
|
||||||
lon: points[points.length - 1].lon
|
lon: points[points.length - 1].lon,
|
||||||
}
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return getElevation(route).then((elevations) => {
|
return getElevation(route).then((elevations) => {
|
||||||
route.forEach((point, i) => {
|
route.forEach((point, i) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { canChangeStart } from './RoutingControls';
|
import { canChangeStart } from './RoutingControls';
|
||||||
import { CirclePlay, Trash2 } from 'lucide-svelte';
|
import { CirclePlay, Trash2 } from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from '$lib/i18n';
|
||||||
|
|
||||||
export let element: HTMLElement;
|
export let element: HTMLElement;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from "gpx";
|
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
||||||
import { get, writable, type Readable } from "svelte/store";
|
import { get, writable, type Readable } from 'svelte/store';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { route } from "./Routing";
|
import { route } from './Routing';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
import { toast } from "svelte-sonner";
|
import { _ } from '$lib/i18n';
|
||||||
|
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { _ } from "svelte-i18n";
|
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
||||||
import { dbUtils, type GPXFileWithStatistics } from "$lib/db";
|
import {
|
||||||
import { getOrderedSelection, selection } from "$lib/components/file-list/Selection";
|
ListFileItem,
|
||||||
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
ListTrackItem,
|
||||||
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
|
ListTrackSegmentItem,
|
||||||
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils";
|
} 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);
|
export const canChangeStart = writable(false);
|
||||||
|
|
||||||
function stopPropagation(e: any) {
|
function stopPropagation(e: any) {
|
||||||
@@ -32,12 +35,19 @@ export class RoutingControls {
|
|||||||
fileUnsubscribe: () => void = () => {};
|
fileUnsubscribe: () => void = () => {};
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
toggleAnchorsForZoomLevelAndBoundsBinded: () => void = this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
toggleAnchorsForZoomLevelAndBoundsBinded: () => void =
|
||||||
|
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
||||||
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
|
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
|
||||||
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
||||||
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
|
constructor(
|
||||||
|
map: mapboxgl.Map,
|
||||||
|
fileId: string,
|
||||||
|
file: Readable<GPXFileWithStatistics | undefined>,
|
||||||
|
popup: mapboxgl.Popup,
|
||||||
|
popupElement: HTMLElement
|
||||||
|
) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
@@ -47,8 +57,8 @@ export class RoutingControls {
|
|||||||
let point = new TrackPoint({
|
let point = new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: 0,
|
lat: 0,
|
||||||
lon: 0
|
lon: 0,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0);
|
this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0);
|
||||||
this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers
|
this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers
|
||||||
@@ -66,7 +76,9 @@ export class RoutingControls {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, ['waypoints']);
|
let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, [
|
||||||
|
'waypoints',
|
||||||
|
]);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
this.updateControls();
|
this.updateControls();
|
||||||
@@ -89,7 +101,8 @@ export class RoutingControls {
|
|||||||
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateControls() { // Update the markers when the file changes
|
updateControls() {
|
||||||
|
// Update the markers when the file changes
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
@@ -97,8 +110,13 @@ export class RoutingControls {
|
|||||||
|
|
||||||
let anchorIndex = 0;
|
let anchorIndex = 0;
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
for (let point of segment.trkpt) { // Update the existing anchors (could be improved by matching the existing anchors with the new ones?)
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
for (let point of segment.trkpt) {
|
||||||
|
// Update the existing anchors (could be improved by matching the existing anchors with the new ones?)
|
||||||
if (point._data.anchor) {
|
if (point._data.anchor) {
|
||||||
if (anchorIndex < this.anchors.length) {
|
if (anchorIndex < this.anchors.length) {
|
||||||
this.anchors[anchorIndex].point = point;
|
this.anchors[anchorIndex].point = point;
|
||||||
@@ -107,7 +125,9 @@ export class RoutingControls {
|
|||||||
this.anchors[anchorIndex].segmentIndex = segmentIndex;
|
this.anchors[anchorIndex].segmentIndex = segmentIndex;
|
||||||
this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates());
|
this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates());
|
||||||
} else {
|
} else {
|
||||||
this.anchors.push(this.createAnchor(point, segment, trackIndex, segmentIndex));
|
this.anchors.push(
|
||||||
|
this.createAnchor(point, segment, trackIndex, segmentIndex)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
anchorIndex++;
|
anchorIndex++;
|
||||||
}
|
}
|
||||||
@@ -115,7 +135,8 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
while (anchorIndex < this.anchors.length) { // Remove the extra anchors
|
while (anchorIndex < this.anchors.length) {
|
||||||
|
// Remove the extra anchors
|
||||||
this.anchors.pop()?.marker.remove();
|
this.anchors.pop()?.marker.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,14 +163,19 @@ export class RoutingControls {
|
|||||||
this.map = map;
|
this.map = map;
|
||||||
}
|
}
|
||||||
|
|
||||||
createAnchor(point: TrackPoint, segment: TrackSegment, trackIndex: number, segmentIndex: number): AnchorWithMarker {
|
createAnchor(
|
||||||
|
point: TrackPoint,
|
||||||
|
segment: TrackSegment,
|
||||||
|
trackIndex: number,
|
||||||
|
segmentIndex: number
|
||||||
|
): AnchorWithMarker {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
|
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
|
|
||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: true,
|
draggable: true,
|
||||||
className: 'z-10',
|
className: 'z-10',
|
||||||
element
|
element,
|
||||||
}).setLngLat(point.getCoordinates());
|
}).setLngLat(point.getCoordinates());
|
||||||
|
|
||||||
let anchor = {
|
let anchor = {
|
||||||
@@ -158,7 +184,7 @@ export class RoutingControls {
|
|||||||
trackIndex,
|
trackIndex,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
marker,
|
marker,
|
||||||
inZoom: false
|
inZoom: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
marker.on('dragstart', (e) => {
|
marker.on('dragstart', (e) => {
|
||||||
@@ -186,7 +212,8 @@ export class RoutingControls {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (Date.now() - this.lastDragEvent < 100) { // Prevent click event during drag
|
if (Date.now() - this.lastDragEvent < 100) {
|
||||||
|
// Prevent click event during drag
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +232,12 @@ export class RoutingControls {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let segment = anchor.segment;
|
let segment = anchor.segment;
|
||||||
if (distance(segment.trkpt[0].getCoordinates(), segment.trkpt[segment.trkpt.length - 1].getCoordinates()) > 1000) {
|
if (
|
||||||
|
distance(
|
||||||
|
segment.trkpt[0].getCoordinates(),
|
||||||
|
segment.trkpt[segment.trkpt.length - 1].getCoordinates()
|
||||||
|
) > 1000
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -225,7 +257,8 @@ export class RoutingControls {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAnchorsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
|
toggleAnchorsForZoomLevelAndBounds() {
|
||||||
|
// Show markers only if they are in the current zoom level and bounds
|
||||||
this.shownAnchors.splice(0, this.shownAnchors.length);
|
this.shownAnchors.splice(0, this.shownAnchors.length);
|
||||||
|
|
||||||
let center = this.map.getCenter();
|
let center = this.map.getCenter();
|
||||||
@@ -246,7 +279,8 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTemporaryAnchor(e: any) {
|
showTemporaryAnchor(e: any) {
|
||||||
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not not change the source point if it is already being dragged
|
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
|
||||||
|
// Do not not change the source point if it is already being dragged
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +288,15 @@ export class RoutingControls {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, e.features[0].properties.trackIndex, e.features[0].properties.segmentIndex))) {
|
if (
|
||||||
|
!get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(
|
||||||
|
this.fileId,
|
||||||
|
e.features[0].properties.trackIndex,
|
||||||
|
e.features[0].properties.segmentIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +306,7 @@ export class RoutingControls {
|
|||||||
|
|
||||||
this.temporaryAnchor.point.setCoordinates({
|
this.temporaryAnchor.point.setCoordinates({
|
||||||
lat: e.lngLat.lat,
|
lat: e.lngLat.lat,
|
||||||
lon: e.lngLat.lng
|
lon: e.lngLat.lng,
|
||||||
});
|
});
|
||||||
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
|
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
|
||||||
|
|
||||||
@@ -272,12 +314,17 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTemporaryAnchor(e: any) {
|
updateTemporaryAnchor(e: any) {
|
||||||
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not hide if it is being dragged, and stop listening for mousemove
|
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
|
||||||
|
// Do not hide if it is being dragged, and stop listening for mousemove
|
||||||
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 || this.temporaryAnchorCloseToOtherAnchor(e)) { // Hide if too far from the layer
|
if (
|
||||||
|
e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
|
||||||
|
this.temporaryAnchorCloseToOtherAnchor(e)
|
||||||
|
) {
|
||||||
|
// Hide if too far from the layer
|
||||||
this.temporaryAnchor.marker.remove();
|
this.temporaryAnchor.marker.remove();
|
||||||
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||||
return;
|
return;
|
||||||
@@ -295,14 +342,16 @@ export class RoutingControls {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveAnchor(anchorWithMarker: AnchorWithMarker) { // Move the anchor and update the route from and to the neighbouring anchors
|
async moveAnchor(anchorWithMarker: AnchorWithMarker) {
|
||||||
|
// Move the anchor and update the route from and to the neighbouring anchors
|
||||||
let coordinates = {
|
let coordinates = {
|
||||||
lat: anchorWithMarker.marker.getLngLat().lat,
|
lat: anchorWithMarker.marker.getLngLat().lat,
|
||||||
lon: anchorWithMarker.marker.getLngLat().lng
|
lon: anchorWithMarker.marker.getLngLat().lng,
|
||||||
};
|
};
|
||||||
|
|
||||||
let anchor = anchorWithMarker as Anchor;
|
let anchor = anchorWithMarker as Anchor;
|
||||||
if (anchorWithMarker === this.temporaryAnchor) { // Temporary anchor, need to find the closest point of the segment and create an anchor for it
|
if (anchorWithMarker === this.temporaryAnchor) {
|
||||||
|
// Temporary anchor, need to find the closest point of the segment and create an anchor for it
|
||||||
this.temporaryAnchor.marker.remove();
|
this.temporaryAnchor.marker.remove();
|
||||||
anchor = this.getPermanentAnchor();
|
anchor = this.getPermanentAnchor();
|
||||||
}
|
}
|
||||||
@@ -327,7 +376,8 @@ export class RoutingControls {
|
|||||||
|
|
||||||
let success = await this.routeBetweenAnchors(anchors, targetCoordinates);
|
let success = await this.routeBetweenAnchors(anchors, targetCoordinates);
|
||||||
|
|
||||||
if (!success) { // Route failed, revert the anchor to the previous position
|
if (!success) {
|
||||||
|
// Route failed, revert the anchor to the previous position
|
||||||
anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates());
|
anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,16 +389,24 @@ export class RoutingControls {
|
|||||||
let minDetails: any = { distance: Number.MAX_VALUE };
|
let minDetails: any = { distance: Number.MAX_VALUE };
|
||||||
let minAnchor = this.temporaryAnchor as Anchor;
|
let minAnchor = this.temporaryAnchor as Anchor;
|
||||||
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
let details: any = {};
|
let details: any = {};
|
||||||
let closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
let closest = getClosestLinePoint(
|
||||||
|
segment.trkpt,
|
||||||
|
this.temporaryAnchor.point,
|
||||||
|
details
|
||||||
|
);
|
||||||
if (details.distance < minDetails.distance) {
|
if (details.distance < minDetails.distance) {
|
||||||
minDetails = details;
|
minDetails = details;
|
||||||
minAnchor = {
|
minAnchor = {
|
||||||
point: closest,
|
point: closest,
|
||||||
segment,
|
segment,
|
||||||
trackIndex,
|
trackIndex,
|
||||||
segmentIndex
|
segmentIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,41 +433,67 @@ export class RoutingControls {
|
|||||||
point: this.temporaryAnchor.point,
|
point: this.temporaryAnchor.point,
|
||||||
trackIndex: -1,
|
trackIndex: -1,
|
||||||
segmentIndex: -1,
|
segmentIndex: -1,
|
||||||
trkptIndex: -1
|
trkptIndex: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
let details: any = {};
|
let details: any = {};
|
||||||
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
||||||
if (details.distance < minDetails.distance) {
|
if (details.distance < minDetails.distance) {
|
||||||
minDetails = details;
|
minDetails = details;
|
||||||
let before = details.before ? details.index : details.index - 1;
|
let before = details.before ? details.index : details.index - 1;
|
||||||
|
|
||||||
let projectedPt = projectedPoint(segment.trkpt[before], segment.trkpt[before + 1], this.temporaryAnchor.point);
|
let projectedPt = projectedPoint(
|
||||||
let ratio = distance(segment.trkpt[before], projectedPt) / distance(segment.trkpt[before], segment.trkpt[before + 1]);
|
segment.trkpt[before],
|
||||||
|
segment.trkpt[before + 1],
|
||||||
|
this.temporaryAnchor.point
|
||||||
|
);
|
||||||
|
let ratio =
|
||||||
|
distance(segment.trkpt[before], projectedPt) /
|
||||||
|
distance(segment.trkpt[before], segment.trkpt[before + 1]);
|
||||||
|
|
||||||
let point = segment.trkpt[before].clone();
|
let point = segment.trkpt[before].clone();
|
||||||
point.setCoordinates(projectedPt);
|
point.setCoordinates(projectedPt);
|
||||||
point.ele = (1 - ratio) * (segment.trkpt[before].ele ?? 0) + ratio * (segment.trkpt[before + 1].ele ?? 0);
|
point.ele =
|
||||||
point.time = (segment.trkpt[before].time && segment.trkpt[before + 1].time) ? new Date((1 - ratio) * segment.trkpt[before].time.getTime() + ratio * segment.trkpt[before + 1].time.getTime()) : undefined;
|
(1 - ratio) * (segment.trkpt[before].ele ?? 0) +
|
||||||
|
ratio * (segment.trkpt[before + 1].ele ?? 0);
|
||||||
|
point.time =
|
||||||
|
segment.trkpt[before].time && segment.trkpt[before + 1].time
|
||||||
|
? new Date(
|
||||||
|
(1 - ratio) * segment.trkpt[before].time.getTime() +
|
||||||
|
ratio * segment.trkpt[before + 1].time.getTime()
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
point._data = {
|
point._data = {
|
||||||
anchor: true,
|
anchor: true,
|
||||||
zoom: 0
|
zoom: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
minInfo = {
|
minInfo = {
|
||||||
point,
|
point,
|
||||||
trackIndex,
|
trackIndex,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
trkptIndex: before + 1
|
trkptIndex: before + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (minInfo.trackIndex !== -1) {
|
if (minInfo.trackIndex !== -1) {
|
||||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(minInfo.trackIndex, minInfo.segmentIndex, minInfo.trkptIndex, minInfo.trkptIndex - 1, [minInfo.point]));
|
dbUtils.applyToFile(this.fileId, (file) =>
|
||||||
|
file.replaceTrackPoints(
|
||||||
|
minInfo.trackIndex,
|
||||||
|
minInfo.segmentIndex,
|
||||||
|
minInfo.trkptIndex,
|
||||||
|
minInfo.trkptIndex - 1,
|
||||||
|
[minInfo.point]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,22 +501,46 @@ export class RoutingControls {
|
|||||||
return () => this.deleteAnchor(anchor);
|
return () => this.deleteAnchor(anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAnchor(anchor: Anchor) { // Remove the anchor and route between the neighbouring anchors if they exist
|
async deleteAnchor(anchor: Anchor) {
|
||||||
|
// Remove the anchor and route between the neighbouring anchors if they exist
|
||||||
this.popup.remove();
|
this.popup.remove();
|
||||||
|
|
||||||
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
|
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
|
||||||
|
|
||||||
if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it
|
if (previousAnchor === null && nextAnchor === null) {
|
||||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []));
|
// Only one point, remove it
|
||||||
} else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor
|
dbUtils.applyToFile(this.fileId, (file) =>
|
||||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, nextAnchor.point._data.index - 1, []));
|
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
|
||||||
} else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor
|
);
|
||||||
|
} else if (previousAnchor === null) {
|
||||||
|
// First point, remove trackpoints until nextAnchor
|
||||||
|
dbUtils.applyToFile(this.fileId, (file) =>
|
||||||
|
file.replaceTrackPoints(
|
||||||
|
anchor.trackIndex,
|
||||||
|
anchor.segmentIndex,
|
||||||
|
0,
|
||||||
|
nextAnchor.point._data.index - 1,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (nextAnchor === null) {
|
||||||
|
// Last point, remove trackpoints from previousAnchor
|
||||||
dbUtils.applyToFile(this.fileId, (file) => {
|
dbUtils.applyToFile(this.fileId, (file) => {
|
||||||
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
|
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
|
||||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
|
file.replaceTrackPoints(
|
||||||
|
anchor.trackIndex,
|
||||||
|
anchor.segmentIndex,
|
||||||
|
previousAnchor.point._data.index + 1,
|
||||||
|
segment.trkpt.length - 1,
|
||||||
|
[]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else { // Route between previousAnchor and nextAnchor
|
} else {
|
||||||
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
|
// Route between previousAnchor and nextAnchor
|
||||||
|
this.routeBetweenAnchors(
|
||||||
|
[previousAnchor, nextAnchor],
|
||||||
|
[previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,27 +556,43 @@ export class RoutingControls {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let speed = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)).global.speed.moving;
|
let speed = fileWithStats.statistics.getStatisticsFor(
|
||||||
|
new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)
|
||||||
|
).global.speed.moving;
|
||||||
|
|
||||||
let segment = anchor.segment;
|
let segment = anchor.segment;
|
||||||
dbUtils.applyToFile(this.fileId, (file) => {
|
dbUtils.applyToFile(this.fileId, (file) => {
|
||||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
|
file.replaceTrackPoints(
|
||||||
file.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]);
|
anchor.trackIndex,
|
||||||
|
anchor.segmentIndex,
|
||||||
|
segment.trkpt.length,
|
||||||
|
segment.trkpt.length - 1,
|
||||||
|
segment.trkpt.slice(0, anchor.point._data.index),
|
||||||
|
speed > 0 ? speed : undefined
|
||||||
|
);
|
||||||
|
file.crop(
|
||||||
|
anchor.point._data.index,
|
||||||
|
anchor.point._data.index + segment.trkpt.length - 1,
|
||||||
|
[anchor.trackIndex],
|
||||||
|
[anchor.segmentIndex]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async appendAnchor(e: mapboxgl.MapMouseEvent) { // Add a new anchor to the end of the last segment
|
async appendAnchor(e: mapboxgl.MapMouseEvent) {
|
||||||
if (get(streetViewEnabled)) {
|
// Add a new anchor to the end of the last segment
|
||||||
|
if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appendAnchorWithCoordinates({
|
this.appendAnchorWithCoordinates({
|
||||||
lat: e.lngLat.lat,
|
lat: e.lngLat.lat,
|
||||||
lon: e.lngLat.lng
|
lon: e.lngLat.lng,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async appendAnchorWithCoordinates(coordinates: Coordinates) { // Add a new anchor to the end of the last segment
|
async appendAnchorWithCoordinates(coordinates: Coordinates) {
|
||||||
|
// Add a new anchor to the end of the last segment
|
||||||
let selected = getOrderedSelection();
|
let selected = getOrderedSelection();
|
||||||
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
|
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
|
||||||
return;
|
return;
|
||||||
@@ -478,7 +602,7 @@ export class RoutingControls {
|
|||||||
let lastAnchor = this.anchors[this.anchors.length - 1];
|
let lastAnchor = this.anchors[this.anchors.length - 1];
|
||||||
|
|
||||||
let newPoint = new TrackPoint({
|
let newPoint = new TrackPoint({
|
||||||
attributes: coordinates
|
attributes: coordinates,
|
||||||
});
|
});
|
||||||
newPoint._data.anchor = true;
|
newPoint._data.anchor = true;
|
||||||
newPoint._data.zoom = 0;
|
newPoint._data.zoom = 0;
|
||||||
@@ -489,7 +613,10 @@ export class RoutingControls {
|
|||||||
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||||
trackIndex = item.getTrackIndex();
|
trackIndex = item.getTrackIndex();
|
||||||
}
|
}
|
||||||
let segmentIndex = (file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0) ? file.trk[trackIndex].trkseg.length - 1 : 0;
|
let segmentIndex =
|
||||||
|
file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0
|
||||||
|
? file.trk[trackIndex].trkseg.length - 1
|
||||||
|
: 0;
|
||||||
if (item instanceof ListTrackSegmentItem) {
|
if (item instanceof ListTrackSegmentItem) {
|
||||||
segmentIndex = item.getSegmentIndex();
|
segmentIndex = item.getSegmentIndex();
|
||||||
}
|
}
|
||||||
@@ -513,10 +640,13 @@ export class RoutingControls {
|
|||||||
point: newPoint,
|
point: newPoint,
|
||||||
segment: lastAnchor.segment,
|
segment: lastAnchor.segment,
|
||||||
trackIndex: lastAnchor.trackIndex,
|
trackIndex: lastAnchor.trackIndex,
|
||||||
segmentIndex: lastAnchor.segmentIndex
|
segmentIndex: lastAnchor.segmentIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]);
|
await this.routeBetweenAnchors(
|
||||||
|
[lastAnchor, newAnchor],
|
||||||
|
[lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] {
|
getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] {
|
||||||
@@ -526,11 +656,17 @@ export class RoutingControls {
|
|||||||
for (let i = 0; i < this.anchors.length; i++) {
|
for (let i = 0; i < this.anchors.length; i++) {
|
||||||
if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) {
|
if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) {
|
||||||
if (this.anchors[i].point._data.index < anchor.point._data.index) {
|
if (this.anchors[i].point._data.index < anchor.point._data.index) {
|
||||||
if (!previousAnchor || this.anchors[i].point._data.index > previousAnchor.point._data.index) {
|
if (
|
||||||
|
!previousAnchor ||
|
||||||
|
this.anchors[i].point._data.index > previousAnchor.point._data.index
|
||||||
|
) {
|
||||||
previousAnchor = this.anchors[i];
|
previousAnchor = this.anchors[i];
|
||||||
}
|
}
|
||||||
} else if (this.anchors[i].point._data.index > anchor.point._data.index) {
|
} else if (this.anchors[i].point._data.index > anchor.point._data.index) {
|
||||||
if (!nextAnchor || this.anchors[i].point._data.index < nextAnchor.point._data.index) {
|
if (
|
||||||
|
!nextAnchor ||
|
||||||
|
this.anchors[i].point._data.index < nextAnchor.point._data.index
|
||||||
|
) {
|
||||||
nextAnchor = this.anchors[i];
|
nextAnchor = this.anchors[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,7 +676,10 @@ export class RoutingControls {
|
|||||||
return [previousAnchor, nextAnchor];
|
return [previousAnchor, nextAnchor];
|
||||||
}
|
}
|
||||||
|
|
||||||
async routeBetweenAnchors(anchors: Anchor[], targetCoordinates: Coordinates[]): Promise<boolean> {
|
async routeBetweenAnchors(
|
||||||
|
anchors: Anchor[],
|
||||||
|
targetCoordinates: Coordinates[]
|
||||||
|
): Promise<boolean> {
|
||||||
let segment = anchors[0].segment;
|
let segment = anchors[0].segment;
|
||||||
|
|
||||||
let fileWithStats = get(this.file);
|
let fileWithStats = get(this.file);
|
||||||
@@ -548,10 +687,15 @@ export class RoutingControls {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchors.length === 1) { // Only one anchor, update the point in the segment
|
if (anchors.length === 1) {
|
||||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [new TrackPoint({
|
// Only one anchor, update the point in the segment
|
||||||
|
dbUtils.applyToFile(this.fileId, (file) =>
|
||||||
|
file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
|
||||||
|
new TrackPoint({
|
||||||
attributes: targetCoordinates[0],
|
attributes: targetCoordinates[0],
|
||||||
})]));
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,23 +704,28 @@ export class RoutingControls {
|
|||||||
response = await route(targetCoordinates);
|
response = await route(targetCoordinates);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('from-position not mapped in existing datafile')) {
|
if (e.message.includes('from-position not mapped in existing datafile')) {
|
||||||
toast.error(get(_)("toolbar.routing.error.from"));
|
toast.error(get(_)('toolbar.routing.error.from'));
|
||||||
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
|
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
|
||||||
toast.error(get(_)("toolbar.routing.error.via"));
|
toast.error(get(_)('toolbar.routing.error.via'));
|
||||||
} else if (e.message.includes('to-position not mapped in existing datafile')) {
|
} else if (e.message.includes('to-position not mapped in existing datafile')) {
|
||||||
toast.error(get(_)("toolbar.routing.error.to"));
|
toast.error(get(_)('toolbar.routing.error.to'));
|
||||||
} else if (e.message.includes('Time-out')) {
|
} else if (e.message.includes('Time-out')) {
|
||||||
toast.error(get(_)("toolbar.routing.error.timeout"));
|
toast.error(get(_)('toolbar.routing.error.timeout'));
|
||||||
} else {
|
} else {
|
||||||
toast.error(e.message);
|
toast.error(e.message);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchors[0].point._data.index === 0) { // First anchor is the first point of the segment
|
if (anchors[0].point._data.index === 0) {
|
||||||
|
// First anchor is the first point of the segment
|
||||||
anchors[0].point = response[0]; // replace the first anchor
|
anchors[0].point = response[0]; // replace the first anchor
|
||||||
anchors[0].point._data.index = 0;
|
anchors[0].point._data.index = 0;
|
||||||
} else if (anchors[0].point._data.index === segment.trkpt.length - 1 && distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1) { // First anchor is the last point of the segment, and the new point is close enough
|
} else if (
|
||||||
|
anchors[0].point._data.index === segment.trkpt.length - 1 &&
|
||||||
|
distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1
|
||||||
|
) {
|
||||||
|
// First anchor is the last point of the segment, and the new point is close enough
|
||||||
anchors[0].point = response[0]; // replace the first anchor
|
anchors[0].point = response[0]; // replace the first anchor
|
||||||
anchors[0].point._data.index = segment.trkpt.length - 1;
|
anchors[0].point._data.index = segment.trkpt.length - 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -584,7 +733,8 @@ export class RoutingControls {
|
|||||||
response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it
|
response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) { // Last anchor is the last point of the segment
|
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) {
|
||||||
|
// Last anchor is the last point of the segment
|
||||||
anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor
|
anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor
|
||||||
anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1;
|
anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -603,36 +753,64 @@ export class RoutingControls {
|
|||||||
anchor.point._data.zoom = 0; // Make these anchors permanent
|
anchor.point._data.zoom = 0; // Make these anchors permanent
|
||||||
});
|
});
|
||||||
|
|
||||||
let stats = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex));
|
let stats = fileWithStats.statistics.getStatisticsFor(
|
||||||
|
new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex)
|
||||||
|
);
|
||||||
let speed: number | undefined = undefined;
|
let speed: number | undefined = undefined;
|
||||||
let startTime = anchors[0].point.time;
|
let startTime = anchors[0].point.time;
|
||||||
|
|
||||||
if (stats.global.speed.moving > 0) {
|
if (stats.global.speed.moving > 0) {
|
||||||
let replacingDistance = 0;
|
let replacingDistance = 0;
|
||||||
for (let i = 1; i < response.length; i++) {
|
for (let i = 1; i < response.length; i++) {
|
||||||
replacingDistance += distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
replacingDistance +=
|
||||||
|
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
||||||
}
|
}
|
||||||
let replacedDistance = stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - stats.local.distance.moving[anchors[0].point._data.index];
|
let replacedDistance =
|
||||||
|
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.distance.moving[anchors[0].point._data.index];
|
||||||
|
|
||||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||||
let newTime = newDistance / stats.global.speed.moving * 3600;
|
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
||||||
|
|
||||||
let remainingTime = stats.global.time.moving - (stats.local.time.moving[anchors[anchors.length - 1].point._data.index] - stats.local.time.moving[anchors[0].point._data.index]);
|
let remainingTime =
|
||||||
|
stats.global.time.moving -
|
||||||
|
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.time.moving[anchors[0].point._data.index]);
|
||||||
let replacingTime = newTime - remainingTime;
|
let replacingTime = newTime - remainingTime;
|
||||||
|
|
||||||
if (replacingTime <= 0) { // Fallback to simple time difference
|
if (replacingTime <= 0) {
|
||||||
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index];
|
// Fallback to simple time difference
|
||||||
|
replacingTime =
|
||||||
|
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.time.total[anchors[0].point._data.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
speed = replacingDistance / replacingTime * 3600;
|
speed = (replacingDistance / replacingTime) * 3600;
|
||||||
|
|
||||||
if (startTime === undefined) { // Replacing the first point
|
if (startTime === undefined) {
|
||||||
|
// Replacing the first point
|
||||||
let endIndex = anchors[anchors.length - 1].point._data.index;
|
let endIndex = anchors[anchors.length - 1].point._data.index;
|
||||||
startTime = new Date((segment.trkpt[endIndex].time?.getTime() ?? 0) - (replacingTime + stats.local.time.total[endIndex] - stats.local.time.moving[endIndex]) * 1000);
|
startTime = new Date(
|
||||||
|
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
||||||
|
(replacingTime +
|
||||||
|
stats.local.time.total[endIndex] -
|
||||||
|
stats.local.time.moving[endIndex]) *
|
||||||
|
1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, anchors[0].point._data.index, anchors[anchors.length - 1].point._data.index, response, speed, startTime));
|
dbUtils.applyToFile(this.fileId, (file) =>
|
||||||
|
file.replaceTrackPoints(
|
||||||
|
anchors[0].trackIndex,
|
||||||
|
anchors[0].segmentIndex,
|
||||||
|
anchors[0].point._data.index,
|
||||||
|
anchors[anchors.length - 1].point._data.index,
|
||||||
|
response,
|
||||||
|
speed,
|
||||||
|
startTime
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx";
|
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
|
|
||||||
@@ -17,7 +17,8 @@ export function updateAnchorPoints(file: GPXFile) {
|
|||||||
let segments = file.getSegments();
|
let segments = file.getSegments();
|
||||||
|
|
||||||
for (let segment of segments) {
|
for (let segment of segments) {
|
||||||
if (!segment._data.anchors) { // New segment, compute anchor points for it
|
if (!segment._data.anchors) {
|
||||||
|
// New segment, compute anchor points for it
|
||||||
computeAnchorPoints(segment);
|
computeAnchorPoints(segment);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -42,4 +43,3 @@ function computeAnchorPoints(segment: TrackSegment) {
|
|||||||
});
|
});
|
||||||
segment._data.anchors = true;
|
segment._data.anchors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
export enum SplitType {
|
export enum SplitType {
|
||||||
FILES = 'files',
|
FILES = 'files',
|
||||||
TRACKS = 'tracks',
|
TRACKS = 'tracks',
|
||||||
SEGMENTS = 'segments'
|
SEGMENTS = 'segments',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from '$lib/i18n';
|
||||||
import { onDestroy, tick } from 'svelte';
|
import { onDestroy, tick } from 'svelte';
|
||||||
import { Crop } from 'lucide-svelte';
|
import { Crop } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
$slicedGPXStatistics = [
|
$slicedGPXStatistics = [
|
||||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
||||||
sliderValues[0],
|
sliderValues[0],
|
||||||
sliderValues[1]
|
sliderValues[1],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$slicedGPXStatistics = undefined;
|
$slicedGPXStatistics = undefined;
|
||||||
@@ -93,10 +93,10 @@
|
|||||||
const splitTypes = [
|
const splitTypes = [
|
||||||
{ value: SplitType.FILES, label: $_('gpx.files') },
|
{ value: SplitType.FILES, label: $_('gpx.files') },
|
||||||
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
|
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
|
||||||
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') }
|
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') },
|
||||||
];
|
];
|
||||||
|
|
||||||
let splitType = splitTypes[0];
|
let splitType = splitTypes.find((type) => type.value === $splitAs) ?? splitTypes[0];
|
||||||
|
|
||||||
$: splitAs.set(splitType.value);
|
$: splitAs.set(splitType.value);
|
||||||
|
|
||||||
@@ -111,7 +111,12 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<Slider bind:value={sliderValues} max={maxSliderValue} step={1} disabled={!validSelection} />
|
<Slider
|
||||||
|
bind:value={sliderValues}
|
||||||
|
max={maxSliderValue}
|
||||||
|
step={1}
|
||||||
|
disabled={!validSelection}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { TrackPoint, TrackSegment } from "gpx";
|
import { TrackPoint, TrackSegment } from 'gpx';
|
||||||
import { get } from "svelte/store";
|
import { get } from 'svelte/store';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { dbUtils, getFile } from "$lib/db";
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { applyToOrderedSelectedItemsFromFile, selection } from "$lib/components/file-list/Selection";
|
import {
|
||||||
import { ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
applyToOrderedSelectedItemsFromFile,
|
||||||
import { currentTool, gpxStatistics, Tool } from "$lib/stores";
|
selection,
|
||||||
import { _ } from "svelte-i18n";
|
} from '$lib/components/file-list/Selection';
|
||||||
import { Scissors } from "lucide-static";
|
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
||||||
|
import { currentTool, gpxStatistics, Tool } from '$lib/stores';
|
||||||
|
import { _ } from '$lib/i18n';
|
||||||
|
import { Scissors } from 'lucide-static';
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
active: boolean = false;
|
active: boolean = false;
|
||||||
@@ -15,7 +18,8 @@ export class SplitControls {
|
|||||||
shownControls: ControlWithMarker[] = [];
|
shownControls: ControlWithMarker[] = [];
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
toggleControlsForZoomLevelAndBoundsBinded: () => void = this.toggleControlsForZoomLevelAndBounds.bind(this);
|
toggleControlsForZoomLevelAndBoundsBinded: () => void =
|
||||||
|
this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
@@ -48,15 +52,21 @@ export class SplitControls {
|
|||||||
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateControls() { // Update the markers when the files change
|
updateControls() {
|
||||||
|
// Update the markers when the files change
|
||||||
let controlIndex = 0;
|
let controlIndex = 0;
|
||||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let file = getFile(fileId);
|
let file = getFile(fileId);
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
for (let point of segment.trkpt.slice(1, -1)) {
|
||||||
|
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
||||||
if (point._data.anchor) {
|
if (point._data.anchor) {
|
||||||
if (controlIndex < this.controls.length) {
|
if (controlIndex < this.controls.length) {
|
||||||
this.controls[controlIndex].fileId = fileId;
|
this.controls[controlIndex].fileId = fileId;
|
||||||
@@ -64,20 +74,30 @@ export class SplitControls {
|
|||||||
this.controls[controlIndex].segment = segment;
|
this.controls[controlIndex].segment = segment;
|
||||||
this.controls[controlIndex].trackIndex = trackIndex;
|
this.controls[controlIndex].trackIndex = trackIndex;
|
||||||
this.controls[controlIndex].segmentIndex = segmentIndex;
|
this.controls[controlIndex].segmentIndex = segmentIndex;
|
||||||
this.controls[controlIndex].marker.setLngLat(point.getCoordinates());
|
this.controls[controlIndex].marker.setLngLat(
|
||||||
|
point.getCoordinates()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.controls.push(this.createControl(point, segment, fileId, trackIndex, segmentIndex));
|
this.controls.push(
|
||||||
|
this.createControl(
|
||||||
|
point,
|
||||||
|
segment,
|
||||||
|
fileId,
|
||||||
|
trackIndex,
|
||||||
|
segmentIndex
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
controlIndex++;
|
controlIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
while (controlIndex < this.controls.length) { // Remove the extra controls
|
while (controlIndex < this.controls.length) {
|
||||||
|
// Remove the extra controls
|
||||||
this.controls.pop()?.marker.remove();
|
this.controls.pop()?.marker.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +114,8 @@ export class SplitControls {
|
|||||||
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleControlsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
|
toggleControlsForZoomLevelAndBounds() {
|
||||||
|
// Show markers only if they are in the current zoom level and bounds
|
||||||
this.shownControls.splice(0, this.shownControls.length);
|
this.shownControls.splice(0, this.shownControls.length);
|
||||||
|
|
||||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
||||||
@@ -113,15 +134,23 @@ export class SplitControls {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker {
|
createControl(
|
||||||
|
point: TrackPoint,
|
||||||
|
segment: TrackSegment,
|
||||||
|
fileId: string,
|
||||||
|
trackIndex: number,
|
||||||
|
segmentIndex: number
|
||||||
|
): ControlWithMarker {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "").replace('stroke="currentColor"', 'stroke="black"');
|
element.innerHTML = Scissors.replace('width="24"', '')
|
||||||
|
.replace('height="24"', '')
|
||||||
|
.replace('stroke="currentColor"', 'stroke="black"');
|
||||||
|
|
||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: true,
|
draggable: true,
|
||||||
className: 'z-10',
|
className: 'z-10',
|
||||||
element
|
element,
|
||||||
}).setLngLat(point.getCoordinates());
|
}).setLngLat(point.getCoordinates());
|
||||||
|
|
||||||
let control = {
|
let control = {
|
||||||
@@ -131,12 +160,18 @@ export class SplitControls {
|
|||||||
trackIndex,
|
trackIndex,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
marker,
|
marker,
|
||||||
inZoom: false
|
inZoom: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
marker.getElement().addEventListener('click', (e) => {
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dbUtils.split(control.fileId, control.trackIndex, control.segmentIndex, control.point.getCoordinates(), control.point._data.index);
|
dbUtils.split(
|
||||||
|
control.fileId,
|
||||||
|
control.trackIndex,
|
||||||
|
control.segmentIndex,
|
||||||
|
control.point.getCoordinates(),
|
||||||
|
control.point._data.index
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return control;
|
return control;
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
|
||||||
import { Scrollbar } from "./index.js";
|
import { Scrollbar } from './index.js';
|
||||||
import { cn } from "$lib/utils.js";
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
type $$Props = ScrollAreaPrimitive.Props & {
|
type $$Props = ScrollAreaPrimitive.Props & {
|
||||||
orientation?: "vertical" | "horizontal" | "both";
|
orientation?: 'vertical' | 'horizontal' | 'both';
|
||||||
scrollbarXClasses?: string;
|
scrollbarXClasses?: string;
|
||||||
scrollbarYClasses?: string;
|
scrollbarYClasses?: string;
|
||||||
|
viewportClasses?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
export let orientation = "vertical";
|
export let orientation = 'vertical';
|
||||||
export let scrollbarXClasses: string = "";
|
export let scrollbarXClasses: string = '';
|
||||||
export let scrollbarYClasses: string = "";
|
export let scrollbarYClasses: string = '';
|
||||||
|
export let viewportClasses: string = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollAreaPrimitive.Root {...$$restProps} class={cn("relative overflow-hidden", className)}>
|
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>
|
||||||
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
|
<ScrollAreaPrimitive.Viewport class={cn('h-full w-full rounded-[inherit]', viewportClasses)}>
|
||||||
<ScrollAreaPrimitive.Content>
|
<ScrollAreaPrimitive.Content>
|
||||||
<slot />
|
<slot />
|
||||||
</ScrollAreaPrimitive.Content>
|
</ScrollAreaPrimitive.Content>
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
{#if orientation === "vertical" || orientation === "both"}
|
{#if orientation === 'vertical' || orientation === 'both'}
|
||||||
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if orientation === "horizontal" || orientation === "both"}
|
{#if orientation === 'horizontal' || orientation === 'both'}
|
||||||
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||||
{/if}
|
{/if}
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user