prettier config + format all, closes #175
This commit is contained in:
parent
01cfd448f0
commit
0b457f9a1e
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.svelte",
|
||||||
|
"options": {
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"svelte.svelte-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
13
.vscode/settings.json
vendored
Normal file
13
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1
gpx/.prettierignore
Normal file
1
gpx/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
package-lock.json
|
||||||
1573
gpx/package-lock.json
generated
1573
gpx/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,10 +18,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"@types/node": "^20.16.10",
|
"@types/node": "^20.16.10",
|
||||||
|
"@typescript-eslint/parser": "^8.22.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"postinstall": "npm run build"
|
"postinstall": "npm run build",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
993
gpx/src/gpx.ts
993
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';
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +1,40 @@
|
|||||||
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||||
import { GPXFileType } from "./types";
|
import { GPXFileType } from './types';
|
||||||
import { GPXFile } from "./gpx";
|
import { GPXFile } from './gpx';
|
||||||
|
|
||||||
const attributesWithNamespace = {
|
const attributesWithNamespace = {
|
||||||
'RoutePointExtension': 'gpxx:RoutePointExtension',
|
RoutePointExtension: 'gpxx:RoutePointExtension',
|
||||||
'rpt': 'gpxx:rpt',
|
rpt: 'gpxx:rpt',
|
||||||
'TrackPointExtension': 'gpxtpx:TrackPointExtension',
|
TrackPointExtension: 'gpxtpx:TrackPointExtension',
|
||||||
'PowerExtension': 'gpxpx:PowerExtension',
|
PowerExtension: 'gpxpx:PowerExtension',
|
||||||
'atemp': 'gpxtpx:atemp',
|
atemp: 'gpxtpx:atemp',
|
||||||
'hr': 'gpxtpx:hr',
|
hr: 'gpxtpx:hr',
|
||||||
'cad': 'gpxtpx:cad',
|
cad: 'gpxtpx:cad',
|
||||||
'Extensions': 'gpxtpx:Extensions',
|
Extensions: 'gpxtpx:Extensions',
|
||||||
'PowerInWatts': 'gpxpx:PowerInWatts',
|
PowerInWatts: 'gpxpx:PowerInWatts',
|
||||||
'power': 'gpxpx:PowerExtension',
|
power: 'gpxpx:PowerExtension',
|
||||||
'line': 'gpx_style:line',
|
line: 'gpx_style:line',
|
||||||
'color': 'gpx_style:color',
|
color: 'gpx_style:color',
|
||||||
'opacity': 'gpx_style:opacity',
|
opacity: 'gpx_style:opacity',
|
||||||
'width': 'gpx_style:width',
|
width: 'gpx_style:width',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseGPX(gpxData: string): GPXFile {
|
export function parseGPX(gpxData: string): GPXFile {
|
||||||
const parser = new XMLParser({
|
const parser = new XMLParser({
|
||||||
ignoreAttributes: false,
|
ignoreAttributes: false,
|
||||||
attributeNamePrefix: "",
|
attributeNamePrefix: '',
|
||||||
attributesGroupName: 'attributes',
|
attributesGroupName: 'attributes',
|
||||||
removeNSPrefix: true,
|
removeNSPrefix: true,
|
||||||
isArray(name: string) {
|
isArray(name: string) {
|
||||||
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt';
|
return (
|
||||||
|
name === 'trk' ||
|
||||||
|
name === 'trkseg' ||
|
||||||
|
name === 'trkpt' ||
|
||||||
|
name === 'wpt' ||
|
||||||
|
name === 'rte' ||
|
||||||
|
name === 'rtept' ||
|
||||||
|
name === 'gpxx:rpt'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
attributeValueProcessor(attrName, attrValue, jPath) {
|
attributeValueProcessor(attrName, attrValue, jPath) {
|
||||||
if (attrName === 'lat' || attrName === 'lon') {
|
if (attrName === 'lat' || attrName === 'lon') {
|
||||||
@ -51,8 +59,14 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
return new Date(tagValue);
|
return new Date(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' ||
|
if (
|
||||||
tagName === 'gpx_style:opacity' || tagName === 'gpx_style:width') {
|
tagName === 'gpxtpx:atemp' ||
|
||||||
|
tagName === 'gpxtpx:hr' ||
|
||||||
|
tagName === 'gpxtpx:cad' ||
|
||||||
|
tagName === 'gpxpx:PowerInWatts' ||
|
||||||
|
tagName === 'gpx_style:opacity' ||
|
||||||
|
tagName === 'gpx_style:width'
|
||||||
|
) {
|
||||||
return parseFloat(tagValue);
|
return parseFloat(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +74,7 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
||||||
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
||||||
return {
|
return {
|
||||||
'gpxpx:PowerInWatts': parseFloat(tagValue)
|
'gpxpx:PowerInWatts': parseFloat(tagValue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,7 +86,7 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (parsed.metadata === "") {
|
if (parsed.metadata === '') {
|
||||||
parsed.metadata = {};
|
parsed.metadata = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +99,7 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
|||||||
const builder = new XMLBuilder({
|
const builder = new XMLBuilder({
|
||||||
format: true,
|
format: true,
|
||||||
ignoreAttributes: false,
|
ignoreAttributes: false,
|
||||||
attributeNamePrefix: "",
|
attributeNamePrefix: '',
|
||||||
attributesGroupName: 'attributes',
|
attributesGroupName: 'attributes',
|
||||||
suppressEmptyNode: true,
|
suppressEmptyNode: true,
|
||||||
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
|
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
|
||||||
@ -96,13 +110,13 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!gpx.attributes)
|
if (!gpx.attributes) gpx.attributes = {};
|
||||||
gpx.attributes = {};
|
|
||||||
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
|
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
|
||||||
gpx.attributes['version'] = '1.1';
|
gpx.attributes['version'] = '1.1';
|
||||||
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
||||||
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
||||||
gpx.attributes['xsi:schemaLocation'] = 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
gpx.attributes['xsi:schemaLocation'] =
|
||||||
|
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
||||||
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
||||||
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
||||||
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
||||||
@ -113,19 +127,24 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder.build({
|
return builder.build({
|
||||||
"?xml": {
|
'?xml': {
|
||||||
attributes: {
|
attributes: {
|
||||||
version: "1.0",
|
version: '1.0',
|
||||||
encoding: "UTF-8",
|
encoding: 'UTF-8',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
gpx: removeEmptyElements(gpx)
|
gpx: removeEmptyElements(gpx),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) {
|
if (
|
||||||
|
obj[key] === null ||
|
||||||
|
obj[key] === undefined ||
|
||||||
|
obj[key] === '' ||
|
||||||
|
(Array.isArray(obj[key]) && obj[key].length === 0)
|
||||||
|
) {
|
||||||
delete obj[key];
|
delete obj[key];
|
||||||
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
||||||
removeEmptyElements(obj[key]);
|
removeEmptyElements(obj[key]);
|
||||||
@ -135,4 +154,4 @@ function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,12 +60,20 @@ 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 {
|
||||||
// Calculates the shortest distance in meters
|
// Calculates the shortest distance in meters
|
||||||
// between an arc (defined by p1 and p2) and a third point, p3.
|
// between an arc (defined by p1 and p2) and a third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||||
|
|
||||||
@ -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,15 +179,23 @@ 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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,11 +93,11 @@ export type TrackPointExtension = {
|
|||||||
'gpxtpx:hr'?: number;
|
'gpxtpx:hr'?: number;
|
||||||
'gpxtpx:cad'?: number;
|
'gpxtpx:cad'?: number;
|
||||||
'gpxtpx:Extensions'?: Record<string, string>;
|
'gpxtpx:Extensions'?: Record<string, string>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PowerExtension = {
|
export type PowerExtension = {
|
||||||
'gpxpx:PowerInWatts'?: number;
|
'gpxpx:PowerInWatts'?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Author = {
|
export type Author = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -114,12 +114,12 @@ export type RouteType = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
extensions?: TrackExtensions;
|
extensions?: TrackExtensions;
|
||||||
rtept: WaypointType[];
|
rtept: WaypointType[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type RoutePointExtension = {
|
export type RoutePointExtension = {
|
||||||
'gpxx:rpt'?: GPXXRoutePoint[];
|
'gpxx:rpt'?: GPXXRoutePoint[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GPXXRoutePoint = {
|
export type GPXXRoutePoint = {
|
||||||
attributes: Coordinates;
|
attributes: Coordinates;
|
||||||
}
|
};
|
||||||
|
|||||||
@ -4,9 +4,7 @@
|
|||||||
"target": "ES2015",
|
"target": "ES2015",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
}
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
/** @type { import("eslint").Linter.Config } */
|
/** @type { import("eslint").Linter.Config } */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: [
|
||||||
'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" } }]
|
|
||||||
}
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "default",
|
"style": "default",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "slate"
|
"baseColor": "slate"
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "$lib/components",
|
"components": "$lib/components",
|
||||||
"utils": "$lib/utils"
|
"utils": "$lib/utils"
|
||||||
},
|
},
|
||||||
"typescript": true
|
"typescript": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
14
website/src/app.d.ts
vendored
14
website/src/app.d.ts
vendored
@ -1,13 +1,13 @@
|
|||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
// interface Locals {}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body data-sveltekit-preload-data="hover">
|
||||||
<meta charset="utf-8" />
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
</body>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
</html>
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,86 +1,86 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
|
||||||
--muted-foreground: 215.4 16.3% 45%;
|
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
|
||||||
--input: 214.3 31.8% 91.4%;
|
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
|
||||||
--primary-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--accent: 210 40% 92%;
|
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
|
||||||
--destructive-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--support: 220 15 130;
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 45%;
|
||||||
|
|
||||||
--link: 0 110 180;
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: 222.2 84% 4.9%;
|
|
||||||
--foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
|
||||||
--popover-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
|
||||||
--card-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
|
||||||
--input: 217.2 32.6% 17.5%;
|
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
|
||||||
--secondary-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--accent: 217.2 32.6% 30%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--support: 255 110 190;
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--link: 80 190 255;
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: hsl(212.7,26.8%,83.9);
|
|
||||||
}
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 92%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 72.2% 50.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--support: 220 15 130;
|
||||||
|
|
||||||
|
--link: 0 110 180;
|
||||||
|
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 30%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--support: 255 110 190;
|
||||||
|
|
||||||
|
--link: 80 190 255;
|
||||||
|
|
||||||
|
--ring: hsl(212.7, 26.8%, 83.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,8 +46,9 @@ export async function handle({ event, resolve }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await resolve(event, {
|
const response = await resolve(event, {
|
||||||
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag),
|
transformPageChunk: ({ html }) =>
|
||||||
|
html.replace('<html>', htmlTag).replace('<head>', headTag),
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
export const surfaceColors: { [key: string]: string } = {
|
export const surfaceColors: { [key: string]: string } = {
|
||||||
"missing": "#d1d1d1",
|
missing: '#d1d1d1',
|
||||||
"paved": "#8c8c8c",
|
paved: '#8c8c8c',
|
||||||
"unpaved": "#6b443a",
|
unpaved: '#6b443a',
|
||||||
"asphalt": "#8c8c8c",
|
asphalt: '#8c8c8c',
|
||||||
"concrete": "#8c8c8c",
|
concrete: '#8c8c8c',
|
||||||
"cobblestone": "#ffd991",
|
cobblestone: '#ffd991',
|
||||||
"paving_stones": "#8c8c8c",
|
paving_stones: '#8c8c8c',
|
||||||
"sett": "#ffd991",
|
sett: '#ffd991',
|
||||||
"metal": "#8c8c8c",
|
metal: '#8c8c8c',
|
||||||
"wood": "#6b443a",
|
wood: '#6b443a',
|
||||||
"compacted": "#ffffa8",
|
compacted: '#ffffa8',
|
||||||
"fine_gravel": "#ffffa8",
|
fine_gravel: '#ffffa8',
|
||||||
"gravel": "#ffffa8",
|
gravel: '#ffffa8',
|
||||||
"pebblestone": "#ffffa8",
|
pebblestone: '#ffffa8',
|
||||||
"rock": "#ffd991",
|
rock: '#ffd991',
|
||||||
"dirt": "#ffffa8",
|
dirt: '#ffffa8',
|
||||||
"ground": "#6b443a",
|
ground: '#6b443a',
|
||||||
"earth": "#6b443a",
|
earth: '#6b443a',
|
||||||
"mud": "#6b443a",
|
mud: '#6b443a',
|
||||||
"sand": "#ffffc4",
|
sand: '#ffffc4',
|
||||||
"grass": "#61b55c",
|
grass: '#61b55c',
|
||||||
"grass_paver": "#61b55c",
|
grass_paver: '#61b55c',
|
||||||
"clay": "#6b443a",
|
clay: '#6b443a',
|
||||||
"stone": "#ffd991",
|
stone: '#ffd991',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSurfaceColor(surface: string): string {
|
export function getSurfaceColor(surface: string): string {
|
||||||
@ -30,66 +30,72 @@ export function getSurfaceColor(surface: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const highwayColors: { [key: string]: string } = {
|
export const highwayColors: { [key: string]: string } = {
|
||||||
"missing": "#d1d1d1",
|
missing: '#d1d1d1',
|
||||||
"motorway": "#ff4d33",
|
motorway: '#ff4d33',
|
||||||
"motorway_link": "#ff4d33",
|
motorway_link: '#ff4d33',
|
||||||
"trunk": "#ff5e4d",
|
trunk: '#ff5e4d',
|
||||||
"trunk_link": "#ff947f",
|
trunk_link: '#ff947f',
|
||||||
"primary": "#ff6e5c",
|
primary: '#ff6e5c',
|
||||||
"primary_link": "#ff6e5c",
|
primary_link: '#ff6e5c',
|
||||||
"secondary": "#ff8d7b",
|
secondary: '#ff8d7b',
|
||||||
"secondary_link": "#ff8d7b",
|
secondary_link: '#ff8d7b',
|
||||||
"tertiary": "#ffd75f",
|
tertiary: '#ffd75f',
|
||||||
"tertiary_link": "#ffd75f",
|
tertiary_link: '#ffd75f',
|
||||||
"unclassified": "#f1f2a5",
|
unclassified: '#f1f2a5',
|
||||||
"road": "#f1f2a5",
|
road: '#f1f2a5',
|
||||||
"residential": "#73b2ff",
|
residential: '#73b2ff',
|
||||||
"living_street": "#73b2ff",
|
living_street: '#73b2ff',
|
||||||
"service": "#9c9cd9",
|
service: '#9c9cd9',
|
||||||
"track": "#a8e381",
|
track: '#a8e381',
|
||||||
"footway": "#a8e381",
|
footway: '#a8e381',
|
||||||
"path": "#a8e381",
|
path: '#a8e381',
|
||||||
"pedestrian": "#a8e381",
|
pedestrian: '#a8e381',
|
||||||
"cycleway": "#9de2ff",
|
cycleway: '#9de2ff',
|
||||||
"construction": "#e09a4a",
|
construction: '#e09a4a',
|
||||||
"bridleway": "#946f43",
|
bridleway: '#946f43',
|
||||||
"raceway": "#ff0000",
|
raceway: '#ff0000',
|
||||||
"rest_area": "#9c9cd9",
|
rest_area: '#9c9cd9',
|
||||||
"services": "#9c9cd9",
|
services: '#9c9cd9',
|
||||||
"corridor": "#474747",
|
corridor: '#474747',
|
||||||
"elevator": "#474747",
|
elevator: '#474747',
|
||||||
"steps": "#474747",
|
steps: '#474747',
|
||||||
"bus_stop": "#8545a3",
|
bus_stop: '#8545a3',
|
||||||
"busway": "#8545a3",
|
busway: '#8545a3',
|
||||||
"via_ferrata": "#474747"
|
via_ferrata: '#474747',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sacScaleColors: { [key: string]: string } = {
|
export const sacScaleColors: { [key: string]: string } = {
|
||||||
"hiking": "#007700",
|
hiking: '#007700',
|
||||||
"mountain_hiking": "#1843ad",
|
mountain_hiking: '#1843ad',
|
||||||
"demanding_mountain_hiking": "#ffff00",
|
demanding_mountain_hiking: '#ffff00',
|
||||||
"alpine_hiking": "#ff9233",
|
alpine_hiking: '#ff9233',
|
||||||
"demanding_alpine_hiking": "#ff0000",
|
demanding_alpine_hiking: '#ff0000',
|
||||||
"difficult_alpine_hiking": "#000000",
|
difficult_alpine_hiking: '#000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mtbScaleColors: { [key: string]: string } = {
|
export const mtbScaleColors: { [key: string]: string } = {
|
||||||
"0-": "#007700",
|
'0-': '#007700',
|
||||||
"0": "#007700",
|
'0': '#007700',
|
||||||
"0+": "#007700",
|
'0+': '#007700',
|
||||||
"1-": "#1843ad",
|
'1-': '#1843ad',
|
||||||
"1": "#1843ad",
|
'1': '#1843ad',
|
||||||
"1+": "#1843ad",
|
'1+': '#1843ad',
|
||||||
"2-": "#ffff00",
|
'2-': '#ffff00',
|
||||||
"2": "#ffff00",
|
'2': '#ffff00',
|
||||||
"2+": "#ffff00",
|
'2+': '#ffff00',
|
||||||
"3": "#ff0000",
|
'3': '#ff0000',
|
||||||
"4": "#00ff00",
|
'4': '#00ff00',
|
||||||
"5": "#000000",
|
'5': '#000000',
|
||||||
"6": "#b105eb",
|
'6': '#b105eb',
|
||||||
};
|
};
|
||||||
|
|
||||||
function createPattern(backgroundColor: string, sacScaleColor: string | undefined, mtbScaleColor: string | undefined, size: number = 16, lineWidth: number = 4) {
|
function createPattern(
|
||||||
|
backgroundColor: string,
|
||||||
|
sacScaleColor: string | undefined,
|
||||||
|
mtbScaleColor: string | undefined,
|
||||||
|
size: number = 16,
|
||||||
|
lineWidth: number = 4
|
||||||
|
) {
|
||||||
let canvas = document.createElement('canvas');
|
let canvas = document.createElement('canvas');
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
@ -104,11 +110,11 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
|||||||
if (sacScaleColor) {
|
if (sacScaleColor) {
|
||||||
ctx.strokeStyle = sacScaleColor;
|
ctx.strokeStyle = sacScaleColor;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(halfSize - halfLineWidth, - halfLineWidth);
|
ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
|
||||||
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
|
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(- halfLineWidth, halfSize - halfLineWidth);
|
ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
|
||||||
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
|
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
@ -119,8 +125,8 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
|||||||
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
|
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(- halfLineWidth, halfSize + halfLineWidth);
|
ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
|
||||||
ctx.lineTo(halfSize + halfLineWidth, - halfLineWidth);
|
ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,12 +134,16 @@ function createPattern(backgroundColor: string, sacScaleColor: string | undefine
|
|||||||
}
|
}
|
||||||
|
|
||||||
const patterns: Record<string, string | CanvasPattern> = {};
|
const patterns: Record<string, string | CanvasPattern> = {};
|
||||||
export function getHighwayColor(highway: string, sacScale: string | undefined, mtbScale: string | undefined) {
|
export function getHighwayColor(
|
||||||
|
highway: string,
|
||||||
|
sacScale: string | undefined,
|
||||||
|
mtbScale: string | undefined
|
||||||
|
) {
|
||||||
let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
|
let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
|
||||||
let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
|
let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
|
||||||
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
|
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
|
||||||
if (sacScale || mtbScale) {
|
if (sacScale || mtbScale) {
|
||||||
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter(x => x).join('-')}`;
|
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter((x) => x).join('-')}`;
|
||||||
if (!patterns[patternId]) {
|
if (!patterns[patternId]) {
|
||||||
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
|
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
|
||||||
}
|
}
|
||||||
@ -158,4 +168,4 @@ export function getSlopeColor(slope: number): string {
|
|||||||
let lightness = 90 - Math.abs(v) * 70;
|
let lightness = 90 - Math.abs(v) * 70;
|
||||||
|
|
||||||
return `hsl(${hue},70%,${lightness}%)`;
|
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
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,67 @@
|
|||||||
import { Landmark, Icon, Shell, Bike, Building, Tent, Car, Wrench, ShoppingBasket, Droplet, DoorOpen, Trees, Fuel, Home, Info, TreeDeciduous, CircleParking, Cross, Utensils, Construction, BrickWall, ShowerHead, Mountain, Phone, TrainFront, Bed, Binoculars, TriangleAlert, Anchor, Toilet } from "lucide-svelte";
|
import {
|
||||||
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg, Toilet as ToiletSvg } from "lucide-static";
|
Landmark,
|
||||||
import type { ComponentType } from "svelte";
|
Icon,
|
||||||
|
Shell,
|
||||||
|
Bike,
|
||||||
|
Building,
|
||||||
|
Tent,
|
||||||
|
Car,
|
||||||
|
Wrench,
|
||||||
|
ShoppingBasket,
|
||||||
|
Droplet,
|
||||||
|
DoorOpen,
|
||||||
|
Trees,
|
||||||
|
Fuel,
|
||||||
|
Home,
|
||||||
|
Info,
|
||||||
|
TreeDeciduous,
|
||||||
|
CircleParking,
|
||||||
|
Cross,
|
||||||
|
Utensils,
|
||||||
|
Construction,
|
||||||
|
BrickWall,
|
||||||
|
ShowerHead,
|
||||||
|
Mountain,
|
||||||
|
Phone,
|
||||||
|
TrainFront,
|
||||||
|
Bed,
|
||||||
|
Binoculars,
|
||||||
|
TriangleAlert,
|
||||||
|
Anchor,
|
||||||
|
Toilet,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import {
|
||||||
|
Landmark as LandmarkSvg,
|
||||||
|
Shell as ShellSvg,
|
||||||
|
Bike as BikeSvg,
|
||||||
|
Building as BuildingSvg,
|
||||||
|
Tent as TentSvg,
|
||||||
|
Car as CarSvg,
|
||||||
|
Wrench as WrenchSvg,
|
||||||
|
ShoppingBasket as ShoppingBasketSvg,
|
||||||
|
Droplet as DropletSvg,
|
||||||
|
DoorOpen as DoorOpenSvg,
|
||||||
|
Trees as TreesSvg,
|
||||||
|
Fuel as FuelSvg,
|
||||||
|
Home as HomeSvg,
|
||||||
|
Info as InfoSvg,
|
||||||
|
TreeDeciduous as TreeDeciduousSvg,
|
||||||
|
CircleParking as CircleParkingSvg,
|
||||||
|
Cross as CrossSvg,
|
||||||
|
Utensils as UtensilsSvg,
|
||||||
|
Construction as ConstructionSvg,
|
||||||
|
BrickWall as BrickWallSvg,
|
||||||
|
ShowerHead as ShowerHeadSvg,
|
||||||
|
Mountain as MountainSvg,
|
||||||
|
Phone as PhoneSvg,
|
||||||
|
TrainFront as TrainFrontSvg,
|
||||||
|
Bed as BedSvg,
|
||||||
|
Binoculars as BinocularsSvg,
|
||||||
|
TriangleAlert as TriangleAlertSvg,
|
||||||
|
Anchor as AnchorSvg,
|
||||||
|
Toilet as ToiletSvg,
|
||||||
|
} from 'lucide-static';
|
||||||
|
import type { ComponentType } from 'svelte';
|
||||||
|
|
||||||
export type Symbol = {
|
export type Symbol = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
|
|||||||
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
||||||
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
||||||
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
|
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
|
||||||
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
convenience_store: {
|
||||||
|
value: 'Convenience Store',
|
||||||
|
icon: ShoppingBasket,
|
||||||
|
iconSvg: ShoppingBasketSvg,
|
||||||
|
},
|
||||||
crossing: { value: 'Crossing' },
|
crossing: { value: 'Crossing' },
|
||||||
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
department_store: {
|
||||||
|
value: 'Department Store',
|
||||||
|
icon: ShoppingBasket,
|
||||||
|
iconSvg: ShoppingBasketSvg,
|
||||||
|
},
|
||||||
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
|
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
|
||||||
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
||||||
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
||||||
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
||||||
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
||||||
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
|
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
|
||||||
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg },
|
ground_transportation: {
|
||||||
|
value: 'Ground Transportation',
|
||||||
|
icon: TrainFront,
|
||||||
|
iconSvg: TrainFrontSvg,
|
||||||
|
},
|
||||||
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
|
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
|
||||||
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
||||||
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
||||||
@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
|
|||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
return Object.keys(symbols).find(key => symbols[key].value === value);
|
return Object.keys(symbols).find((key) => symbols[key].value === value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,60 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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, waitLocale } from 'svelte-i18n';
|
||||||
|
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
function initDocsearch() {
|
function initDocsearch() {
|
||||||
docsearch({
|
docsearch({
|
||||||
appId: '21XLD94PE3',
|
appId: '21XLD94PE3',
|
||||||
apiKey: 'd2c1ed6cb0ed12adb2bd84eb2a38494d',
|
apiKey: 'd2c1ed6cb0ed12adb2bd84eb2a38494d',
|
||||||
indexName: 'gpx',
|
indexName: 'gpx',
|
||||||
container: '#docsearch',
|
container: '#docsearch',
|
||||||
searchParameters: {
|
searchParameters: {
|
||||||
facetFilters: ['lang:' + ($locale ?? 'en')]
|
facetFilters: ['lang:' + ($locale ?? 'en')],
|
||||||
},
|
},
|
||||||
placeholder: $_('docs.search.search'),
|
placeholder: $_('docs.search.search'),
|
||||||
disableUserPersonalization: true,
|
disableUserPersonalization: true,
|
||||||
translations: {
|
translations: {
|
||||||
button: {
|
button: {
|
||||||
buttonText: $_('docs.search.search'),
|
buttonText: $_('docs.search.search'),
|
||||||
buttonAriaLabel: $_('docs.search.search')
|
buttonAriaLabel: $_('docs.search.search'),
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
searchBox: {
|
searchBox: {
|
||||||
resetButtonTitle: $_('docs.search.clear'),
|
resetButtonTitle: $_('docs.search.clear'),
|
||||||
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'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (mounted && $locale) {
|
$: if (mounted && $locale) {
|
||||||
waitLocale().then(initDocsearch);
|
waitLocale().then(initDocsearch);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
|
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div id="docsearch" {...$$restProps}></div>
|
<div id="docsearch" {...$$restProps}></div>
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
<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';
|
import type { Builder } from 'bits-ui';
|
||||||
|
|
||||||
export let variant:
|
export let variant:
|
||||||
| 'default'
|
| 'default'
|
||||||
| 'secondary'
|
| 'secondary'
|
||||||
| 'link'
|
| 'link'
|
||||||
| 'destructive'
|
| 'destructive'
|
||||||
| 'outline'
|
| 'outline'
|
||||||
| 'ghost'
|
| 'ghost'
|
||||||
| 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[] = [];
|
export let builders: Builder[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild let:builder>
|
<Tooltip.Trigger asChild let:builder>
|
||||||
<Button builders={[...builders, builder]} {variant} {...$$restProps} on:click>
|
<Button builders={[...builders, builder]} {variant} {...$$restProps} on:click>
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content {side}>
|
<Tooltip.Content {side}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { trackpointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
import { trackpointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
||||||
import { TrackPoint } from 'gpx';
|
import { TrackPoint } from 'gpx';
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
$map.on('contextmenu', (e) => {
|
$map.on('contextmenu', (e) => {
|
||||||
trackpointPopup?.setItem({
|
trackpointPopup?.setItem({
|
||||||
item: new TrackPoint({
|
item: new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: e.lngLat.lat,
|
lat: e.lngLat.lat,
|
||||||
lon: e.lngLat.lng
|
lon: e.lngLat.lng,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,186 +1,190 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { Dialog } from 'bits-ui';
|
import { Dialog } from 'bits-ui';
|
||||||
import {
|
import {
|
||||||
currentTool,
|
currentTool,
|
||||||
exportAllFiles,
|
exportAllFiles,
|
||||||
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,
|
||||||
Earth,
|
Earth,
|
||||||
HeartPulse,
|
HeartPulse,
|
||||||
Orbit,
|
Orbit,
|
||||||
Thermometer,
|
Thermometer,
|
||||||
SquareActivity
|
SquareActivity,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { selection } from './file-list/Selection';
|
import { selection } from './file-list/Selection';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { GPXStatistics } from 'gpx';
|
import { GPXStatistics } from 'gpx';
|
||||||
import { ListRootItem } from './file-list/FileList';
|
import { ListRootItem } from './file-list/FileList';
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
let exportOptions: Record<string, boolean> = {
|
let exportOptions: Record<string, boolean> = {
|
||||||
time: true,
|
time: true,
|
||||||
hr: true,
|
hr: true,
|
||||||
cad: true,
|
cad: true,
|
||||||
atemp: true,
|
atemp: true,
|
||||||
power: true,
|
power: true,
|
||||||
extensions: true
|
extensions: true,
|
||||||
};
|
};
|
||||||
let hide: Record<string, boolean> = {
|
let hide: Record<string, boolean> = {
|
||||||
time: false,
|
time: false,
|
||||||
hr: false,
|
hr: false,
|
||||||
cad: false,
|
cad: false,
|
||||||
atemp: false,
|
atemp: false,
|
||||||
power: false,
|
power: false,
|
||||||
extensions: false
|
extensions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if ($exportState !== ExportState.NONE) {
|
$: if ($exportState !== ExportState.NONE) {
|
||||||
open = true;
|
open = true;
|
||||||
$currentTool = null;
|
$currentTool = null;
|
||||||
|
|
||||||
let statistics = $gpxStatistics;
|
let statistics = $gpxStatistics;
|
||||||
if ($exportState === ExportState.ALL) {
|
if ($exportState === ExportState.ALL) {
|
||||||
statistics = Array.from($fileObservers.values())
|
statistics = Array.from($fileObservers.values())
|
||||||
.map((file) => get(file)?.statistics)
|
.map((file) => get(file)?.statistics)
|
||||||
.reduce((acc, cur) => {
|
.reduce((acc, cur) => {
|
||||||
if (cur !== undefined) {
|
if (cur !== undefined) {
|
||||||
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, new GPXStatistics());
|
}, new GPXStatistics());
|
||||||
}
|
}
|
||||||
|
|
||||||
hide.time = statistics.global.time.total === 0;
|
hide.time = statistics.global.time.total === 0;
|
||||||
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;
|
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]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
bind:open
|
bind:open
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
$exportState = ExportState.NONE;
|
$exportState = ExportState.NONE;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog.Trigger class="hidden" />
|
<Dialog.Trigger class="hidden" />
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
|
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
|
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
|
||||||
>
|
>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span class="max-w-[80%] text-sm">
|
<span class="max-w-[80%] text-sm">
|
||||||
{$_('menu.support_message')}
|
{$_('menu.support_message')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row flex-wrap gap-2">
|
<div class="w-full flex flex-row flex-wrap gap-2">
|
||||||
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
|
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
|
||||||
{$_('menu.support_button')}
|
{$_('menu.support_button')}
|
||||||
<span class="ml-2">🙏</span>
|
<span class="ml-2">🙏</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="grow"
|
class="grow"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if ($exportState === ExportState.SELECTION) {
|
if ($exportState === ExportState.SELECTION) {
|
||||||
exportSelectedFiles(exclude);
|
exportSelectedFiles(exclude);
|
||||||
} else if ($exportState === ExportState.ALL) {
|
} else if ($exportState === ExportState.ALL) {
|
||||||
exportAllFiles(exclude);
|
exportAllFiles(exclude);
|
||||||
}
|
}
|
||||||
open = false;
|
open = false;
|
||||||
$exportState = ExportState.NONE;
|
$exportState = ExportState.NONE;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Download size="16" class="mr-1" />
|
<Download size="16" class="mr-1" />
|
||||||
{#if $fileObservers.size === 1 || ($exportState === ExportState.SELECTION && $selection.size === 1)}
|
{#if $fileObservers.size === 1 || ($exportState === ExportState.SELECTION && $selection.size === 1)}
|
||||||
{$_('menu.download_file')}
|
{$_('menu.download_file')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('menu.download_files')}
|
{$_('menu.download_files')}
|
||||||
{/if}
|
{/if}
|
||||||
</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'}"
|
)
|
||||||
>
|
? ''
|
||||||
<div class="w-full flex flex-row items-center gap-3">
|
: 'hidden'}"
|
||||||
<div class="grow">
|
>
|
||||||
<Separator />
|
<div class="w-full flex flex-row items-center gap-3">
|
||||||
</div>
|
<div class="grow">
|
||||||
<Label class="shrink-0">
|
<Separator />
|
||||||
{$_('menu.export_options')}
|
</div>
|
||||||
</Label>
|
<Label class="shrink-0">
|
||||||
<div class="grow">
|
{$_('menu.export_options')}
|
||||||
<Separator />
|
</Label>
|
||||||
</div>
|
<div class="grow">
|
||||||
</div>
|
<Separator />
|
||||||
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
|
</div>
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
|
</div>
|
||||||
<Checkbox id="export-time" bind:checked={exportOptions.time} />
|
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
|
||||||
<Label for="export-time" class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
|
||||||
<Zap size="16" />
|
<Checkbox id="export-time" bind:checked={exportOptions.time} />
|
||||||
{$_('quantities.time')}
|
<Label for="export-time" class="flex flex-row items-center gap-1">
|
||||||
</Label>
|
<Zap size="16" />
|
||||||
</div>
|
{$_('quantities.time')}
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}">
|
</Label>
|
||||||
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
|
</div>
|
||||||
<Label for="export-extensions" class="flex flex-row items-center gap-1">
|
<div
|
||||||
<Earth size="16" />
|
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
|
||||||
{$_('quantities.osm_extensions')}
|
>
|
||||||
</Label>
|
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
|
||||||
</div>
|
<Label for="export-extensions" class="flex flex-row items-center gap-1">
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
|
<Earth size="16" />
|
||||||
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
|
{$_('quantities.osm_extensions')}
|
||||||
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
|
</Label>
|
||||||
<HeartPulse size="16" />
|
</div>
|
||||||
{$_('quantities.heartrate')}
|
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
|
||||||
</Label>
|
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
|
||||||
</div>
|
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
|
<HeartPulse size="16" />
|
||||||
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
|
{$_('quantities.heartrate')}
|
||||||
<Label for="export-cadence" class="flex flex-row items-center gap-1">
|
</Label>
|
||||||
<Orbit size="16" />
|
</div>
|
||||||
{$_('quantities.cadence')}
|
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
|
||||||
</Label>
|
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
|
||||||
</div>
|
<Label for="export-cadence" class="flex flex-row items-center gap-1">
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
|
<Orbit size="16" />
|
||||||
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
|
{$_('quantities.cadence')}
|
||||||
<Label for="export-temperature" class="flex flex-row items-center gap-1">
|
</Label>
|
||||||
<Thermometer size="16" />
|
</div>
|
||||||
{$_('quantities.temperature')}
|
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
|
||||||
</Label>
|
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
|
||||||
</div>
|
<Label for="export-temperature" class="flex flex-row items-center gap-1">
|
||||||
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
|
<Thermometer size="16" />
|
||||||
<Checkbox id="export-power" bind:checked={exportOptions.power} />
|
{$_('quantities.temperature')}
|
||||||
<Label for="export-power" class="flex flex-row items-center gap-1">
|
</Label>
|
||||||
<SquareActivity size="16" />
|
</div>
|
||||||
{$_('quantities.power')}
|
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
|
||||||
</Label>
|
<Checkbox id="export-power" bind:checked={exportOptions.power} />
|
||||||
</div>
|
<Label for="export-power" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<SquareActivity size="16" />
|
||||||
</div>
|
{$_('quantities.power')}
|
||||||
</Dialog.Content>
|
</Label>
|
||||||
</Dialog.Portal>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@ -1,125 +1,125 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
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 'svelte-i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="w-full">
|
<footer class="w-full">
|
||||||
<div class="mx-6 border-t">
|
<div class="mx-6 border-t">
|
||||||
<div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
<div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||||
<div class="grow flex flex-col items-start">
|
<div class="grow flex flex-col items-start">
|
||||||
<Logo class="h-8" width="153" />
|
<Logo class="h-8" width="153" />
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
MIT © 2024 gpx.studio
|
MIT © 2024 gpx.studio
|
||||||
</Button>
|
</Button>
|
||||||
<LanguageSelect class="w-40 mt-3" />
|
<LanguageSelect class="w-40 mt-3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||||
<div class="flex flex-col items-start gap-1">
|
<div class="flex flex-col items-start gap-1">
|
||||||
<span class="font-semibold">{$_('homepage.website')}</span>
|
<span class="font-semibold">{$_('homepage.website')}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage($locale, '/')}
|
href={getURLForLanguage($locale, '/')}
|
||||||
>
|
>
|
||||||
<Home size="16" class="mr-1" />
|
<Home size="16" class="mr-1" />
|
||||||
{$_('homepage.home')}
|
{$_('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage($locale, '/app')}
|
href={getURLForLanguage($locale, '/app')}
|
||||||
>
|
>
|
||||||
<Map size="16" class="mr-1" />
|
<Map size="16" class="mr-1" />
|
||||||
{$_('homepage.app')}
|
{$_('homepage.app')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage($locale, '/help')}
|
href={getURLForLanguage($locale, '/help')}
|
||||||
>
|
>
|
||||||
<BookOpenText size="16" class="mr-1" />
|
<BookOpenText size="16" class="mr-1" />
|
||||||
{$_('menu.help')}
|
{$_('menu.help')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-start gap-1" id="contact">
|
<div class="flex flex-col items-start gap-1" id="contact">
|
||||||
<span class="font-semibold">{$_('homepage.contact')}</span>
|
<span class="font-semibold">{$_('homepage.contact')}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://www.reddit.com/r/gpxstudio/"
|
href="https://www.reddit.com/r/gpxstudio/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Logo company="reddit" class="h-4 mr-1 fill-muted-foreground" />
|
<Logo company="reddit" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
{$_('homepage.reddit')}
|
{$_('homepage.reddit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://facebook.com/gpx.studio"
|
href="https://facebook.com/gpx.studio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Logo company="facebook" class="h-4 mr-1 fill-muted-foreground" />
|
<Logo company="facebook" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
{$_('homepage.facebook')}
|
{$_('homepage.facebook')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://x.com/gpxstudio"
|
href="https://x.com/gpxstudio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Logo company="x" class="h-4 mr-1 fill-muted-foreground" />
|
<Logo company="x" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
{$_('homepage.x')}
|
{$_('homepage.x')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="mailto:hello@gpx.studio"
|
href="mailto:hello@gpx.studio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<AtSign size="16" class="mr-1" />
|
<AtSign size="16" class="mr-1" />
|
||||||
{$_('homepage.email')}
|
{$_('homepage.email')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-start gap-1">
|
<div class="flex flex-col items-start gap-1">
|
||||||
<span class="font-semibold">{$_('homepage.contribute')}</span>
|
<span class="font-semibold">{$_('homepage.contribute')}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://ko-fi.com/gpxstudio"
|
href="https://ko-fi.com/gpxstudio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Heart size="16" class="mr-1" />
|
<Heart size="16" class="mr-1" />
|
||||||
{$_('menu.donate')}
|
{$_('menu.donate')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://crowdin.com/project/gpxstudio"
|
href="https://crowdin.com/project/gpxstudio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Logo company="crowdin" class="h-4 mr-1 fill-muted-foreground" />
|
<Logo company="crowdin" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
{$_('homepage.crowdin')}
|
{$_('homepage.crowdin')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
href="https://github.com/gpxstudio/gpx.studio"
|
href="https://github.com/gpxstudio/gpx.studio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Logo company="github" class="h-4 mr-1 fill-muted-foreground" />
|
<Logo company="github" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
{$_('homepage.github')}
|
{$_('homepage.github')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -1,82 +1,88 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
|
|
||||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-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';
|
||||||
|
|
||||||
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 orientation: 'horizontal' | 'vertical';
|
export let orientation: 'horizontal' | 'vertical';
|
||||||
export let panelSize: number;
|
export let panelSize: number;
|
||||||
|
|
||||||
const { velocityUnits } = settings;
|
const { velocityUnits } = settings;
|
||||||
|
|
||||||
let statistics: GPXStatistics;
|
let statistics: GPXStatistics;
|
||||||
|
|
||||||
$: if ($slicedGPXStatistics !== undefined) {
|
$: if ($slicedGPXStatistics !== undefined) {
|
||||||
statistics = $slicedGPXStatistics[0];
|
statistics = $slicedGPXStatistics[0];
|
||||||
} else {
|
} else {
|
||||||
statistics = $gpxStatistics;
|
statistics = $gpxStatistics;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
class="h-full {orientation === 'vertical'
|
class="h-full {orientation === 'vertical'
|
||||||
? 'min-w-40 sm:min-w-44 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
|
||||||
class="h-full flex {orientation === 'vertical'
|
class="h-full flex {orientation === 'vertical'
|
||||||
? 'flex-col justify-center'
|
? 'flex-col justify-center'
|
||||||
: 'flex-row w-full justify-between'} gap-4 p-0"
|
: 'flex-row w-full justify-between'} gap-4 p-0"
|
||||||
>
|
>
|
||||||
<Tooltip label={$_('quantities.distance')}>
|
<Tooltip label={$_('quantities.distance')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Ruler size="16" 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="16" 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="16" 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="16" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
|
<WithUnits
|
||||||
<span class="mx-1">/</span>
|
value={statistics.global.speed.moving}
|
||||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
type="speed"
|
||||||
</span>
|
showUnits={false}
|
||||||
</Tooltip>
|
/>
|
||||||
{/if}
|
<span class="mx-1">/</span>
|
||||||
{#if panelSize > 160 || orientation === 'horizontal'}
|
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||||
<Tooltip
|
</span>
|
||||||
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
</Tooltip>
|
||||||
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})"
|
{/if}
|
||||||
>
|
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||||
<span class="flex flex-row items-center">
|
<Tooltip
|
||||||
<Timer size="16" class="mr-1" />
|
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
||||||
<WithUnits value={statistics.global.time.moving} type="time" />
|
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_(
|
||||||
<span class="mx-1">/</span>
|
'quantities.total'
|
||||||
<WithUnits value={statistics.global.time.total} type="time" />
|
)})"
|
||||||
</span>
|
>
|
||||||
</Tooltip>
|
<span class="flex flex-row items-center">
|
||||||
{/if}
|
<Timer size="16" class="mr-1" />
|
||||||
</Card.Content>
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
|
<span class="mx-1">/</span>
|
||||||
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CircleHelp } from 'lucide-svelte';
|
import { CircleHelp } from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let link: string | undefined = undefined;
|
export let link: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}"
|
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}"
|
||||||
>
|
>
|
||||||
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
|
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
|
||||||
<div>
|
<div>
|
||||||
<slot />
|
<slot />
|
||||||
{#if link}
|
{#if link}
|
||||||
<a href={link} target="_blank" class="text-sm text-link hover:underline">
|
<a href={link} target="_blank" class="text-sm text-link hover:underline">
|
||||||
{$_('menu.more')}
|
{$_('menu.more')}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,51 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
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 'svelte-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>
|
||||||
|
|
||||||
<Select.Root bind:selected>
|
<Select.Root bind:selected>
|
||||||
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}>
|
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}>
|
||||||
<Languages size="16" />
|
<Languages size="16" />
|
||||||
<Select.Value class="ml-2 mr-auto" />
|
<Select.Value class="ml-2 mr-auto" />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
{#if $page.url.pathname.includes('404')}
|
{#if $page.url.pathname.includes('404')}
|
||||||
<a href={getURLForLanguage(lang, '/')}>
|
<a href={getURLForLanguage(lang, '/')}>
|
||||||
<Select.Item value={lang}>{label}</Select.Item>
|
<Select.Item value={lang}>{label}</Select.Item>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
||||||
<Select.Item value={lang}>{label}</Select.Item>
|
<Select.Item value={lang}>{label}</Select.Item>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
|
||||||
<!-- hidden links for svelte crawling -->
|
<!-- hidden links for svelte crawling -->
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
{#if !$page.url.pathname.includes('404')}
|
{#if !$page.url.pathname.includes('404')}
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,73 +1,73 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { mode, systemPrefersMode } from 'mode-watcher';
|
import { mode, systemPrefersMode } from 'mode-watcher';
|
||||||
|
|
||||||
export let iconOnly = false;
|
export let iconOnly = false;
|
||||||
export let company = 'gpx.studio';
|
export let company = 'gpx.studio';
|
||||||
|
|
||||||
$: effectiveMode = $mode ?? $systemPrefersMode ?? 'light';
|
$: effectiveMode = $mode ?? $systemPrefersMode ?? 'light';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if company === 'gpx.studio'}
|
{#if company === 'gpx.studio'}
|
||||||
<img
|
<img
|
||||||
src="{base}/{iconOnly ? 'icon' : 'logo'}{effectiveMode === 'dark' ? '-dark' : ''}.svg"
|
src="{base}/{iconOnly ? 'icon' : 'logo'}{effectiveMode === 'dark' ? '-dark' : ''}.svg"
|
||||||
alt="Logo of gpx.studio."
|
alt="Logo of gpx.studio."
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
/>
|
/>
|
||||||
{:else if company === 'mapbox'}
|
{:else if company === 'mapbox'}
|
||||||
<img
|
<img
|
||||||
src="{base}/mapbox-logo-{effectiveMode === 'dark' ? 'white' : 'black'}.svg"
|
src="{base}/mapbox-logo-{effectiveMode === 'dark' ? 'white' : 'black'}.svg"
|
||||||
alt="Logo of Mapbox."
|
alt="Logo of Mapbox."
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
/>
|
/>
|
||||||
{:else if company === 'github'}
|
{:else if company === 'github'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="fill-foreground {$$restProps.class ?? ''}"
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
><title>GitHub</title><path
|
><title>GitHub</title><path
|
||||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{:else if company === 'crowdin'}
|
{:else if company === 'crowdin'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="fill-foreground {$$restProps.class ?? ''}"
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
><title>Crowdin</title><path
|
><title>Crowdin</title><path
|
||||||
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
|
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{:else if company === 'facebook'}
|
{:else if company === 'facebook'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="fill-foreground {$$restProps.class ?? ''}"
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
><title>Facebook</title><path
|
><title>Facebook</title><path
|
||||||
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{:else if company === 'x'}
|
{:else if company === 'x'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="fill-foreground {$$restProps.class ?? ''}"
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
><title>X</title><path
|
><title>X</title><path
|
||||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{:else if company === 'reddit'}
|
{:else if company === 'reddit'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="fill-foreground {$$restProps.class ?? ''}"
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
><title>Reddit</title><path
|
><title>Reddit</title><path
|
||||||
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
|
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@ -1,392 +1,393 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
|
|
||||||
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 'svelte-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';
|
||||||
|
|
||||||
export let accessToken = PUBLIC_MAPBOX_TOKEN;
|
export let accessToken = PUBLIC_MAPBOX_TOKEN;
|
||||||
export let geolocate = true;
|
export let geolocate = true;
|
||||||
export let geocoder = true;
|
export let geocoder = true;
|
||||||
export let hash = true;
|
export let hash = true;
|
||||||
|
|
||||||
mapboxgl.accessToken = accessToken;
|
mapboxgl.accessToken = accessToken;
|
||||||
|
|
||||||
let webgl2Supported = true;
|
let webgl2Supported = true;
|
||||||
let embeddedApp = false;
|
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, treeFileView, bottomPanelSize, rightPanelSize } =
|
const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
|
||||||
settings;
|
settings;
|
||||||
let scaleControl = new mapboxgl.ScaleControl({
|
let scaleControl = new mapboxgl.ScaleControl({
|
||||||
unit: $distanceUnits
|
unit: $distanceUnits,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let gl = document.createElement('canvas').getContext('webgl2');
|
let gl = document.createElement('canvas').getContext('webgl2');
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
webgl2Supported = false;
|
webgl2Supported = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (window.top !== window.self && !$page.route.id?.includes('embed')) {
|
if (window.top !== window.self && !$page.route.id?.includes('embed')) {
|
||||||
embeddedApp = true;
|
embeddedApp = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let language = $page.params.language;
|
let language = $page.params.language;
|
||||||
if (language === 'zh') {
|
if (language === 'zh') {
|
||||||
language = 'zh-Hans';
|
language = 'zh-Hans';
|
||||||
} else if (language?.includes('-')) {
|
} else if (language?.includes('-')) {
|
||||||
language = language.split('-')[0];
|
language = language.split('-')[0];
|
||||||
} else if (language === '' || language === undefined) {
|
} else if (language === '' || language === undefined) {
|
||||||
language = 'en';
|
language = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
let newMap = new mapboxgl.Map({
|
let newMap = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: {
|
style: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
imports: [
|
imports: [
|
||||||
{
|
{
|
||||||
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
||||||
url: '',
|
url: '',
|
||||||
data: {
|
data: {
|
||||||
version: 8,
|
version: 8,
|
||||||
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',
|
||||||
url: '',
|
url: '',
|
||||||
data: {
|
data: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {},
|
sources: {},
|
||||||
layers: []
|
layers: [],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
projection: 'globe',
|
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
|
||||||
window._map = newMap; // entry point for extensions
|
window._map = newMap; // entry point for extensions
|
||||||
scaleControl.setUnit($distanceUnits);
|
scaleControl.setUnit($distanceUnits);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (geocoder) {
|
if (geocoder) {
|
||||||
let geocoder = new MapboxGeocoder({
|
let geocoder = new MapboxGeocoder({
|
||||||
mapboxgl: mapboxgl,
|
mapboxgl: mapboxgl,
|
||||||
enableEventLogging: false,
|
enableEventLogging: false,
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
flyTo: fitBoundsOptions,
|
flyTo: fitBoundsOptions,
|
||||||
language,
|
language,
|
||||||
localGeocoder: () => [],
|
localGeocoder: () => [],
|
||||||
localGeocoderOnly: true,
|
localGeocoderOnly: true,
|
||||||
externalGeocoder: (query: string) =>
|
externalGeocoder: (query: string) =>
|
||||||
fetch(
|
fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
|
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
|
||||||
)
|
)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.map((result: any) => {
|
return data.map((result: any) => {
|
||||||
return {
|
return {
|
||||||
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) => {
|
||||||
// Trigger search on Enter key only
|
// Trigger search on Enter key only
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
|
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
|
||||||
} else if (geocoder._typeahead.data.length > 0) {
|
} else if (geocoder._typeahead.data.length > 0) {
|
||||||
geocoder._typeahead.clear();
|
geocoder._typeahead.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
newMap.addControl(geocoder);
|
newMap.addControl(geocoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geolocate) {
|
if (geolocate) {
|
||||||
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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newMap.addControl(scaleControl);
|
newMap.addControl(scaleControl);
|
||||||
|
|
||||||
newMap.on('style.load', () => {
|
newMap.on('style.load', () => {
|
||||||
newMap.addSource('mapbox-dem', {
|
newMap.addSource('mapbox-dem', {
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.remove();
|
$map.remove();
|
||||||
$map = null;
|
$map = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if ($map && (!$treeFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)) {
|
$: if ($map && (!$treeFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)) {
|
||||||
$map.resize();
|
$map.resize();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<div {...$$restProps}>
|
||||||
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
|
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported && !embeddedApp
|
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported &&
|
||||||
? 'hidden'
|
!embeddedApp
|
||||||
: ''} {embeddedApp ? 'z-30' : ''}"
|
? 'hidden'
|
||||||
>
|
: ''} {embeddedApp ? 'z-30' : ''}"
|
||||||
{#if !webgl2Supported}
|
>
|
||||||
<p>{$_('webgl2_required')}</p>
|
{#if !webgl2Supported}
|
||||||
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
<p>{$_('webgl2_required')}</p>
|
||||||
{$_('enable_webgl2')}
|
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
||||||
</Button>
|
{$_('enable_webgl2')}
|
||||||
{:else if embeddedApp}
|
</Button>
|
||||||
<p>The app cannot be embedded in an iframe.</p>
|
{:else if embeddedApp}
|
||||||
<Button href="https://gpx.studio/help/integration" target="_blank">
|
<p>The app cannot be embedded in an iframe.</p>
|
||||||
Learn how to create a map for your website
|
<Button href="https://gpx.studio/help/integration" target="_blank">
|
||||||
</Button>
|
Learn how to create a map for your website
|
||||||
{/if}
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(.mapboxgl-map) {
|
div :global(.mapboxgl-map) {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-icon) {
|
div :global(.mapboxgl-ctrl-icon) {
|
||||||
@apply dark:brightness-[4.7];
|
@apply dark:brightness-[4.7];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder) {
|
div :global(.mapboxgl-ctrl-geocoder) {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@apply min-w-fit;
|
@apply min-w-fit;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.suggestions) {
|
div :global(.suggestions) {
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply hover:text-accent-foreground;
|
@apply hover:text-accent-foreground;
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--button) {
|
div :global(.mapboxgl-ctrl-geocoder--button) {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply hover:bg-transparent;
|
@apply hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
||||||
@apply fill-foreground;
|
@apply fill-foreground;
|
||||||
@apply hover:fill-accent-foreground;
|
@apply hover:fill-accent-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply top-0;
|
@apply top-0;
|
||||||
@apply left-0;
|
@apply left-0;
|
||||||
@apply my-2;
|
@apply my-2;
|
||||||
@apply w-[29px];
|
@apply w-[29px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply w-64;
|
@apply w-64;
|
||||||
@apply py-0;
|
@apply py-0;
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
@apply transition-[width];
|
@apply transition-[width];
|
||||||
@apply duration-200;
|
@apply duration-200;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply w-0;
|
@apply w-0;
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-top-right) {
|
div :global(.mapboxgl-ctrl-top-right) {
|
||||||
@apply z-40;
|
@apply z-40;
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-col;
|
@apply flex-col;
|
||||||
@apply items-end;
|
@apply items-end;
|
||||||
@apply h-full;
|
@apply h-full;
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib) {
|
div :global(.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-transparent;
|
@apply dark:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-background;
|
@apply dark:bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib-button) {
|
div :global(.mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-attrib a) {
|
div :global(.mapboxgl-ctrl-attrib a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup) {
|
div :global(.mapboxgl-popup) {
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@apply z-50;
|
@apply z-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-content) {
|
div :global(.mapboxgl-popup-content) {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply shadow-none;
|
@apply shadow-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
||||||
@apply border-r-background;
|
@apply border-r-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
||||||
@apply border-l-background;
|
@apply border-l-background;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
<svelte:options accessors />
|
<svelte:options accessors />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TrackPoint, Waypoint } from 'gpx';
|
import { TrackPoint, Waypoint } from 'gpx';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import WaypointPopup from '$lib/components/gpx-layer/WaypointPopup.svelte';
|
import WaypointPopup from '$lib/components/gpx-layer/WaypointPopup.svelte';
|
||||||
import TrackpointPopup from '$lib/components/gpx-layer/TrackpointPopup.svelte';
|
import TrackpointPopup from '$lib/components/gpx-layer/TrackpointPopup.svelte';
|
||||||
import OverpassPopup from '$lib/components/layer-control/OverpassPopup.svelte';
|
import OverpassPopup from '$lib/components/layer-control/OverpassPopup.svelte';
|
||||||
import type { PopupItem } from './MapPopup';
|
import type { PopupItem } from './MapPopup';
|
||||||
|
|
||||||
export let item: Writable<PopupItem | null>;
|
export let item: Writable<PopupItem | null>;
|
||||||
export let container: HTMLDivElement | null = null;
|
export let container: HTMLDivElement | null = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={container}>
|
<div bind:this={container}>
|
||||||
{#if $item}
|
{#if $item}
|
||||||
{#if $item.item instanceof Waypoint}
|
{#if $item.item instanceof Waypoint}
|
||||||
<WaypointPopup waypoint={$item} />
|
<WaypointPopup waypoint={$item} />
|
||||||
{:else if $item.item instanceof TrackPoint}
|
{:else if $item.item instanceof TrackPoint}
|
||||||
<TrackpointPopup trackpoint={$item} />
|
<TrackpointPopup trackpoint={$item} />
|
||||||
{:else}
|
{:else}
|
||||||
<OverpassPopup poi={$item} />
|
<OverpassPopup poi={$item} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { TrackPoint, Waypoint } from "gpx";
|
import { TrackPoint, Waypoint } from 'gpx';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { tick } from "svelte";
|
import { tick } from 'svelte';
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import MapPopupComponent from "./MapPopup.svelte";
|
import MapPopupComponent from './MapPopup.svelte';
|
||||||
|
|
||||||
export type PopupItem<T = Waypoint | TrackPoint | any> = {
|
export type PopupItem<T = Waypoint | TrackPoint | any> = {
|
||||||
item: T;
|
item: T;
|
||||||
@ -23,16 +23,15 @@ export class MapPopup {
|
|||||||
let component = new MapPopupComponent({
|
let component = new MapPopupComponent({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
item: this.item
|
item: this.item,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
tick().then(() => this.popup.setDOMContent(component.container));
|
tick().then(() => this.popup.setDOMContent(component.container));
|
||||||
}
|
}
|
||||||
|
|
||||||
setItem(item: PopupItem | null) {
|
setItem(item: PopupItem | null) {
|
||||||
if (item)
|
if (item) item.hide = () => this.hide();
|
||||||
item.hide = () => this.hide();
|
|
||||||
this.item.set(item);
|
this.item.set(item);
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
this.hide();
|
this.hide();
|
||||||
@ -76,6 +75,8 @@ export class MapPopup {
|
|||||||
if (i === null) {
|
if (i === null) {
|
||||||
return new mapboxgl.LngLat(0, 0);
|
return new mapboxgl.LngLat(0, 0);
|
||||||
}
|
}
|
||||||
return (i.item instanceof Waypoint || i.item instanceof TrackPoint) ? i.item.getCoordinates() : new mapboxgl.LngLat(i.item.lon, i.item.lat);
|
return i.item instanceof Waypoint || i.item instanceof TrackPoint
|
||||||
|
? i.item.getCoordinates()
|
||||||
|
: new mapboxgl.LngLat(i.item.lon, i.item.lat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 'svelte-i18n';
|
||||||
|
|
||||||
export let size = '20';
|
export let size = '20';
|
||||||
|
|
||||||
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
|
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 px-1.5 {$$props.class ?? ''}"
|
class="h-8 px-1.5 {$$props.class ?? ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
setMode(selectedMode === 'light' ? 'dark' : 'light');
|
setMode(selectedMode === 'light' ? 'dark' : 'light');
|
||||||
}}
|
}}
|
||||||
aria-label={$_('menu.mode')}
|
aria-label={$_('menu.mode')}
|
||||||
>
|
>
|
||||||
{#if selectedMode === 'light'}
|
{#if selectedMode === 'light'}
|
||||||
<Sun {size} />
|
<Sun {size} />
|
||||||
{:else}
|
{:else}
|
||||||
<Moon {size} />
|
<Moon {size} />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,32 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
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 'svelte-i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="w-full sticky top-0 bg-background z-50">
|
<nav class="w-full sticky top-0 bg-background z-50">
|
||||||
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
|
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
|
||||||
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
|
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
|
||||||
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
|
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
|
||||||
<Logo class="h-8 hidden sm:block" width="153" />
|
<Logo class="h-8 hidden sm:block" width="153" />
|
||||||
</a>
|
</a>
|
||||||
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
|
||||||
<Home size="18" class="mr-1.5" />
|
<Home size="18" class="mr-1.5" />
|
||||||
{$_('homepage.home')}
|
{$_('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/app')}>
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/app')}>
|
||||||
<Map size="18" class="mr-1.5" />
|
<Map size="18" class="mr-1.5" />
|
||||||
{$_('homepage.app')}
|
{$_('homepage.app')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/help')}>
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/help')}>
|
||||||
<BookOpenText size="18" class="mr-1.5" />
|
<BookOpenText size="18" class="mr-1.5" />
|
||||||
{$_('menu.help')}
|
{$_('menu.help')}
|
||||||
</Button>
|
</Button>
|
||||||
<AlgoliaDocSearch class="ml-auto" />
|
<AlgoliaDocSearch class="ml-auto" />
|
||||||
<ModeSwitch class="hidden xs:block" />
|
<ModeSwitch class="hidden xs:block" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,41 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let orientation: 'col' | 'row' = 'col';
|
export let orientation: 'col' | 'row' = 'col';
|
||||||
|
|
||||||
export let after: number;
|
export let after: number;
|
||||||
export let minAfter: number = 0;
|
export let minAfter: number = 0;
|
||||||
export let maxAfter: number = Number.MAX_SAFE_INTEGER;
|
export let maxAfter: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
function handleMouseDown(event: PointerEvent) {
|
function handleMouseDown(event: PointerEvent) {
|
||||||
const startX = event.clientX;
|
const startX = event.clientX;
|
||||||
const startY = event.clientY;
|
const startY = event.clientY;
|
||||||
const startAfter = after;
|
const startAfter = after;
|
||||||
|
|
||||||
const handleMouseMove = (event: PointerEvent) => {
|
const handleMouseMove = (event: PointerEvent) => {
|
||||||
const newAfter =
|
const newAfter =
|
||||||
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY);
|
startAfter +
|
||||||
if (newAfter >= minAfter && newAfter <= maxAfter) {
|
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
|
||||||
after = newAfter;
|
if (newAfter >= minAfter && newAfter <= maxAfter) {
|
||||||
} else if (newAfter < minAfter && after !== minAfter) {
|
after = newAfter;
|
||||||
after = minAfter;
|
} else if (newAfter < minAfter && after !== minAfter) {
|
||||||
} else if (newAfter > maxAfter && after !== maxAfter) {
|
after = minAfter;
|
||||||
after = maxAfter;
|
} else if (newAfter > maxAfter && after !== maxAfter) {
|
||||||
}
|
after = maxAfter;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
window.removeEventListener('pointermove', handleMouseMove);
|
window.removeEventListener('pointermove', handleMouseMove);
|
||||||
window.removeEventListener('pointerup', handleMouseUp);
|
window.removeEventListener('pointerup', handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('pointermove', handleMouseMove);
|
window.addEventListener('pointermove', handleMouseMove);
|
||||||
window.addEventListener('pointerup', handleMouseUp);
|
window.addEventListener('pointerup', handleMouseUp);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="{orientation === 'col'
|
class="{orientation === 'col'
|
||||||
? 'w-1 h-full cursor-col-resize border-l'
|
? 'w-1 h-full cursor-col-resize border-l'
|
||||||
: 'w-full h-1 cursor-row-resize border-t'} {orientation}"
|
: 'w-full h-1 cursor-row-resize border-t'} {orientation}"
|
||||||
on:pointerdown={handleMouseDown}
|
on:pointerdown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,36 +1,36 @@
|
|||||||
<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 'svelte-i18n';
|
||||||
|
|
||||||
export let key: string | undefined = undefined;
|
export let key: string | undefined = undefined;
|
||||||
export let shift: boolean = false;
|
export let shift: boolean = false;
|
||||||
export let ctrl: boolean = false;
|
export let ctrl: boolean = false;
|
||||||
export let click: boolean = false;
|
export let click: boolean = false;
|
||||||
|
|
||||||
let mac = false;
|
let mac = false;
|
||||||
let safari = false;
|
let safari = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mac = isMac();
|
mac = isMac();
|
||||||
safari = isSafari();
|
safari = isSafari();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
||||||
{...$$props}
|
{...$$props}
|
||||||
>
|
>
|
||||||
{#if shift}
|
{#if shift}
|
||||||
<span>⇧</span>
|
<span>⇧</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if ctrl}
|
{#if ctrl}
|
||||||
<span>{mac && !safari ? '⌘' : $_('menu.ctrl') + '+'}</span>
|
<span>{mac && !safari ? '⌘' : $_('menu.ctrl') + '+'}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if key}
|
{#if key}
|
||||||
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if click}
|
{#if click}
|
||||||
<span>{$_('menu.click')}</span>
|
<span>{$_('menu.click')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger {...$$restProps} aria-label={label}>
|
<Tooltip.Trigger {...$$restProps} aria-label={label}>
|
||||||
<slot />
|
<slot />
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content {side}>
|
<Tooltip.Content {side}>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
|||||||
@ -1,48 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import {
|
import {
|
||||||
celsiusToFahrenheit,
|
celsiusToFahrenheit,
|
||||||
getConvertedDistance,
|
getConvertedDistance,
|
||||||
getConvertedElevation,
|
getConvertedElevation,
|
||||||
getConvertedVelocity,
|
getConvertedVelocity,
|
||||||
getDistanceUnits,
|
getDistanceUnits,
|
||||||
getElevationUnits,
|
getElevationUnits,
|
||||||
getVelocityUnits,
|
getVelocityUnits,
|
||||||
secondsToHHMMSS
|
secondsToHHMMSS,
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-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';
|
||||||
export let showUnits: boolean = true;
|
export let showUnits: boolean = true;
|
||||||
export let decimals: number | undefined = undefined;
|
export let decimals: number | undefined = undefined;
|
||||||
|
|
||||||
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class={$$props.class}>
|
<span class={$$props.class}>
|
||||||
{#if type === 'distance'}
|
{#if type === 'distance'}
|
||||||
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
|
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
{showUnits ? getDistanceUnits($distanceUnits) : ''}
|
{showUnits ? getDistanceUnits($distanceUnits) : ''}
|
||||||
{:else if type === 'elevation'}
|
{:else if type === 'elevation'}
|
||||||
{getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
|
{getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
|
||||||
{showUnits ? getElevationUnits($distanceUnits) : ''}
|
{showUnits ? getElevationUnits($distanceUnits) : ''}
|
||||||
{:else if type === 'speed'}
|
{:else if type === 'speed'}
|
||||||
{#if $velocityUnits === 'speed'}
|
{#if $velocityUnits === 'speed'}
|
||||||
{getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
|
{getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
{:else}
|
{:else}
|
||||||
{secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
|
{secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
|
||||||
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if type === 'temperature'}
|
{:else if type === 'temperature'}
|
||||||
{#if $temperatureUnits === 'celsius'}
|
{#if $temperatureUnits === 'celsius'}
|
||||||
{value} {showUnits ? $_('units.celsius') : ''}
|
{value} {showUnits ? $_('units.celsius') : ''}
|
||||||
{:else}
|
{:else}
|
||||||
{celsiusToFahrenheit(value)} {showUnits ? $_('units.fahrenheit') : ''}
|
{celsiusToFahrenheit(value)} {showUnits ? $_('units.fahrenheit') : ''}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if type === 'time'}
|
{:else if type === 'time'}
|
||||||
{secondsToHHMMSS(value)}
|
{secondsToHHMMSS(value)}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export let defaultState: 'open' | 'closed' = 'open';
|
export let defaultState: 'open' | 'closed' = 'open';
|
||||||
export let side: 'left' | 'right' = 'right';
|
export let side: 'left' | 'right' = 'right';
|
||||||
export let nohover: boolean = false;
|
export let nohover: boolean = false;
|
||||||
export let slotInsideTrigger: boolean = true;
|
export let slotInsideTrigger: boolean = true;
|
||||||
|
|
||||||
let open = writable<Record<string, boolean>>({});
|
let open = writable<Record<string, boolean>>({});
|
||||||
|
|
||||||
setContext('collapsible-tree-default-state', defaultState);
|
setContext('collapsible-tree-default-state', defaultState);
|
||||||
setContext('collapsible-tree-state', open);
|
setContext('collapsible-tree-state', open);
|
||||||
setContext('collapsible-tree-side', side);
|
setContext('collapsible-tree-side', side);
|
||||||
setContext('collapsible-tree-nohover', nohover);
|
setContext('collapsible-tree-nohover', nohover);
|
||||||
setContext('collapsible-tree-parent-id', 'root');
|
setContext('collapsible-tree-parent-id', 'root');
|
||||||
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
|
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@ -1,97 +1,97 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-svelte';
|
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||||
import { getContext, onMount, setContext } from 'svelte';
|
import { getContext, onMount, setContext } from 'svelte';
|
||||||
import { get, type Writable } from 'svelte/store';
|
import { get, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
export let id: string | number;
|
export let id: string | number;
|
||||||
|
|
||||||
let defaultState = getContext<'open' | 'closed'>('collapsible-tree-default-state');
|
let defaultState = getContext<'open' | 'closed'>('collapsible-tree-default-state');
|
||||||
let open = getContext<Writable<Record<string, boolean>>>('collapsible-tree-state');
|
let open = getContext<Writable<Record<string, boolean>>>('collapsible-tree-state');
|
||||||
let side = getContext<'left' | 'right'>('collapsible-tree-side');
|
let side = getContext<'left' | 'right'>('collapsible-tree-side');
|
||||||
let nohover = getContext<boolean>('collapsible-tree-nohover');
|
let nohover = getContext<boolean>('collapsible-tree-nohover');
|
||||||
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
|
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
|
||||||
let parentId = getContext<string>('collapsible-tree-parent-id');
|
let parentId = getContext<string>('collapsible-tree-parent-id');
|
||||||
|
|
||||||
let fullId = `${parentId}.${id}`;
|
let fullId = `${parentId}.${id}`;
|
||||||
setContext('collapsible-tree-parent-id', fullId);
|
setContext('collapsible-tree-parent-id', fullId);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!get(open).hasOwnProperty(fullId)) {
|
if (!get(open).hasOwnProperty(fullId)) {
|
||||||
open.update((value) => {
|
open.update((value) => {
|
||||||
value[fullId] = defaultState === 'open';
|
value[fullId] = defaultState === 'open';
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function openNode() {
|
export function openNode() {
|
||||||
open.update((value) => {
|
open.update((value) => {
|
||||||
value[fullId] = true;
|
value[fullId] = true;
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapsible.Root bind:open={$open[fullId]} class={$$props.class ?? ''}>
|
<Collapsible.Root bind:open={$open[fullId]} class={$$props.class ?? ''}>
|
||||||
{#if slotInsideTrigger}
|
{#if slotInsideTrigger}
|
||||||
<Collapsible.Trigger class="w-full">
|
<Collapsible.Trigger class="w-full">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full flex flex-row {side === 'right'
|
class="w-full flex flex-row {side === 'right'
|
||||||
? 'justify-between'
|
? 'justify-between'
|
||||||
: 'justify-start'} py-0 px-1 h-fit {nohover
|
: 'justify-start'} py-0 px-1 h-fit {nohover
|
||||||
? 'hover:bg-background'
|
? 'hover:bg-background'
|
||||||
: ''} pointer-events-none"
|
: ''} pointer-events-none"
|
||||||
>
|
>
|
||||||
{#if side === 'left'}
|
{#if side === 'left'}
|
||||||
{#if $open[fullId]}
|
{#if $open[fullId]}
|
||||||
<ChevronDown size="16" class="shrink-0" />
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight size="16" class="shrink-0" />
|
<ChevronRight size="16" class="shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
{#if side === 'right'}
|
{#if side === 'right'}
|
||||||
{#if $open[fullId]}
|
{#if $open[fullId]}
|
||||||
<ChevronDown size="16" class="shrink-0" />
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronLeft size="16" class="shrink-0" />
|
<ChevronLeft size="16" class="shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full flex flex-row {side === 'right'
|
class="w-full flex flex-row {side === 'right'
|
||||||
? 'justify-between'
|
? 'justify-between'
|
||||||
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}"
|
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}"
|
||||||
>
|
>
|
||||||
{#if side === 'left'}
|
{#if side === 'left'}
|
||||||
<Collapsible.Trigger>
|
<Collapsible.Trigger>
|
||||||
{#if $open[fullId]}
|
{#if $open[fullId]}
|
||||||
<ChevronDown size="16" class="shrink-0" />
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight size="16" class="shrink-0" />
|
<ChevronRight size="16" class="shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
{/if}
|
{/if}
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
{#if side === 'right'}
|
{#if side === 'right'}
|
||||||
<Collapsible.Trigger>
|
<Collapsible.Trigger>
|
||||||
{#if $open[fullId]}
|
{#if $open[fullId]}
|
||||||
<ChevronDown size="16" class="shrink-0" />
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronLeft size="16" class="shrink-0" />
|
<ChevronLeft size="16" class="shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Collapsible.Content class="ml-2">
|
<Collapsible.Content class="ml-2">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export { default as CollapsibleTree } from './CollapsibleTree.svelte';
|
export { default as CollapsibleTree } from './CollapsibleTree.svelte';
|
||||||
export { default as CollapsibleTreeNode } from './CollapsibleTreeNode.svelte';
|
export { default as CollapsibleTreeNode } from './CollapsibleTreeNode.svelte';
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from './CustomControl';
|
import CustomControl from './CustomControl';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
|
|
||||||
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
|
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let control: CustomControl | undefined = undefined;
|
let control: CustomControl | undefined = undefined;
|
||||||
|
|
||||||
$: if ($map && container) {
|
$: if ($map && container) {
|
||||||
if (position.includes('right')) container.classList.add('float-right');
|
if (position.includes('right')) container.classList.add('float-right');
|
||||||
else container.classList.add('float-left');
|
else container.classList.add('float-left');
|
||||||
container.classList.remove('hidden');
|
container.classList.remove('hidden');
|
||||||
if (control === undefined) {
|
if (control === undefined) {
|
||||||
control = new CustomControl(container);
|
control = new CustomControl(container);
|
||||||
}
|
}
|
||||||
$map.addControl(control, position);
|
$map.addControl(control, position);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="{$$props.class ||
|
class="{$$props.class ||
|
||||||
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
|
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,4 +17,4 @@ export default class CustomControl implements IControl {
|
|||||||
this._container?.parentNode?.removeChild(this._container);
|
this._container?.parentNode?.removeChild(this._container);
|
||||||
this._map = undefined;
|
this._map = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,82 +1,82 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let module;
|
export let module;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="markdown flex flex-col gap-3">
|
<div class="markdown flex flex-col gap-3">
|
||||||
<svelte:component this={module} />
|
<svelte:component this={module} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:global(.markdown) {
|
:global(.markdown) {
|
||||||
@apply text-muted-foreground;
|
@apply text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown h1) {
|
:global(.markdown h1) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply text-3xl;
|
@apply text-3xl;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply mb-3 pt-6;
|
@apply mb-3 pt-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown h2) {
|
:global(.markdown h2) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply text-2xl;
|
@apply text-2xl;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply pt-3;
|
@apply pt-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown h3) {
|
:global(.markdown h3) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply text-lg;
|
@apply text-lg;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply pt-1.5;
|
@apply pt-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown p > button, .markdown li > button) {
|
:global(.markdown p > button, .markdown li > button) {
|
||||||
@apply border;
|
@apply border;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
@apply px-1;
|
@apply px-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown > a) {
|
:global(.markdown > a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown p > a) {
|
:global(.markdown p > a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown li > a) {
|
:global(.markdown li > a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown kbd) {
|
:global(.markdown kbd) {
|
||||||
@apply p-1;
|
@apply p-1;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
@apply border;
|
@apply border;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown ul) {
|
:global(.markdown ul) {
|
||||||
@apply list-disc;
|
@apply list-disc;
|
||||||
@apply pl-4;
|
@apply pl-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown ol) {
|
:global(.markdown ol) {
|
||||||
@apply list-decimal;
|
@apply list-decimal;
|
||||||
@apply pl-4;
|
@apply pl-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown li) {
|
:global(.markdown li) {
|
||||||
@apply mt-1;
|
@apply mt-1;
|
||||||
@apply first:mt-0;
|
@apply first:mt-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown hr) {
|
:global(.markdown hr) {
|
||||||
@apply my-5;
|
@apply my-5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,25 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
|
export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
|
||||||
export let alt: string;
|
export let alt: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center py-6 w-full">
|
<div class="flex flex-col items-center py-6 w-full">
|
||||||
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
|
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
|
||||||
{#if src === 'getting-started/interface'}
|
{#if src === 'getting-started/interface'}
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src="/src/lib/assets/img/docs/getting-started/interface.png"
|
src="/src/lib/assets/img/docs/getting-started/interface.png"
|
||||||
{alt}
|
{alt}
|
||||||
class="w-full max-w-3xl"
|
class="w-full max-w-3xl"
|
||||||
/>
|
/>
|
||||||
{:else if src === 'tools/routing'}
|
{:else if src === 'tools/routing'}
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src="/src/lib/assets/img/docs/tools/routing.png"
|
src="/src/lib/assets/img/docs/tools/routing.png"
|
||||||
{alt}
|
{alt}
|
||||||
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
|
||||||
{/if}
|
src="/src/lib/assets/img/docs/tools/split.png"
|
||||||
</div>
|
{alt}
|
||||||
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
class="w-full max-w-3xl"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
|
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
|
||||||
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
|
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
|
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
|
||||||
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
|
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src={waymarkedMap}
|
src={waymarkedMap}
|
||||||
alt="Waymarked Trails map screenshot."
|
alt="Waymarked Trails map screenshot."
|
||||||
class="absolute opacity-0 hover:opacity-100 transition-opacity duration-200"
|
class="absolute opacity-0 hover:opacity-100 transition-opacity duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let type: 'note' | 'warning' = 'note';
|
export let type: 'note' | 'warning' = 'note';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-secondary border-l-8 {type === 'note'
|
class="bg-secondary border-l-8 {type === 'note'
|
||||||
? 'border-link'
|
? 'border-link'
|
||||||
: 'border-destructive'} p-2 text-sm rounded-md"
|
: 'border-destructive'} p-2 text-sm rounded-md"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(a) {
|
div :global(a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -96,4 +121,4 @@ export function getNextGuide(currentGuide: string): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,267 +1,271 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
|
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
|
||||||
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
|
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
|
||||||
import FileList from '$lib/components/file-list/FileList.svelte';
|
import FileList from '$lib/components/file-list/FileList.svelte';
|
||||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||||
import Map from '$lib/components/Map.svelte';
|
import Map from '$lib/components/Map.svelte';
|
||||||
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
||||||
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
|
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
|
||||||
import {
|
import {
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
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';
|
||||||
import { readable } from 'svelte/store';
|
import { readable } from 'svelte/store';
|
||||||
import type { GPXFile } from 'gpx';
|
import type { GPXFile } from 'gpx';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListFileItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem } from '$lib/components/file-list/FileList';
|
||||||
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';
|
||||||
|
|
||||||
$embedding = true;
|
$embedding = true;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
distanceUnits,
|
distanceUnits,
|
||||||
velocityUnits,
|
velocityUnits,
|
||||||
temperatureUnits,
|
temperatureUnits,
|
||||||
fileOrder,
|
fileOrder,
|
||||||
distanceMarkers,
|
distanceMarkers,
|
||||||
directionMarkers
|
directionMarkers,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
export let useHash = true;
|
export let useHash = true;
|
||||||
export let options: EmbeddingOptions;
|
export let options: EmbeddingOptions;
|
||||||
export let hash: string;
|
export let hash: string;
|
||||||
|
|
||||||
let prevSettings = {
|
let prevSettings = {
|
||||||
distanceMarkers: false,
|
distanceMarkers: false,
|
||||||
directionMarkers: false,
|
directionMarkers: false,
|
||||||
distanceUnits: 'metric',
|
distanceUnits: 'metric',
|
||||||
velocityUnits: 'speed',
|
velocityUnits: 'speed',
|
||||||
temperatureUnits: 'celsius',
|
temperatureUnits: 'celsius',
|
||||||
theme: 'system'
|
theme: 'system',
|
||||||
};
|
};
|
||||||
|
|
||||||
function applyOptions() {
|
function applyOptions() {
|
||||||
fileObservers.update(($fileObservers) => {
|
fileObservers.update(($fileObservers) => {
|
||||||
$fileObservers.clear();
|
$fileObservers.clear();
|
||||||
return $fileObservers;
|
return $fileObservers;
|
||||||
});
|
});
|
||||||
|
|
||||||
let downloads: Promise<GPXFile | null>[] = [];
|
let downloads: Promise<GPXFile | null>[] = [];
|
||||||
getFilesFromEmbeddingOptions(options).forEach((url) => {
|
getFilesFromEmbeddingOptions(options).forEach((url) => {
|
||||||
downloads.push(
|
downloads.push(
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((response) => response.blob())
|
.then((response) => response.blob())
|
||||||
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
||||||
.then(loadFile)
|
.then(loadFile)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(downloads).then((files) => {
|
Promise.all(downloads).then((files) => {
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
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) => {
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = `gpx-${index}-embed`;
|
let id = `gpx-${index}-embed`;
|
||||||
file._data.id = id;
|
file._data.id = id;
|
||||||
let statistics = new GPXStatisticsTree(file);
|
let statistics = new GPXStatisticsTree(file);
|
||||||
|
|
||||||
$fileObservers.set(
|
$fileObservers.set(
|
||||||
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);
|
||||||
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
|
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
|
||||||
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
|
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
|
||||||
});
|
});
|
||||||
|
|
||||||
return $fileObservers;
|
return $fileObservers;
|
||||||
});
|
});
|
||||||
|
|
||||||
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
|
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
|
||||||
|
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
$selection.toggle(new ListFileItem(id));
|
$selection.toggle(new ListFileItem(id));
|
||||||
});
|
});
|
||||||
return $selection;
|
return $selection;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hash.length === 0) {
|
if (hash.length === 0) {
|
||||||
map.subscribe(($map) => {
|
map.subscribe(($map) => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.fitBounds(
|
$map.fitBounds(
|
||||||
[
|
[
|
||||||
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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
|
if (
|
||||||
$currentBasemap = options.basemap;
|
options.basemap !== $currentBasemap &&
|
||||||
}
|
allowedEmbeddingBasemaps.includes(options.basemap)
|
||||||
|
) {
|
||||||
|
$currentBasemap = options.basemap;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.distanceMarkers !== $distanceMarkers) {
|
if (options.distanceMarkers !== $distanceMarkers) {
|
||||||
$distanceMarkers = options.distanceMarkers;
|
$distanceMarkers = options.distanceMarkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.directionMarkers !== $directionMarkers) {
|
if (options.directionMarkers !== $directionMarkers) {
|
||||||
$directionMarkers = options.directionMarkers;
|
$directionMarkers = options.directionMarkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.distanceUnits !== $distanceUnits) {
|
if (options.distanceUnits !== $distanceUnits) {
|
||||||
$distanceUnits = options.distanceUnits;
|
$distanceUnits = options.distanceUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.velocityUnits !== $velocityUnits) {
|
if (options.velocityUnits !== $velocityUnits) {
|
||||||
$velocityUnits = options.velocityUnits;
|
$velocityUnits = options.velocityUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.temperatureUnits !== $temperatureUnits) {
|
if (options.temperatureUnits !== $temperatureUnits) {
|
||||||
$temperatureUnits = options.temperatureUnits;
|
$temperatureUnits = options.temperatureUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.theme !== $mode) {
|
if (options.theme !== $mode) {
|
||||||
setMode(options.theme);
|
setMode(options.theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
prevSettings.distanceMarkers = $distanceMarkers;
|
prevSettings.distanceMarkers = $distanceMarkers;
|
||||||
prevSettings.directionMarkers = $directionMarkers;
|
prevSettings.directionMarkers = $directionMarkers;
|
||||||
prevSettings.distanceUnits = $distanceUnits;
|
prevSettings.distanceUnits = $distanceUnits;
|
||||||
prevSettings.velocityUnits = $velocityUnits;
|
prevSettings.velocityUnits = $velocityUnits;
|
||||||
prevSettings.temperatureUnits = $temperatureUnits;
|
prevSettings.temperatureUnits = $temperatureUnits;
|
||||||
prevSettings.theme = $mode ?? 'system';
|
prevSettings.theme = $mode ?? 'system';
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (browser && options) {
|
$: if (browser && options) {
|
||||||
applyOptions();
|
applyOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($fileOrder) {
|
$: if ($fileOrder) {
|
||||||
updateGPXData();
|
updateGPXData();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($distanceMarkers !== prevSettings.distanceMarkers) {
|
if ($distanceMarkers !== prevSettings.distanceMarkers) {
|
||||||
$distanceMarkers = prevSettings.distanceMarkers;
|
$distanceMarkers = prevSettings.distanceMarkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($directionMarkers !== prevSettings.directionMarkers) {
|
if ($directionMarkers !== prevSettings.directionMarkers) {
|
||||||
$directionMarkers = prevSettings.directionMarkers;
|
$directionMarkers = prevSettings.directionMarkers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($distanceUnits !== prevSettings.distanceUnits) {
|
if ($distanceUnits !== prevSettings.distanceUnits) {
|
||||||
$distanceUnits = prevSettings.distanceUnits;
|
$distanceUnits = prevSettings.distanceUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($velocityUnits !== prevSettings.velocityUnits) {
|
if ($velocityUnits !== prevSettings.velocityUnits) {
|
||||||
$velocityUnits = prevSettings.velocityUnits;
|
$velocityUnits = prevSettings.velocityUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($temperatureUnits !== prevSettings.temperatureUnits) {
|
if ($temperatureUnits !== prevSettings.temperatureUnits) {
|
||||||
$temperatureUnits = prevSettings.temperatureUnits;
|
$temperatureUnits = prevSettings.temperatureUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($mode !== prevSettings.theme) {
|
if ($mode !== prevSettings.theme) {
|
||||||
setMode(prevSettings.theme);
|
setMode(prevSettings.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
$fileObservers.clear();
|
$fileObservers.clear();
|
||||||
$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
|
$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Map
|
<Map
|
||||||
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
|
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
|
||||||
accessToken={options.token}
|
accessToken={options.token}
|
||||||
geocoder={false}
|
geocoder={false}
|
||||||
geolocate={false}
|
geolocate={false}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
/>
|
/>
|
||||||
<OpenIn bind:files={options.files} bind:ids={options.ids} />
|
<OpenIn bind:files={options.files} bind:ids={options.ids} />
|
||||||
<LayerControl />
|
<LayerControl />
|
||||||
<GPXLayers />
|
<GPXLayers />
|
||||||
{#if $fileObservers.size > 1}
|
{#if $fileObservers.size > 1}
|
||||||
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
||||||
<FileList orientation="horizontal" />
|
<FileList orientation="horizontal" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
||||||
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
||||||
>
|
>
|
||||||
<GPXStatistics
|
<GPXStatistics
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
panelSize={options.elevation.height}
|
panelSize={options.elevation.height}
|
||||||
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||||
/>
|
/>
|
||||||
{#if options.elevation.show}
|
{#if options.elevation.show}
|
||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
additionalDatasets={[
|
additionalDatasets={[
|
||||||
options.elevation.speed ? 'speed' : null,
|
options.elevation.speed ? 'speed' : null,
|
||||||
options.elevation.hr ? 'hr' : null,
|
options.elevation.hr ? 'hr' : null,
|
||||||
options.elevation.cad ? 'cad' : null,
|
options.elevation.cad ? 'cad' : null,
|
||||||
options.elevation.temp ? 'temp' : null,
|
options.elevation.temp ? 'temp' : null,
|
||||||
options.elevation.power ? 'power' : null
|
options.elevation.power ? 'power' : null,
|
||||||
].filter((dataset) => dataset !== null)}
|
].filter((dataset) => dataset !== null)}
|
||||||
elevationFill={options.elevation.fill}
|
elevationFill={options.elevation.fill}
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -1,328 +1,339 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
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 {
|
import {
|
||||||
Zap,
|
Zap,
|
||||||
HeartPulse,
|
HeartPulse,
|
||||||
Orbit,
|
Orbit,
|
||||||
Thermometer,
|
Thermometer,
|
||||||
SquareActivity,
|
SquareActivity,
|
||||||
Coins,
|
Coins,
|
||||||
Milestone,
|
Milestone,
|
||||||
Video
|
Video,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
allowedEmbeddingBasemaps,
|
allowedEmbeddingBasemaps,
|
||||||
getCleanedEmbeddingOptions,
|
getCleanedEmbeddingOptions,
|
||||||
getDefaultEmbeddingOptions
|
getDefaultEmbeddingOptions,
|
||||||
} from './Embedding';
|
} from './Embedding';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import Embedding from './Embedding.svelte';
|
import Embedding from './Embedding.svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
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];
|
||||||
$: {
|
$: {
|
||||||
let urls = files.split(',');
|
let urls = files.split(',');
|
||||||
urls = urls.filter((url) => url.length > 0);
|
urls = urls.filter((url) => url.length > 0);
|
||||||
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
||||||
options.files = urls;
|
options.files = urls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let driveIds = '';
|
let driveIds = '';
|
||||||
$: {
|
$: {
|
||||||
let ids = driveIds.split(',');
|
let ids = driveIds.split(',');
|
||||||
ids = ids.filter((id) => id.length > 0);
|
ids = ids.filter((id) => id.length > 0);
|
||||||
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
|
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
|
||||||
options.ids = ids;
|
options.ids = ids;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let manualCamera = false;
|
let manualCamera = false;
|
||||||
|
|
||||||
let zoom = '0';
|
let zoom = '0';
|
||||||
let lat = '0';
|
let lat = '0';
|
||||||
let lon = '0';
|
let lon = '0';
|
||||||
let bearing = '0';
|
let bearing = '0';
|
||||||
let pitch = '0';
|
let pitch = '0';
|
||||||
|
|
||||||
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
|
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
|
||||||
|
|
||||||
$: iframeOptions =
|
$: iframeOptions =
|
||||||
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||||
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
|
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
|
||||||
: options;
|
: options;
|
||||||
|
|
||||||
async function resizeMap() {
|
async function resizeMap() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
await tick();
|
await tick();
|
||||||
$map.resize();
|
$map.resize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (options.elevation.height || options.elevation.show) {
|
$: if (options.elevation.height || options.elevation.show) {
|
||||||
resizeMap();
|
resizeMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCamera() {
|
function updateCamera() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
let center = $map.getCenter();
|
let center = $map.getCenter();
|
||||||
lat = center.lat.toFixed(4);
|
lat = center.lat.toFixed(4);
|
||||||
lon = center.lng.toFixed(4);
|
lon = center.lng.toFixed(4);
|
||||||
zoom = $map.getZoom().toFixed(2);
|
zoom = $map.getZoom().toFixed(2);
|
||||||
bearing = $map.getBearing().toFixed(1);
|
bearing = $map.getBearing().toFixed(1);
|
||||||
pitch = $map.getPitch().toFixed(0);
|
pitch = $map.getPitch().toFixed(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
$map.on('moveend', updateCamera);
|
$map.on('moveend', updateCamera);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root id="embedding-playground">
|
<Card.Root id="embedding-playground">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{$_('embedding.title')}</Card.Title>
|
<Card.Title>{$_('embedding.title')}</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<fieldset class="flex flex-col gap-3">
|
<fieldset class="flex flex-col gap-3">
|
||||||
<Label for="token">{$_('embedding.mapbox_token')}</Label>
|
<Label for="token">{$_('embedding.mapbox_token')}</Label>
|
||||||
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
||||||
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
|
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
|
||||||
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
||||||
<Label for="drive_ids">{$_('embedding.drive_ids')}</Label>
|
<Label for="drive_ids">{$_('embedding.drive_ids')}</Label>
|
||||||
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
||||||
<Label for="basemap">{$_('embedding.basemap')}</Label>
|
<Label for="basemap">{$_('embedding.basemap')}</Label>
|
||||||
<Select.Root
|
<Select.Root
|
||||||
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
|
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
|
||||||
onSelectedChange={(selected) => {
|
onSelectedChange={(selected) => {
|
||||||
if (selected?.value) {
|
if (selected?.value) {
|
||||||
options.basemap = selected?.value;
|
options.basemap = selected?.value;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select.Trigger id="basemap" class="w-full h-8">
|
<Select.Trigger id="basemap" class="w-full h-8">
|
||||||
<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">
|
||||||
{#each allowedEmbeddingBasemaps as basemap}
|
{#each allowedEmbeddingBasemaps as basemap}
|
||||||
<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item>
|
<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Label for="profile">{$_('menu.elevation_profile')}</Label>
|
<Label for="profile">{$_('menu.elevation_profile')}</Label>
|
||||||
<Checkbox id="profile" bind:checked={options.elevation.show} />
|
<Checkbox id="profile" bind:checked={options.elevation.show} />
|
||||||
</div>
|
</div>
|
||||||
{#if options.elevation.show}
|
{#if options.elevation.show}
|
||||||
<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
|
||||||
</Label>
|
type="number"
|
||||||
<div class="flex flex-row items-center gap-2">
|
bind:value={options.elevation.height}
|
||||||
<span class="shrink-0">
|
class="h-8 w-20"
|
||||||
{$_('embedding.fill_by')}
|
/>
|
||||||
</span>
|
</Label>
|
||||||
<Select.Root
|
<div class="flex flex-row items-center gap-2">
|
||||||
selected={{ value: 'none', label: $_('embedding.none') }}
|
<span class="shrink-0">
|
||||||
onSelectedChange={(selected) => {
|
{$_('embedding.fill_by')}
|
||||||
let value = selected?.value;
|
</span>
|
||||||
if (value === 'none') {
|
<Select.Root
|
||||||
options.elevation.fill = undefined;
|
selected={{ value: 'none', label: $_('embedding.none') }}
|
||||||
} else if (value === 'slope' || value === 'surface' || value === 'highway') {
|
onSelectedChange={(selected) => {
|
||||||
options.elevation.fill = value;
|
let value = selected?.value;
|
||||||
}
|
if (value === 'none') {
|
||||||
}}
|
options.elevation.fill = undefined;
|
||||||
>
|
} else if (
|
||||||
<Select.Trigger class="grow h-8">
|
value === 'slope' ||
|
||||||
<Select.Value />
|
value === 'surface' ||
|
||||||
</Select.Trigger>
|
value === 'highway'
|
||||||
<Select.Content>
|
) {
|
||||||
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
options.elevation.fill = value;
|
||||||
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
|
}
|
||||||
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item>
|
}}
|
||||||
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
>
|
||||||
</Select.Content>
|
<Select.Trigger class="grow h-8">
|
||||||
</Select.Root>
|
<Select.Value />
|
||||||
</div>
|
</Select.Trigger>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<Select.Content>
|
||||||
<Checkbox id="controls" bind:checked={options.elevation.controls} />
|
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
||||||
<Label for="controls">{$_('embedding.show_controls')}</Label>
|
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item
|
||||||
</div>
|
>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item
|
||||||
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
|
>
|
||||||
<Label for="show-speed" class="flex flex-row items-center gap-1">
|
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
||||||
<Zap size="16" />
|
</Select.Content>
|
||||||
{$_('quantities.speed')}
|
</Select.Root>
|
||||||
</Label>
|
</div>
|
||||||
</div>
|
<div class="flex flex-row items-center gap-2">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<Checkbox id="controls" bind:checked={options.elevation.controls} />
|
||||||
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
|
<Label for="controls">{$_('embedding.show_controls')}</Label>
|
||||||
<Label for="show-hr" class="flex flex-row items-center gap-1">
|
</div>
|
||||||
<HeartPulse size="16" />
|
<div class="flex flex-row items-center gap-2">
|
||||||
{$_('quantities.heartrate')}
|
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
|
||||||
</Label>
|
<Label for="show-speed" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<Zap size="16" />
|
||||||
<div class="flex flex-row items-center gap-2">
|
{$_('quantities.speed')}
|
||||||
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
|
</Label>
|
||||||
<Label for="show-cad" class="flex flex-row items-center gap-1">
|
</div>
|
||||||
<Orbit size="16" />
|
<div class="flex flex-row items-center gap-2">
|
||||||
{$_('quantities.cadence')}
|
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
|
||||||
</Label>
|
<Label for="show-hr" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<HeartPulse size="16" />
|
||||||
<div class="flex flex-row items-center gap-2">
|
{$_('quantities.heartrate')}
|
||||||
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
|
</Label>
|
||||||
<Label for="show-temp" class="flex flex-row items-center gap-1">
|
</div>
|
||||||
<Thermometer size="16" />
|
<div class="flex flex-row items-center gap-2">
|
||||||
{$_('quantities.temperature')}
|
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
|
||||||
</Label>
|
<Label for="show-cad" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<Orbit size="16" />
|
||||||
<div class="flex flex-row items-center gap-2">
|
{$_('quantities.cadence')}
|
||||||
<Checkbox id="show-power" bind:checked={options.elevation.power} />
|
</Label>
|
||||||
<Label for="show-power" class="flex flex-row items-center gap-1">
|
</div>
|
||||||
<SquareActivity size="16" />
|
<div class="flex flex-row items-center gap-2">
|
||||||
{$_('quantities.power')}
|
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
|
||||||
</Label>
|
<Label for="show-temp" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<Thermometer size="16" />
|
||||||
</div>
|
{$_('quantities.temperature')}
|
||||||
{/if}
|
</Label>
|
||||||
<div class="flex flex-row items-center gap-2">
|
</div>
|
||||||
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Label for="distance-markers" class="flex flex-row items-center gap-1">
|
<Checkbox id="show-power" bind:checked={options.elevation.power} />
|
||||||
<Coins size="16" />
|
<Label for="show-power" class="flex flex-row items-center gap-1">
|
||||||
{$_('menu.distance_markers')}
|
<SquareActivity size="16" />
|
||||||
</Label>
|
{$_('quantities.power')}
|
||||||
</div>
|
</Label>
|
||||||
<div class="flex flex-row items-center gap-2">
|
</div>
|
||||||
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
|
</div>
|
||||||
<Label for="direction-markers" class="flex flex-row items-center gap-1">
|
{/if}
|
||||||
<Milestone size="16" />
|
<div class="flex flex-row items-center gap-2">
|
||||||
{$_('menu.direction_markers')}
|
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
|
||||||
</Label>
|
<Label for="distance-markers" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
<Coins size="16" />
|
||||||
<div class="flex flex-row flex-wrap justify-between gap-3">
|
{$_('menu.distance_markers')}
|
||||||
<Label class="flex flex-col items-start gap-2">
|
</Label>
|
||||||
{$_('menu.distance_units')}
|
</div>
|
||||||
<RadioGroup.Root bind:value={options.distanceUnits}>
|
<div class="flex flex-row items-center gap-2">
|
||||||
<div class="flex items-center space-x-2">
|
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
|
||||||
<RadioGroup.Item value="metric" id="metric" />
|
<Label for="direction-markers" class="flex flex-row items-center gap-1">
|
||||||
<Label for="metric">{$_('menu.metric')}</Label>
|
<Milestone size="16" />
|
||||||
</div>
|
{$_('menu.direction_markers')}
|
||||||
<div class="flex items-center space-x-2">
|
</Label>
|
||||||
<RadioGroup.Item value="imperial" id="imperial" />
|
</div>
|
||||||
<Label for="imperial">{$_('menu.imperial')}</Label>
|
<div class="flex flex-row flex-wrap justify-between gap-3">
|
||||||
</div>
|
<Label class="flex flex-col items-start gap-2">
|
||||||
<div class="flex items-center space-x-2">
|
{$_('menu.distance_units')}
|
||||||
<RadioGroup.Item value="nautical" id="nautical" />
|
<RadioGroup.Root bind:value={options.distanceUnits}>
|
||||||
<Label for="nautical">{$_('menu.nautical')}</Label>
|
<div class="flex items-center space-x-2">
|
||||||
</div>
|
<RadioGroup.Item value="metric" id="metric" />
|
||||||
</RadioGroup.Root>
|
<Label for="metric">{$_('menu.metric')}</Label>
|
||||||
</Label>
|
</div>
|
||||||
<Label class="flex flex-col items-start gap-2">
|
<div class="flex items-center space-x-2">
|
||||||
{$_('menu.velocity_units')}
|
<RadioGroup.Item value="imperial" id="imperial" />
|
||||||
<RadioGroup.Root bind:value={options.velocityUnits}>
|
<Label for="imperial">{$_('menu.imperial')}</Label>
|
||||||
<div class="flex items-center space-x-2">
|
</div>
|
||||||
<RadioGroup.Item value="speed" id="speed" />
|
<div class="flex items-center space-x-2">
|
||||||
<Label for="speed">{$_('quantities.speed')}</Label>
|
<RadioGroup.Item value="nautical" id="nautical" />
|
||||||
</div>
|
<Label for="nautical">{$_('menu.nautical')}</Label>
|
||||||
<div class="flex items-center space-x-2">
|
</div>
|
||||||
<RadioGroup.Item value="pace" id="pace" />
|
</RadioGroup.Root>
|
||||||
<Label for="pace">{$_('quantities.pace')}</Label>
|
</Label>
|
||||||
</div>
|
<Label class="flex flex-col items-start gap-2">
|
||||||
</RadioGroup.Root>
|
{$_('menu.velocity_units')}
|
||||||
</Label>
|
<RadioGroup.Root bind:value={options.velocityUnits}>
|
||||||
<Label class="flex flex-col items-start gap-2">
|
<div class="flex items-center space-x-2">
|
||||||
{$_('menu.temperature_units')}
|
<RadioGroup.Item value="speed" id="speed" />
|
||||||
<RadioGroup.Root bind:value={options.temperatureUnits}>
|
<Label for="speed">{$_('quantities.speed')}</Label>
|
||||||
<div class="flex items-center space-x-2">
|
</div>
|
||||||
<RadioGroup.Item value="celsius" id="celsius" />
|
<div class="flex items-center space-x-2">
|
||||||
<Label for="celsius">{$_('menu.celsius')}</Label>
|
<RadioGroup.Item value="pace" id="pace" />
|
||||||
</div>
|
<Label for="pace">{$_('quantities.pace')}</Label>
|
||||||
<div class="flex items-center space-x-2">
|
</div>
|
||||||
<RadioGroup.Item value="fahrenheit" id="fahrenheit" />
|
</RadioGroup.Root>
|
||||||
<Label for="fahrenheit">{$_('menu.fahrenheit')}</Label>
|
</Label>
|
||||||
</div>
|
<Label class="flex flex-col items-start gap-2">
|
||||||
</RadioGroup.Root>
|
{$_('menu.temperature_units')}
|
||||||
</Label>
|
<RadioGroup.Root bind:value={options.temperatureUnits}>
|
||||||
</div>
|
<div class="flex items-center space-x-2">
|
||||||
<Label class="flex flex-col items-start gap-2">
|
<RadioGroup.Item value="celsius" id="celsius" />
|
||||||
{$_('menu.mode')}
|
<Label for="celsius">{$_('menu.celsius')}</Label>
|
||||||
<RadioGroup.Root bind:value={options.theme} class="flex flex-row">
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<RadioGroup.Item value="system" id="system" />
|
<RadioGroup.Item value="fahrenheit" id="fahrenheit" />
|
||||||
<Label for="system">{$_('menu.system')}</Label>
|
<Label for="fahrenheit">{$_('menu.fahrenheit')}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
</RadioGroup.Root>
|
||||||
<RadioGroup.Item value="light" id="light" />
|
</Label>
|
||||||
<Label for="light">{$_('menu.light')}</Label>
|
</div>
|
||||||
</div>
|
<Label class="flex flex-col items-start gap-2">
|
||||||
<div class="flex items-center space-x-2">
|
{$_('menu.mode')}
|
||||||
<RadioGroup.Item value="dark" id="dark" />
|
<RadioGroup.Root bind:value={options.theme} class="flex flex-row">
|
||||||
<Label for="dark">{$_('menu.dark')}</Label>
|
<div class="flex items-center space-x-2">
|
||||||
</div>
|
<RadioGroup.Item value="system" id="system" />
|
||||||
</RadioGroup.Root>
|
<Label for="system">{$_('menu.system')}</Label>
|
||||||
</Label>
|
</div>
|
||||||
<div class="flex flex-col gap-3 p-3 border rounded-md">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<RadioGroup.Item value="light" id="light" />
|
||||||
<Checkbox id="manual-camera" bind:checked={manualCamera} />
|
<Label for="light">{$_('menu.light')}</Label>
|
||||||
<Label for="manual-camera" class="flex flex-row items-center gap-1">
|
</div>
|
||||||
<Video size="16" />
|
<div class="flex items-center space-x-2">
|
||||||
{$_('embedding.manual_camera')}
|
<RadioGroup.Item value="dark" id="dark" />
|
||||||
</Label>
|
<Label for="dark">{$_('menu.dark')}</Label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
</RadioGroup.Root>
|
||||||
{$_('embedding.manual_camera_description')}
|
</Label>
|
||||||
</p>
|
<div class="flex flex-col gap-3 p-3 border rounded-md">
|
||||||
<div class="flex flex-row flex-wrap items-center gap-6">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<Label class="flex flex-col gap-1">
|
<Checkbox id="manual-camera" bind:checked={manualCamera} />
|
||||||
<span>{$_('embedding.latitude')}</span>
|
<Label for="manual-camera" class="flex flex-row items-center gap-1">
|
||||||
<span>{lat}</span>
|
<Video size="16" />
|
||||||
</Label>
|
{$_('embedding.manual_camera')}
|
||||||
<Label class="flex flex-col gap-1">
|
</Label>
|
||||||
<span>{$_('embedding.longitude')}</span>
|
</div>
|
||||||
<span>{lon}</span>
|
<p class="text-sm text-muted-foreground">
|
||||||
</Label>
|
{$_('embedding.manual_camera_description')}
|
||||||
<Label class="flex flex-col gap-1">
|
</p>
|
||||||
<span>{$_('embedding.zoom')}</span>
|
<div class="flex flex-row flex-wrap items-center gap-6">
|
||||||
<span>{zoom}</span>
|
<Label class="flex flex-col gap-1">
|
||||||
</Label>
|
<span>{$_('embedding.latitude')}</span>
|
||||||
<Label class="flex flex-col gap-1">
|
<span>{lat}</span>
|
||||||
<span>{$_('embedding.bearing')}</span>
|
</Label>
|
||||||
<span>{bearing}</span>
|
<Label class="flex flex-col gap-1">
|
||||||
</Label>
|
<span>{$_('embedding.longitude')}</span>
|
||||||
<Label class="flex flex-col gap-1">
|
<span>{lon}</span>
|
||||||
<span>{$_('embedding.pitch')}</span>
|
</Label>
|
||||||
<span>{pitch}</span>
|
<Label class="flex flex-col gap-1">
|
||||||
</Label>
|
<span>{$_('embedding.zoom')}</span>
|
||||||
</div>
|
<span>{zoom}</span>
|
||||||
</div>
|
</Label>
|
||||||
<Label>
|
<Label class="flex flex-col gap-1">
|
||||||
{$_('embedding.preview')}
|
<span>{$_('embedding.bearing')}</span>
|
||||||
</Label>
|
<span>{bearing}</span>
|
||||||
<div class="relative h-[600px]">
|
</Label>
|
||||||
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
|
<Label class="flex flex-col gap-1">
|
||||||
</div>
|
<span>{$_('embedding.pitch')}</span>
|
||||||
<Label>
|
<span>{pitch}</span>
|
||||||
{$_('embedding.code')}
|
</Label>
|
||||||
</Label>
|
</div>
|
||||||
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
</div>
|
||||||
|
<Label>
|
||||||
|
{$_('embedding.preview')}
|
||||||
|
</Label>
|
||||||
|
<div class="relative h-[600px]">
|
||||||
|
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
|
||||||
|
</div>
|
||||||
|
<Label>
|
||||||
|
{$_('embedding.code')}
|
||||||
|
</Label>
|
||||||
|
<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>
|
||||||
</pre>
|
</pre>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@ -1,89 +1,89 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
import { fileObservers, settings } from '$lib/db';
|
import { fileObservers, settings } from '$lib/db';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
||||||
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 'svelte-i18n';
|
||||||
import { createFile } from '$lib/stores';
|
import { createFile } from '$lib/stores';
|
||||||
|
|
||||||
export let orientation: 'vertical' | 'horizontal';
|
export let orientation: 'vertical' | 'horizontal';
|
||||||
export let recursive = false;
|
export let recursive = false;
|
||||||
|
|
||||||
setContext('orientation', orientation);
|
setContext('orientation', orientation);
|
||||||
setContext('recursive', recursive);
|
setContext('recursive', recursive);
|
||||||
|
|
||||||
const { treeFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
treeFileView.subscribe(($vertical) => {
|
treeFileView.subscribe(($vertical) => {
|
||||||
if ($vertical) {
|
if ($vertical) {
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.forEach((item) => {
|
$selection.forEach((item) => {
|
||||||
if ($selection.hasAnyChildren(item, false)) {
|
if ($selection.hasAnyChildren(item, false)) {
|
||||||
$selection.toggle(item);
|
$selection.toggle(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return $selection;
|
return $selection;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.forEach((item) => {
|
$selection.forEach((item) => {
|
||||||
if (!(item instanceof ListFileItem)) {
|
if (!(item instanceof ListFileItem)) {
|
||||||
$selection.toggle(item);
|
$selection.toggle(item);
|
||||||
$selection.set(new ListFileItem(item.getFileId()), true);
|
$selection.set(new ListFileItem(item.getFileId()), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return $selection;
|
return $selection;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
||||||
{orientation}
|
{orientation}
|
||||||
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
||||||
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex {orientation === 'vertical'
|
class="flex {orientation === 'vertical'
|
||||||
? 'flex-col py-1 pl-1 min-h-screen'
|
? 'flex-col py-1 pl-1 min-h-screen'
|
||||||
: 'flex-row'} {$$props.class ?? ''}"
|
: 'flex-row'} {$$props.class ?? ''}"
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
||||||
{#if orientation === 'vertical'}
|
{#if orientation === 'vertical'}
|
||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
<ContextMenu.Trigger class="grow" />
|
<ContextMenu.Trigger class="grow" />
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
<ContextMenu.Item on:click={createFile}>
|
<ContextMenu.Item on:click={createFile}>
|
||||||
<Plus size="16" class="mr-1" />
|
<Plus size="16" class="mr-1" />
|
||||||
{$_('menu.new_file')}
|
{$_('menu.new_file')}
|
||||||
<Shortcut key="+" ctrl={true} />
|
<Shortcut key="+" ctrl={true} />
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
|
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
|
||||||
<FileStack size="16" class="mr-1" />
|
<FileStack size="16" class="mr-1" />
|
||||||
{$_('menu.select_all')}
|
{$_('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
disabled={$copied === undefined ||
|
disabled={$copied === undefined ||
|
||||||
$copied.length === 0 ||
|
$copied.length === 0 ||
|
||||||
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
|
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
|
||||||
on:click={pasteSelection}
|
on:click={pasteSelection}
|
||||||
>
|
>
|
||||||
<ClipboardPaste size="16" class="mr-1" />
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
{$_('menu.paste')}
|
{$_('menu.paste')}
|
||||||
<Shortcut key="V" ctrl={true} />
|
<Shortcut key="V" ctrl={true} />
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@ -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,35 +436,42 @@ 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(
|
||||||
toItems.forEach((item, i) => {
|
files,
|
||||||
if (item instanceof ListFileItem) {
|
callbacks,
|
||||||
if (context[i] instanceof GPXFile) {
|
(files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
let newFile = context[i];
|
toItems.forEach((item, i) => {
|
||||||
if (remove) {
|
if (item instanceof ListFileItem) {
|
||||||
files.delete(newFile._data.id);
|
if (context[i] instanceof GPXFile) {
|
||||||
|
let newFile = context[i];
|
||||||
|
if (remove) {
|
||||||
|
files.delete(newFile._data.id);
|
||||||
|
}
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
} else if (context[i] instanceof Track) {
|
||||||
|
let newFile = newGPXFile();
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
if (context[i].name) {
|
||||||
|
newFile.metadata.name = context[i].name;
|
||||||
|
}
|
||||||
|
newFile.replaceTracks(0, 0, [context[i]]);
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
} else if (context[i] instanceof TrackSegment) {
|
||||||
|
let newFile = newGPXFile();
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
newFile.replaceTracks(0, 0, [
|
||||||
|
new Track({
|
||||||
|
trkseg: [context[i]],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
}
|
}
|
||||||
newFile._data.id = item.getFileId();
|
|
||||||
files.set(item.getFileId(), freeze(newFile));
|
|
||||||
} else if (context[i] instanceof Track) {
|
|
||||||
let newFile = newGPXFile();
|
|
||||||
newFile._data.id = item.getFileId();
|
|
||||||
if (context[i].name) {
|
|
||||||
newFile.metadata.name = context[i].name;
|
|
||||||
}
|
|
||||||
newFile.replaceTracks(0, 0, [context[i]]);
|
|
||||||
files.set(item.getFileId(), freeze(newFile));
|
|
||||||
} else if (context[i] instanceof TrackSegment) {
|
|
||||||
let newFile = newGPXFile();
|
|
||||||
newFile._data.id = item.getFileId();
|
|
||||||
newFile.replaceTracks(0, 0, [new Track({
|
|
||||||
trkseg: [context[i]]
|
|
||||||
})]);
|
|
||||||
files.set(item.getFileId(), freeze(newFile));
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
},
|
||||||
}, context);
|
context
|
||||||
|
);
|
||||||
|
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
|
|||||||
@ -1,83 +1,84 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
GPXFile,
|
GPXFile,
|
||||||
Track,
|
Track,
|
||||||
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';
|
||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||||
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||||
import { afterUpdate, getContext } from 'svelte';
|
import { afterUpdate, getContext } from 'svelte';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
ListWaypointsItem,
|
ListWaypointsItem,
|
||||||
type ListItem,
|
type ListItem,
|
||||||
type ListTrackItem
|
type ListTrackItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||||
| GPXTreeElement<AnyGPXTreeElement>
|
| GPXTreeElement<AnyGPXTreeElement>
|
||||||
| Waypoint[]
|
| Waypoint[]
|
||||||
| Waypoint;
|
| Waypoint;
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
|
|
||||||
let recursive = getContext<boolean>('recursive');
|
let recursive = getContext<boolean>('recursive');
|
||||||
|
|
||||||
let collapsible: CollapsibleTreeNode;
|
let collapsible: CollapsibleTreeNode;
|
||||||
|
|
||||||
$: label =
|
$: label =
|
||||||
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 ??
|
||||||
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
`${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
|
||||||
? $_('gpx.waypoints')
|
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||||
: '';
|
? $_('gpx.waypoints')
|
||||||
|
: '';
|
||||||
|
|
||||||
const { treeFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
function openIfSelectedChild() {
|
function openIfSelectedChild() {
|
||||||
if (collapsible && get(treeFileView) && $selection.hasAnyChildren(item, false)) {
|
if (collapsible && get(treeFileView) && $selection.hasAnyChildren(item, false)) {
|
||||||
collapsible.openNode();
|
collapsible.openNode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($selection) {
|
if ($selection) {
|
||||||
openIfSelectedChild();
|
openIfSelectedChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(openIfSelectedChild);
|
afterUpdate(openIfSelectedChild);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if node instanceof Map}
|
{#if node instanceof Map}
|
||||||
<FileListNodeContent {node} {item} />
|
<FileListNodeContent {node} {item} />
|
||||||
{:else if node instanceof TrackSegment}
|
{:else if node instanceof TrackSegment}
|
||||||
<FileListNodeLabel {node} {item} {label} />
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
{:else if node instanceof Waypoint}
|
{:else if node instanceof Waypoint}
|
||||||
<FileListNodeLabel {node} {item} {label} />
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
{:else if recursive}
|
{:else if recursive}
|
||||||
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
|
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
|
||||||
<FileListNodeLabel {node} {item} {label} slot="trigger" />
|
<FileListNodeLabel {node} {item} {label} slot="trigger" />
|
||||||
<div slot="content" class="ml-2">
|
<div slot="content" class="ml-2">
|
||||||
{#key node}
|
{#key node}
|
||||||
<FileListNodeContent {node} {item} />
|
<FileListNodeContent {node} {item} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTreeNode>
|
</CollapsibleTreeNode>
|
||||||
{:else}
|
{:else}
|
||||||
<FileListNodeLabel {node} {item} {label} />
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
ListWaypointsItem,
|
ListWaypointsItem,
|
||||||
allowedMoves,
|
allowedMoves,
|
||||||
moveItems,
|
moveItems,
|
||||||
type ListItem
|
type ListItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { isMac } from '$lib/utils';
|
import { isMac } from '$lib/utils';
|
||||||
@ -113,7 +113,7 @@
|
|||||||
Sortable.utils.select(element);
|
Sortable.utils.select(element);
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'nearest'
|
block: 'nearest',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Sortable.utils.deselect(element);
|
Sortable.utils.deselect(element);
|
||||||
@ -155,7 +155,7 @@
|
|||||||
group: {
|
group: {
|
||||||
name: sortableLevel,
|
name: sortableLevel,
|
||||||
pull: allowedMoves[sortableLevel],
|
pull: allowedMoves[sortableLevel],
|
||||||
put: true
|
put: true,
|
||||||
},
|
},
|
||||||
direction: orientation,
|
direction: orientation,
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
@ -233,16 +233,16 @@
|
|||||||
|
|
||||||
moveItems(fromItem, toItem, fromItems, toItems);
|
moveItems(fromItem, toItem, fromItems, toItems);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
Object.defineProperty(sortable, '_item', {
|
Object.defineProperty(sortable, '_item', {
|
||||||
value: item,
|
value: item,
|
||||||
writable: true
|
writable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(sortable, '_waypointRoot', {
|
Object.defineProperty(sortable, '_waypointRoot', {
|
||||||
value: waypointRoot,
|
value: waypointRoot,
|
||||||
writable: true
|
writable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,322 +1,335 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
Info,
|
Info,
|
||||||
MapPin,
|
MapPin,
|
||||||
PaintBucket,
|
PaintBucket,
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
Waypoints,
|
Waypoints,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
Maximize,
|
Maximize,
|
||||||
Scissors,
|
Scissors,
|
||||||
FileStack,
|
FileStack,
|
||||||
FileX
|
FileX,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListLevel,
|
ListLevel,
|
||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
allowedPastes,
|
allowedPastes,
|
||||||
type ListItem
|
type ListItem,
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import {
|
import {
|
||||||
copied,
|
copied,
|
||||||
copySelection,
|
copySelection,
|
||||||
cut,
|
cut,
|
||||||
cutSelection,
|
cutSelection,
|
||||||
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';
|
||||||
import {
|
import {
|
||||||
allHidden,
|
allHidden,
|
||||||
editMetadata,
|
editMetadata,
|
||||||
editStyle,
|
editStyle,
|
||||||
embedding,
|
embedding,
|
||||||
centerMapOnSelection,
|
centerMapOnSelection,
|
||||||
gpxLayers,
|
gpxLayers,
|
||||||
map
|
map,
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
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 { waypointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
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;
|
||||||
export let label: string | undefined;
|
export let label: string | undefined;
|
||||||
|
|
||||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||||
|
|
||||||
$: singleSelection = $selection.size === 1;
|
$: singleSelection = $selection.size === 1;
|
||||||
|
|
||||||
let nodeColors: string[] = [];
|
let nodeColors: string[] = [];
|
||||||
|
|
||||||
$: if (node && $map) {
|
$: if (node && $map) {
|
||||||
nodeColors = [];
|
nodeColors = [];
|
||||||
|
|
||||||
if (node instanceof GPXFile) {
|
if (node instanceof GPXFile) {
|
||||||
let defaultColor = undefined;
|
let defaultColor = undefined;
|
||||||
|
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
defaultColor = layer.layerColor;
|
defaultColor = layer.layerColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = node.getStyle(defaultColor);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (node instanceof Track) {
|
} else if (node instanceof Track) {
|
||||||
let style = node.getStyle();
|
let style = node.getStyle();
|
||||||
if (style) {
|
if (style) {
|
||||||
if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
|
if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
|
||||||
nodeColors.push(style['gpx_style:color']);
|
nodeColors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeColors.length === 0) {
|
if (nodeColors.length === 0) {
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (layer) {
|
if (layer) {
|
||||||
nodeColors.push(layer.layerColor);
|
nodeColors.push(layer.layerColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
|
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
|
||||||
|
|
||||||
let openEditMetadata: boolean = false;
|
let openEditMetadata: boolean = false;
|
||||||
let openEditStyle: boolean = false;
|
let openEditStyle: boolean = false;
|
||||||
|
|
||||||
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
|
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
|
||||||
$: openEditStyle =
|
$: openEditStyle =
|
||||||
$editStyle &&
|
$editStyle &&
|
||||||
$selection.has(item) &&
|
$selection.has(item) &&
|
||||||
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||||
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
|
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- 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 -->
|
||||||
<ContextMenu.Root
|
<ContextMenu.Root
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
if (!get(selection).has(item)) {
|
if (!get(selection).has(item)) {
|
||||||
selectItem(item);
|
selectItem(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContextMenu.Trigger class="grow truncate">
|
<ContextMenu.Trigger class="grow truncate">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
|
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
|
||||||
'vertical'
|
'vertical'
|
||||||
? 'h-fit'
|
? 'h-fit'
|
||||||
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
|
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
|
||||||
>
|
>
|
||||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
|
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
|
||||||
<StyleDialog bind:open={openEditStyle} {item} />
|
<StyleDialog bind:open={openEditStyle} {item} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
|
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
|
||||||
<div
|
<div
|
||||||
class="absolute {orientation === 'vertical'
|
class="absolute {orientation === 'vertical'
|
||||||
? 'top-0 bottom-0 right-1 w-1'
|
? 'top-0 bottom-0 right-1 w-1'
|
||||||
: 'top-0 h-1 left-0 right-0'}"
|
: 'top-0 h-1 left-0 right-0'}"
|
||||||
style="background:linear-gradient(to {orientation === 'vertical'
|
style="background:linear-gradient(to {orientation === 'vertical'
|
||||||
? 'bottom'
|
? 'bottom'
|
||||||
: 'right'},{nodeColors
|
: 'right'},{nodeColors
|
||||||
.map(
|
.map(
|
||||||
(c, i) =>
|
(c, i) =>
|
||||||
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
|
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
|
||||||
)
|
)
|
||||||
.join(',')})"
|
.join(',')})"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
|
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
|
||||||
? 'text-muted-foreground'
|
? 'text-muted-foreground'
|
||||||
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
|
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
|
||||||
? 'text-muted-foreground'
|
? 'text-muted-foreground'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:contextmenu={(e) => {
|
on:contextmenu={(e) => {
|
||||||
if ($embedding) {
|
if ($embedding) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
// Add to selection instead of opening context menu
|
// Add to selection instead of opening context menu
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
$selection.toggle(item);
|
$selection.toggle(item);
|
||||||
$selection = $selection;
|
$selection = $selection;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:mouseenter={() => {
|
on:mouseenter={() => {
|
||||||
if (item instanceof ListWaypointItem) {
|
if (item instanceof ListWaypointItem) {
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
if (layer && file) {
|
if (layer && file) {
|
||||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||||
if (waypoint) {
|
if (waypoint) {
|
||||||
waypointPopup?.setItem({ item: waypoint, fileId: item.getFileId() });
|
waypointPopup?.setItem({
|
||||||
}
|
item: waypoint,
|
||||||
}
|
fileId: item.getFileId(),
|
||||||
}
|
});
|
||||||
}}
|
}
|
||||||
on:mouseleave={() => {
|
}
|
||||||
if (item instanceof ListWaypointItem) {
|
}
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
}}
|
||||||
if (layer) {
|
on:mouseleave={() => {
|
||||||
waypointPopup?.setItem(null);
|
if (item instanceof ListWaypointItem) {
|
||||||
}
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
}
|
if (layer) {
|
||||||
}}
|
waypointPopup?.setItem(null);
|
||||||
>
|
}
|
||||||
{#if item.level === ListLevel.SEGMENT}
|
}
|
||||||
<Waypoints size="16" class="mr-1 shrink-0" />
|
}}
|
||||||
{:else if item.level === ListLevel.WAYPOINT}
|
>
|
||||||
{#if symbolKey && symbols[symbolKey].icon}
|
{#if item.level === ListLevel.SEGMENT}
|
||||||
<svelte:component this={symbols[symbolKey].icon} size="16" class="mr-1 shrink-0" />
|
<Waypoints size="16" class="mr-1 shrink-0" />
|
||||||
{:else}
|
{:else if item.level === ListLevel.WAYPOINT}
|
||||||
<MapPin size="16" class="mr-1 shrink-0" />
|
{#if symbolKey && symbols[symbolKey].icon}
|
||||||
{/if}
|
<svelte:component
|
||||||
{/if}
|
this={symbols[symbolKey].icon}
|
||||||
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}">
|
size="16"
|
||||||
{label}
|
class="mr-1 shrink-0"
|
||||||
</span>
|
/>
|
||||||
{#if hidden}
|
{:else}
|
||||||
<EyeOff
|
<MapPin size="16" class="mr-1 shrink-0" />
|
||||||
size="12"
|
{/if}
|
||||||
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
|
{/if}
|
||||||
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
|
<span
|
||||||
? 'mr-3'
|
class="grow select-none truncate {orientation === 'vertical'
|
||||||
: ''}"
|
? 'last:mr-2'
|
||||||
/>
|
: ''}"
|
||||||
{/if}
|
>
|
||||||
</span>
|
{label}
|
||||||
</Button>
|
</span>
|
||||||
</ContextMenu.Trigger>
|
{#if hidden}
|
||||||
<ContextMenu.Content>
|
<EyeOff
|
||||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
size="12"
|
||||||
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
|
||||||
<Info size="16" class="mr-1" />
|
? 'mr-2'
|
||||||
{$_('menu.metadata.button')}
|
: ''} {item.level === ListLevel.SEGMENT ||
|
||||||
<Shortcut key="I" ctrl={true} />
|
item.level === ListLevel.WAYPOINT
|
||||||
</ContextMenu.Item>
|
? 'mr-3'
|
||||||
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
: ''}"
|
||||||
<PaintBucket size="16" class="mr-1" />
|
/>
|
||||||
{$_('menu.style.button')}
|
{/if}
|
||||||
</ContextMenu.Item>
|
</span>
|
||||||
{/if}
|
</Button>
|
||||||
<ContextMenu.Item
|
</ContextMenu.Trigger>
|
||||||
on:click={() => {
|
<ContextMenu.Content>
|
||||||
if ($allHidden) {
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
dbUtils.setHiddenToSelection(false);
|
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
||||||
} else {
|
<Info size="16" class="mr-1" />
|
||||||
dbUtils.setHiddenToSelection(true);
|
{$_('menu.metadata.button')}
|
||||||
}
|
<Shortcut key="I" ctrl={true} />
|
||||||
}}
|
</ContextMenu.Item>
|
||||||
>
|
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
||||||
{#if $allHidden}
|
<PaintBucket size="16" class="mr-1" />
|
||||||
<Eye size="16" class="mr-1" />
|
{$_('menu.style.button')}
|
||||||
{$_('menu.unhide')}
|
</ContextMenu.Item>
|
||||||
{:else}
|
{/if}
|
||||||
<EyeOff size="16" class="mr-1" />
|
<ContextMenu.Item
|
||||||
{$_('menu.hide')}
|
on:click={() => {
|
||||||
{/if}
|
if ($allHidden) {
|
||||||
<Shortcut key="H" ctrl={true} />
|
dbUtils.setHiddenToSelection(false);
|
||||||
</ContextMenu.Item>
|
} else {
|
||||||
<ContextMenu.Separator />
|
dbUtils.setHiddenToSelection(true);
|
||||||
{#if orientation === 'vertical'}
|
}
|
||||||
{#if item instanceof ListFileItem}
|
}}
|
||||||
<ContextMenu.Item
|
>
|
||||||
disabled={!singleSelection}
|
{#if $allHidden}
|
||||||
on:click={() => dbUtils.addNewTrack(item.getFileId())}
|
<Eye size="16" class="mr-1" />
|
||||||
>
|
{$_('menu.unhide')}
|
||||||
<Plus size="16" class="mr-1" />
|
{:else}
|
||||||
{$_('menu.new_track')}
|
<EyeOff size="16" class="mr-1" />
|
||||||
</ContextMenu.Item>
|
{$_('menu.hide')}
|
||||||
<ContextMenu.Separator />
|
{/if}
|
||||||
{:else if item instanceof ListTrackItem}
|
<Shortcut key="H" ctrl={true} />
|
||||||
<ContextMenu.Item
|
</ContextMenu.Item>
|
||||||
disabled={!singleSelection}
|
<ContextMenu.Separator />
|
||||||
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
{#if orientation === 'vertical'}
|
||||||
>
|
{#if item instanceof ListFileItem}
|
||||||
<Plus size="16" class="mr-1" />
|
<ContextMenu.Item
|
||||||
{$_('menu.new_segment')}
|
disabled={!singleSelection}
|
||||||
</ContextMenu.Item>
|
on:click={() => dbUtils.addNewTrack(item.getFileId())}
|
||||||
<ContextMenu.Separator />
|
>
|
||||||
{/if}
|
<Plus size="16" class="mr-1" />
|
||||||
{/if}
|
{$_('menu.new_track')}
|
||||||
{#if item.level !== ListLevel.WAYPOINTS}
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item on:click={selectAll}>
|
<ContextMenu.Separator />
|
||||||
<FileStack size="16" class="mr-1" />
|
{:else if item instanceof ListTrackItem}
|
||||||
{$_('menu.select_all')}
|
<ContextMenu.Item
|
||||||
<Shortcut key="A" ctrl={true} />
|
disabled={!singleSelection}
|
||||||
</ContextMenu.Item>
|
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||||
{/if}
|
>
|
||||||
<ContextMenu.Item on:click={centerMapOnSelection}>
|
<Plus size="16" class="mr-1" />
|
||||||
<Maximize size="16" class="mr-1" />
|
{$_('menu.new_segment')}
|
||||||
{$_('menu.center')}
|
</ContextMenu.Item>
|
||||||
<Shortcut key="⏎" ctrl={true} />
|
<ContextMenu.Separator />
|
||||||
</ContextMenu.Item>
|
{/if}
|
||||||
<ContextMenu.Separator />
|
{/if}
|
||||||
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
{#if item.level !== ListLevel.WAYPOINTS}
|
||||||
<Copy size="16" class="mr-1" />
|
<ContextMenu.Item on:click={selectAll}>
|
||||||
{$_('menu.duplicate')}
|
<FileStack size="16" class="mr-1" />
|
||||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
{$_('menu.select_all')}
|
||||||
>
|
<Shortcut key="A" ctrl={true} />
|
||||||
{#if orientation === 'vertical'}
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item on:click={copySelection}>
|
{/if}
|
||||||
<ClipboardCopy size="16" class="mr-1" />
|
<ContextMenu.Item on:click={centerMapOnSelection}>
|
||||||
{$_('menu.copy')}
|
<Maximize size="16" class="mr-1" />
|
||||||
<Shortcut key="C" ctrl={true} />
|
{$_('menu.center')}
|
||||||
</ContextMenu.Item>
|
<Shortcut key="⏎" ctrl={true} />
|
||||||
<ContextMenu.Item on:click={cutSelection}>
|
</ContextMenu.Item>
|
||||||
<Scissors size="16" class="mr-1" />
|
<ContextMenu.Separator />
|
||||||
{$_('menu.cut')}
|
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
||||||
<Shortcut key="X" ctrl={true} />
|
<Copy size="16" class="mr-1" />
|
||||||
</ContextMenu.Item>
|
{$_('menu.duplicate')}
|
||||||
<ContextMenu.Item
|
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||||
disabled={$copied === undefined ||
|
>
|
||||||
$copied.length === 0 ||
|
{#if orientation === 'vertical'}
|
||||||
!allowedPastes[$copied[0].level].includes(item.level)}
|
<ContextMenu.Item on:click={copySelection}>
|
||||||
on:click={pasteSelection}
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
>
|
{$_('menu.copy')}
|
||||||
<ClipboardPaste size="16" class="mr-1" />
|
<Shortcut key="C" ctrl={true} />
|
||||||
{$_('menu.paste')}
|
</ContextMenu.Item>
|
||||||
<Shortcut key="V" ctrl={true} />
|
<ContextMenu.Item on:click={cutSelection}>
|
||||||
</ContextMenu.Item>
|
<Scissors size="16" class="mr-1" />
|
||||||
{/if}
|
{$_('menu.cut')}
|
||||||
<ContextMenu.Separator />
|
<Shortcut key="X" ctrl={true} />
|
||||||
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
</ContextMenu.Item>
|
||||||
{#if item instanceof ListFileItem}
|
<ContextMenu.Item
|
||||||
<FileX size="16" class="mr-1" />
|
disabled={$copied === undefined ||
|
||||||
{$_('menu.close')}
|
$copied.length === 0 ||
|
||||||
{:else}
|
!allowedPastes[$copied[0].level].includes(item.level)}
|
||||||
<Trash2 size="16" class="mr-1" />
|
on:click={pasteSelection}
|
||||||
{$_('menu.delete')}
|
>
|
||||||
{/if}
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
<Shortcut key="⌫" ctrl={true} />
|
{$_('menu.paste')}
|
||||||
</ContextMenu.Item>
|
<Shortcut key="V" ctrl={true} />
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Item>
|
||||||
|
{/if}
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
||||||
|
{#if item instanceof ListFileItem}
|
||||||
|
<FileX size="16" class="mr-1" />
|
||||||
|
{$_('menu.close')}
|
||||||
|
{:else}
|
||||||
|
<Trash2 size="16" class="mr-1" />
|
||||||
|
{$_('menu.delete')}
|
||||||
|
{/if}
|
||||||
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
|
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
|
||||||
import FileListNode from '$lib/components/file-list/FileListNode.svelte';
|
import FileListNode from '$lib/components/file-list/FileListNode.svelte';
|
||||||
|
|
||||||
import type { GPXFileWithStatistics } from '$lib/db';
|
import type { GPXFileWithStatistics } from '$lib/db';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import type { Readable } from 'svelte/store';
|
import type { Readable } from 'svelte/store';
|
||||||
import { ListFileItem } from './FileList';
|
import { ListFileItem } from './FileList';
|
||||||
|
|
||||||
export let file: Readable<GPXFileWithStatistics | undefined>;
|
export let file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
|
|
||||||
let recursive = getContext<boolean>('recursive');
|
let recursive = getContext<boolean>('recursive');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $file}
|
{#if $file}
|
||||||
{#if recursive}
|
{#if recursive}
|
||||||
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
|
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
|
||||||
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||||
</CollapsibleTree>
|
</CollapsibleTree>
|
||||||
{:else}
|
{:else}
|
||||||
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -312,4 +372,4 @@ export function pasteSelection() {
|
|||||||
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
|
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
|
||||||
resetCopied();
|
resetCopied();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,167 +1,173 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import { dbUtils, getFile, settings } from '$lib/db';
|
import { dbUtils, getFile, settings } from '$lib/db';
|
||||||
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 { 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 'svelte-i18n';
|
||||||
|
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
const { defaultOpacity, defaultWidth } = 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 width: number[] = [];
|
let width: number[] = [];
|
||||||
let colorChanged = false;
|
let colorChanged = false;
|
||||||
let opacityChanged = false;
|
let opacityChanged = false;
|
||||||
let widthChanged = false;
|
let widthChanged = false;
|
||||||
|
|
||||||
function setStyleInputs() {
|
function setStyleInputs() {
|
||||||
colors = [];
|
colors = [];
|
||||||
opacity = [];
|
opacity = [];
|
||||||
width = [];
|
width = [];
|
||||||
|
|
||||||
$selection.forEach((item) => {
|
$selection.forEach((item) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (file && layer) {
|
if (file && layer) {
|
||||||
let style = file.getStyle();
|
let style = file.getStyle();
|
||||||
style.color.push(layer.layerColor);
|
style.color.push(layer.layerColor);
|
||||||
|
|
||||||
style.color.forEach((c) => {
|
style.color.forEach((c) => {
|
||||||
if (!colors.includes(c)) {
|
if (!colors.includes(c)) {
|
||||||
colors.push(c);
|
colors.push(c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
style.opacity.forEach((o) => {
|
style.opacity.forEach((o) => {
|
||||||
if (!opacity.includes(o)) {
|
if (!opacity.includes(o)) {
|
||||||
opacity.push(o);
|
opacity.push(o);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
style.width.forEach((w) => {
|
style.width.forEach((w) => {
|
||||||
if (!width.includes(w)) {
|
if (!width.includes(w)) {
|
||||||
width.push(w);
|
width.push(w);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
if (file && layer) {
|
if (file && layer) {
|
||||||
let track = file.trk[item.getTrackIndex()];
|
let track = file.trk[item.getTrackIndex()];
|
||||||
let style = track.getStyle();
|
let style = track.getStyle();
|
||||||
if (style) {
|
if (style) {
|
||||||
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
|
if (
|
||||||
colors.push(style['gpx_style:color']);
|
style['gpx_style:color'] &&
|
||||||
}
|
!colors.includes(style['gpx_style:color'])
|
||||||
if (style['gpx_style:opacity'] && !opacity.includes(style['gpx_style:opacity'])) {
|
) {
|
||||||
opacity.push(style['gpx_style:opacity']);
|
colors.push(style['gpx_style:color']);
|
||||||
}
|
}
|
||||||
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
|
if (
|
||||||
width.push(style['gpx_style:width']);
|
style['gpx_style:opacity'] &&
|
||||||
}
|
!opacity.includes(style['gpx_style:opacity'])
|
||||||
}
|
) {
|
||||||
if (!colors.includes(layer.layerColor)) {
|
opacity.push(style['gpx_style:opacity']);
|
||||||
colors.push(layer.layerColor);
|
}
|
||||||
}
|
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
|
||||||
}
|
width.push(style['gpx_style:width']);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
if (!colors.includes(layer.layerColor)) {
|
||||||
|
colors.push(layer.layerColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
color = colors[0];
|
color = colors[0];
|
||||||
opacity = [opacity[0] ?? $defaultOpacity];
|
opacity = [opacity[0] ?? $defaultOpacity];
|
||||||
width = [width[0] ?? $defaultWidth];
|
width = [width[0] ?? $defaultWidth];
|
||||||
|
|
||||||
colorChanged = false;
|
colorChanged = false;
|
||||||
opacityChanged = false;
|
opacityChanged = false;
|
||||||
widthChanged = false;
|
widthChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selection && open) {
|
$: if ($selection && open) {
|
||||||
setStyleInputs();
|
setStyleInputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (!open) {
|
$: if (!open) {
|
||||||
$editStyle = false;
|
$editStyle = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover.Root bind:open>
|
<Popover.Root bind:open>
|
||||||
<Popover.Trigger />
|
<Popover.Trigger />
|
||||||
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
{$_('menu.style.color')}
|
{$_('menu.style.color')}
|
||||||
<Input
|
<Input
|
||||||
bind:value={color}
|
bind:value={color}
|
||||||
type="color"
|
type="color"
|
||||||
class="p-0 h-6 w-40"
|
class="p-0 h-6 w-40"
|
||||||
on:change={() => (colorChanged = true)}
|
on:change={() => (colorChanged = true)}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
{$_('menu.style.opacity')}
|
{$_('menu.style.opacity')}
|
||||||
<div class="w-40 p-2">
|
<div class="w-40 p-2">
|
||||||
<Slider
|
<Slider
|
||||||
bind:value={opacity}
|
bind:value={opacity}
|
||||||
min={0.3}
|
min={0.3}
|
||||||
max={1}
|
max={1}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onValueChange={() => (opacityChanged = true)}
|
onValueChange={() => (opacityChanged = true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
{$_('menu.style.width')}
|
{$_('menu.style.width')}
|
||||||
<div class="w-40 p-2">
|
<div class="w-40 p-2">
|
||||||
<Slider
|
<Slider
|
||||||
bind:value={width}
|
bind:value={width}
|
||||||
id="width"
|
id="width"
|
||||||
min={1}
|
min={1}
|
||||||
max={10}
|
max={10}
|
||||||
step={1}
|
step={1}
|
||||||
onValueChange={() => (widthChanged = true)}
|
onValueChange={() => (widthChanged = true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!colorChanged && !opacityChanged && !widthChanged}
|
disabled={!colorChanged && !opacityChanged && !widthChanged}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let style = {};
|
let style = {};
|
||||||
if (colorChanged) {
|
if (colorChanged) {
|
||||||
style['gpx_style:color'] = color;
|
style['gpx_style:color'] = color;
|
||||||
}
|
}
|
||||||
if (opacityChanged) {
|
if (opacityChanged) {
|
||||||
style['gpx_style:opacity'] = opacity[0];
|
style['gpx_style:opacity'] = opacity[0];
|
||||||
}
|
}
|
||||||
if (widthChanged) {
|
if (widthChanged) {
|
||||||
style['gpx_style:width'] = width[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['gpx_style:opacity']) {
|
if (style['gpx_style:opacity']) {
|
||||||
$defaultOpacity = style['gpx_style:opacity'];
|
$defaultOpacity = style['gpx_style:opacity'];
|
||||||
}
|
}
|
||||||
if (style['gpx_style:width']) {
|
if (style['gpx_style:width']) {
|
||||||
$defaultWidth = style['gpx_style:width'];
|
$defaultWidth = style['gpx_style:width'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open = false;
|
open = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Save size="16" class="mr-1" />
|
<Save size="16" class="mr-1" />
|
||||||
{$_('menu.metadata.save')}
|
{$_('menu.metadata.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
|
import { settings } from '$lib/db';
|
||||||
import { settings } from "$lib/db";
|
import { gpxStatistics } from '$lib/stores';
|
||||||
import { gpxStatistics } from "$lib/stores";
|
import { get } from 'svelte/store';
|
||||||
import { get } from "svelte/store";
|
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
const stops = [[100, 0], [50, 7], [25, 8, 10], [10, 10], [5, 11], [1, 13]];
|
const stops = [
|
||||||
|
[100, 0],
|
||||||
|
[50, 7],
|
||||||
|
[25, 8, 10],
|
||||||
|
[10, 10],
|
||||||
|
[5, 11],
|
||||||
|
[1, 13],
|
||||||
|
];
|
||||||
|
|
||||||
export class DistanceMarkers {
|
export class DistanceMarkers {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@ -30,7 +36,7 @@ export class DistanceMarkers {
|
|||||||
} else {
|
} else {
|
||||||
this.map.addSource('distance-markers', {
|
this.map.addSource('distance-markers', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: this.getDistanceMarkersGeoJSON()
|
data: this.getDistanceMarkersGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
stops.forEach(([d, minzoom, maxzoom]) => {
|
stops.forEach(([d, minzoom, maxzoom]) => {
|
||||||
@ -39,7 +45,14 @@ export class DistanceMarkers {
|
|||||||
id: `distance-markers-${d}`,
|
id: `distance-markers-${d}`,
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'distance-markers',
|
source: 'distance-markers',
|
||||||
filter: d === 5 ? ['any', ['==', ['get', 'level'], 5], ['==', ['get', 'level'], 25]] : ['==', ['get', 'level'], d],
|
filter:
|
||||||
|
d === 5
|
||||||
|
? [
|
||||||
|
'any',
|
||||||
|
['==', ['get', 'level'], 5],
|
||||||
|
['==', ['get', 'level'], 25],
|
||||||
|
]
|
||||||
|
: ['==', ['get', 'level'], d],
|
||||||
minzoom: minzoom,
|
minzoom: minzoom,
|
||||||
maxzoom: maxzoom ?? 24,
|
maxzoom: maxzoom ?? 24,
|
||||||
layout: {
|
layout: {
|
||||||
@ -51,7 +64,7 @@ export class DistanceMarkers {
|
|||||||
'text-color': 'black',
|
'text-color': 'black',
|
||||||
'text-halo-width': 2,
|
'text-halo-width': 2,
|
||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.map.moveLayer(`distance-markers-${d}`);
|
this.map.moveLayer(`distance-markers-${d}`);
|
||||||
@ -64,13 +77,14 @@ export class DistanceMarkers {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
@ -79,20 +93,28 @@ export class DistanceMarkers {
|
|||||||
let features = [];
|
let features = [];
|
||||||
let currentTargetDistance = 1;
|
let currentTargetDistance = 1;
|
||||||
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
||||||
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
|
if (
|
||||||
|
statistics.local.distance.total[i] >=
|
||||||
|
currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)
|
||||||
|
) {
|
||||||
let distance = currentTargetDistance.toFixed(0);
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [0, 0];
|
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
||||||
|
0, 0,
|
||||||
|
];
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
|
coordinates: [
|
||||||
|
statistics.local.points[i].getLongitude(),
|
||||||
|
statistics.local.points[i].getLatitude(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
distance,
|
distance,
|
||||||
level,
|
level,
|
||||||
minzoom,
|
minzoom,
|
||||||
}
|
},
|
||||||
} as GeoJSON.Feature);
|
} as GeoJSON.Feature);
|
||||||
currentTargetDistance += 1;
|
currentTargetDistance += 1;
|
||||||
}
|
}
|
||||||
@ -100,7 +122,7 @@ export class DistanceMarkers {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features
|
features,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
import { currentTool, map, Tool } from "$lib/stores";
|
import { currentTool, map, Tool } from '$lib/stores';
|
||||||
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
|
import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
|
||||||
import { get, type Readable } from "svelte/store";
|
import { get, type Readable } from 'svelte/store';
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { waypointPopup, deleteWaypoint, trackpointPopup } from "./GPXLayerPopup";
|
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
|
||||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
|
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
import {
|
||||||
import { getClosestLinePoint, getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
ListTrackSegmentItem,
|
||||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
ListWaypointItem,
|
||||||
import { MapPin, Square } from "lucide-static";
|
ListWaypointsItem,
|
||||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
ListTrackItem,
|
||||||
|
ListFileItem,
|
||||||
|
ListRootItem,
|
||||||
|
} from '$lib/components/file-list/FileList';
|
||||||
|
import {
|
||||||
|
getClosestLinePoint,
|
||||||
|
getElevation,
|
||||||
|
resetCursor,
|
||||||
|
setGrabbingCursor,
|
||||||
|
setPointerCursor,
|
||||||
|
setScissorsCursor,
|
||||||
|
} from '$lib/utils';
|
||||||
|
import { selectedWaypoint } from '$lib/components/toolbar/tools/Waypoint.svelte';
|
||||||
|
import { MapPin, Square } from 'lucide-static';
|
||||||
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000',
|
'#ff0000',
|
||||||
@ -21,7 +35,7 @@ const colors = [
|
|||||||
'#288228',
|
'#288228',
|
||||||
'#9933ff',
|
'#9933ff',
|
||||||
'#50f0be',
|
'#50f0be',
|
||||||
'#8c645a'
|
'#8c645a',
|
||||||
];
|
];
|
||||||
|
|
||||||
const colorCount: { [key: string]: number } = {};
|
const colorCount: { [key: string]: number } = {};
|
||||||
@ -56,12 +70,12 @@ class KeyDown {
|
|||||||
if (e.key === this.key) {
|
if (e.key === this.key) {
|
||||||
this.down = true;
|
this.down = true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
onKeyUp = (e: KeyboardEvent) => {
|
onKeyUp = (e: KeyboardEvent) => {
|
||||||
if (e.key === this.key) {
|
if (e.key === this.key) {
|
||||||
this.down = false;
|
this.down = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
isDown() {
|
isDown() {
|
||||||
return this.down;
|
return this.down;
|
||||||
}
|
}
|
||||||
@ -70,22 +84,26 @@ class KeyDown {
|
|||||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||||
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
${Square
|
${Square.replace('width="24"', 'width="12"')
|
||||||
.replace('width="24"', 'width="12"')
|
.replace('height="24"', 'height="12"')
|
||||||
.replace('height="24"', 'height="12"')
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
${MapPin.replace('width="24"', '')
|
||||||
${MapPin
|
.replace('height="24"', '')
|
||||||
.replace('width="24"', '')
|
.replace('stroke="currentColor"', '')
|
||||||
.replace('height="24"', '')
|
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
||||||
.replace('stroke="currentColor"', '')
|
.replace(
|
||||||
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
'circle',
|
||||||
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
|
`circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`
|
||||||
${symbolSvg?.replace('width="24"', 'width="10"')
|
)}
|
||||||
|
${
|
||||||
|
symbolSvg
|
||||||
|
?.replace('width="24"', 'width="10"')
|
||||||
.replace('height="24"', 'height="10"')
|
.replace('height="24"', 'height="10"')
|
||||||
.replace('stroke="currentColor"', 'stroke="white"')
|
.replace('stroke="currentColor"', 'stroke="white"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
|
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''
|
||||||
|
}
|
||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,32 +126,40 @@ export class GPXLayer {
|
|||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(
|
||||||
|
map: mapboxgl.Map,
|
||||||
|
fileId: string,
|
||||||
|
file: Readable<GPXFileWithStatistics | undefined>
|
||||||
|
) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.layerColor = getColor();
|
this.layerColor = getColor();
|
||||||
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
||||||
this.unsubscribe.push(selection.subscribe($selection => {
|
this.unsubscribe.push(
|
||||||
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
|
selection.subscribe(($selection) => {
|
||||||
if (this.selected || newSelected) {
|
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
|
||||||
this.selected = newSelected;
|
if (this.selected || newSelected) {
|
||||||
this.update();
|
this.selected = newSelected;
|
||||||
}
|
this.update();
|
||||||
if (newSelected) {
|
}
|
||||||
this.moveToFront();
|
if (newSelected) {
|
||||||
}
|
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(
|
||||||
if (tool === Tool.WAYPOINT && !this.draggable) {
|
currentTool.subscribe((tool) => {
|
||||||
this.draggable = true;
|
if (tool === Tool.WAYPOINT && !this.draggable) {
|
||||||
this.markers.forEach(marker => marker.setDraggable(true));
|
this.draggable = true;
|
||||||
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
this.markers.forEach((marker) => marker.setDraggable(true));
|
||||||
this.draggable = false;
|
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||||
this.markers.forEach(marker => marker.setDraggable(false));
|
this.draggable = false;
|
||||||
}
|
this.markers.forEach((marker) => marker.setDraggable(false));
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
|
|
||||||
this.map.on('style.import.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
@ -149,7 +175,11 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
|
if (
|
||||||
|
file._data.style &&
|
||||||
|
file._data.style.color &&
|
||||||
|
this.layerColor !== `#${file._data.style.color}`
|
||||||
|
) {
|
||||||
decrementColor(this.layerColor);
|
decrementColor(this.layerColor);
|
||||||
this.layerColor = `#${file._data.style.color}`;
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
}
|
}
|
||||||
@ -161,7 +191,7 @@ export class GPXLayer {
|
|||||||
} else {
|
} else {
|
||||||
this.map.addSource(this.fileId, {
|
this.map.addSource(this.fileId, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: this.getGeoJSON()
|
data: this.getGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,13 +202,13 @@ export class GPXLayer {
|
|||||||
source: this.fileId,
|
source: this.fileId,
|
||||||
layout: {
|
layout: {
|
||||||
'line-join': 'round',
|
'line-join': 'round',
|
||||||
'line-cap': 'round'
|
'line-cap': 'round',
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['get', 'color'],
|
'line-color': ['get', 'color'],
|
||||||
'line-width': ['get', 'width'],
|
'line-width': ['get', 'width'],
|
||||||
'line-opacity': ['get', 'opacity']
|
'line-opacity': ['get', 'opacity'],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
@ -190,27 +220,30 @@ export class GPXLayer {
|
|||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
if (!this.map.getLayer(this.fileId + '-direction')) {
|
if (!this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.addLayer({
|
this.map.addLayer(
|
||||||
id: this.fileId + '-direction',
|
{
|
||||||
type: 'symbol',
|
id: this.fileId + '-direction',
|
||||||
source: this.fileId,
|
type: 'symbol',
|
||||||
layout: {
|
source: this.fileId,
|
||||||
'text-field': '»',
|
layout: {
|
||||||
'text-offset': [0, -0.1],
|
'text-field': '»',
|
||||||
'text-keep-upright': false,
|
'text-offset': [0, -0.1],
|
||||||
'text-max-angle': 361,
|
'text-keep-upright': false,
|
||||||
'text-allow-overlap': true,
|
'text-max-angle': 361,
|
||||||
'text-font': ['Open Sans Bold'],
|
'text-allow-overlap': true,
|
||||||
'symbol-placement': 'line',
|
'text-font': ['Open Sans Bold'],
|
||||||
'symbol-spacing': 20,
|
'symbol-placement': 'line',
|
||||||
|
'symbol-spacing': 20,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': 'white',
|
||||||
|
'text-opacity': 0.7,
|
||||||
|
'text-halo-width': 0.2,
|
||||||
|
'text-halo-color': 'white',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
paint: {
|
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||||
'text-color': 'white',
|
);
|
||||||
'text-opacity': 0.7,
|
|
||||||
'text-halo-width': 0.2,
|
|
||||||
'text-halo-color': 'white'
|
|
||||||
}
|
|
||||||
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
@ -225,23 +258,53 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
this.map.setFilter(
|
||||||
|
this.fileId,
|
||||||
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
{ validate: false }
|
||||||
|
);
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
this.map.setFilter(
|
||||||
|
this.fileId + '-direction',
|
||||||
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
{ validate: false }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let markerIndex = 0;
|
let markerIndex = 0;
|
||||||
|
|
||||||
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
||||||
file.wpt.forEach((waypoint) => { // Update markers
|
file.wpt.forEach((waypoint) => {
|
||||||
|
// Update markers
|
||||||
let symbolKey = getSymbolKey(waypoint.sym);
|
let symbolKey = getSymbolKey(waypoint.sym);
|
||||||
if (markerIndex < this.markers.length) {
|
if (markerIndex < this.markers.length) {
|
||||||
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
|
||||||
|
symbolKey,
|
||||||
|
this.layerColor
|
||||||
|
);
|
||||||
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
||||||
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
|
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
|
||||||
|
value: waypoint,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||||
@ -249,7 +312,7 @@ export class GPXLayer {
|
|||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: this.draggable,
|
draggable: this.draggable,
|
||||||
element,
|
element,
|
||||||
anchor: 'bottom'
|
anchor: 'bottom',
|
||||||
}).setLngLat(waypoint.getCoordinates());
|
}).setLngLat(waypoint.getCoordinates());
|
||||||
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||||
let dragEndTimestamp = 0;
|
let dragEndTimestamp = 0;
|
||||||
@ -272,10 +335,20 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (get(treeFileView)) {
|
if (get(treeFileView)) {
|
||||||
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
|
if (
|
||||||
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
get(selection).hasAnyChildren(
|
||||||
|
new ListWaypointsItem(this.fileId),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addSelectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
selectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (get(currentTool) === Tool.WAYPOINT) {
|
} else if (get(currentTool) === Tool.WAYPOINT) {
|
||||||
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||||
@ -298,12 +371,12 @@ export class GPXLayer {
|
|||||||
let wpt = file.wpt[marker._waypoint._data.index];
|
let wpt = file.wpt[marker._waypoint._data.index];
|
||||||
wpt.setCoordinates({
|
wpt.setCoordinates({
|
||||||
lat: latLng.lat,
|
lat: latLng.lat,
|
||||||
lon: latLng.lng
|
lon: latLng.lng,
|
||||||
});
|
});
|
||||||
wpt.ele = ele[0];
|
wpt.ele = ele[0];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
dragEndTimestamp = Date.now()
|
dragEndTimestamp = Date.now();
|
||||||
});
|
});
|
||||||
this.markers.push(marker);
|
this.markers.push(marker);
|
||||||
}
|
}
|
||||||
@ -311,7 +384,8 @@ export class GPXLayer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
while (markerIndex < this.markers.length) { // Remove extra markers
|
while (markerIndex < this.markers.length) {
|
||||||
|
// Remove extra markers
|
||||||
this.markers.pop()?.remove();
|
this.markers.pop()?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +438,10 @@ export class GPXLayer {
|
|||||||
this.map.moveLayer(this.fileId);
|
this.map.moveLayer(this.fileId);
|
||||||
}
|
}
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
this.map.moveLayer(
|
||||||
|
this.fileId + '-direction',
|
||||||
|
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +449,12 @@ export class GPXLayer {
|
|||||||
let trackIndex = e.features[0].properties.trackIndex;
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
setScissorsCursor();
|
setScissorsCursor();
|
||||||
} else {
|
} else {
|
||||||
setPointerCursor();
|
setPointerCursor();
|
||||||
@ -390,22 +472,36 @@ export class GPXLayer {
|
|||||||
|
|
||||||
const file = get(this.file)?.file;
|
const file = get(this.file)?.file;
|
||||||
if (file) {
|
if (file) {
|
||||||
const closest = getClosestLinePoint(file.trk[trackIndex].trkseg[segmentIndex].trkpt, { lat: e.lngLat.lat, lon: e.lngLat.lng });
|
const closest = getClosestLinePoint(
|
||||||
|
file.trk[trackIndex].trkseg[segmentIndex].trkpt,
|
||||||
|
{ lat: e.lngLat.lat, lon: e.lngLat.lng }
|
||||||
|
);
|
||||||
trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
|
trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: any) {
|
layerOnClick(e: any) {
|
||||||
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
if (
|
||||||
|
get(currentTool) === Tool.ROUTING &&
|
||||||
|
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let trackIndex = e.features[0].properties.trackIndex;
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (
|
||||||
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
dbUtils.split(this.fileId, trackIndex, segmentIndex, {
|
||||||
|
lat: e.lngLat.lat,
|
||||||
|
lon: e.lngLat.lng,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,8 +511,12 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let item = undefined;
|
let item = undefined;
|
||||||
if (get(treeFileView) && file.getSegments().length > 1) { // Select inner item
|
if (get(treeFileView) && file.getSegments().length > 1) {
|
||||||
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
// Select inner item
|
||||||
|
item =
|
||||||
|
file.children[trackIndex].children.length > 1
|
||||||
|
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
: new ListTrackItem(this.fileId, trackIndex);
|
||||||
} else {
|
} else {
|
||||||
item = new ListFileItem(this.fileId);
|
item = new ListFileItem(this.fileId);
|
||||||
}
|
}
|
||||||
@ -439,13 +539,14 @@ export class GPXLayer {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = file.toGeoJSON();
|
let data = file.toGeoJSON();
|
||||||
|
|
||||||
let trackIndex = 0, segmentIndex = 0;
|
let trackIndex = 0,
|
||||||
|
segmentIndex = 0;
|
||||||
for (let feature of data.features) {
|
for (let feature of data.features) {
|
||||||
if (!feature.properties) {
|
if (!feature.properties) {
|
||||||
feature.properties = {};
|
feature.properties = {};
|
||||||
@ -459,7 +560,12 @@ export class GPXLayer {
|
|||||||
if (!feature.properties.width) {
|
if (!feature.properties.width) {
|
||||||
feature.properties.width = get(defaultWidth);
|
feature.properties.width = get(defaultWidth);
|
||||||
}
|
}
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
|
if (
|
||||||
|
get(selection).hasAnyParent(
|
||||||
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
|
) ||
|
||||||
|
get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)
|
||||||
|
) {
|
||||||
feature.properties.width = feature.properties.width + 2;
|
feature.properties.width = feature.properties.width + 2;
|
||||||
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
|
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
|
||||||
}
|
}
|
||||||
@ -474,4 +580,4 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { dbUtils } from "$lib/db";
|
import { dbUtils } from '$lib/db';
|
||||||
import { MapPopup } from "$lib/components/MapPopup";
|
import { MapPopup } from '$lib/components/MapPopup';
|
||||||
|
|
||||||
export let waypointPopup: MapPopup | null = null;
|
export let waypointPopup: MapPopup | null = null;
|
||||||
export let trackpointPopup: MapPopup | null = null;
|
export let trackpointPopup: MapPopup | null = null;
|
||||||
@ -11,14 +11,14 @@ export function createPopups(map: mapboxgl.Map) {
|
|||||||
focusAfterOpen: false,
|
focusAfterOpen: false,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
offset: {
|
offset: {
|
||||||
'top': [0, 0],
|
top: [0, 0],
|
||||||
'top-left': [0, 0],
|
'top-left': [0, 0],
|
||||||
'top-right': [0, 0],
|
'top-right': [0, 0],
|
||||||
'bottom': [0, -30],
|
bottom: [0, -30],
|
||||||
'bottom-left': [0, -30],
|
'bottom-left': [0, -30],
|
||||||
'bottom-right': [0, -30],
|
'bottom-right': [0, -30],
|
||||||
'left': [10, -15],
|
left: [10, -15],
|
||||||
'right': [-10, -15],
|
right: [-10, -15],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
trackpointPopup = new MapPopup(map, {
|
trackpointPopup = new MapPopup(map, {
|
||||||
@ -41,4 +41,4 @@ export function removePopups() {
|
|||||||
|
|
||||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
|
export function deleteWaypoint(fileId: string, waypointIndex: number) {
|
||||||
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
|
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,56 @@
|
|||||||
<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 { 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';
|
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;
|
||||||
|
|
||||||
$: if ($map && $fileObservers) {
|
$: if ($map && $fileObservers) {
|
||||||
// remove layers for deleted files
|
// remove layers for deleted files
|
||||||
gpxLayers.forEach((layer, fileId) => {
|
gpxLayers.forEach((layer, fileId) => {
|
||||||
if (!$fileObservers.has(fileId)) {
|
if (!$fileObservers.has(fileId)) {
|
||||||
layer.remove();
|
layer.remove();
|
||||||
gpxLayers.delete(fileId);
|
gpxLayers.delete(fileId);
|
||||||
} else if ($map !== layer.map) {
|
} else if ($map !== layer.map) {
|
||||||
layer.updateMap($map);
|
layer.updateMap($map);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// add layers for new files
|
// add layers for new files
|
||||||
$fileObservers.forEach((file, fileId) => {
|
$fileObservers.forEach((file, fileId) => {
|
||||||
if (!gpxLayers.has(fileId)) {
|
if (!gpxLayers.has(fileId)) {
|
||||||
gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
|
gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
if (distanceMarkers) {
|
if (distanceMarkers) {
|
||||||
distanceMarkers.remove();
|
distanceMarkers.remove();
|
||||||
}
|
}
|
||||||
if (startEndMarkers) {
|
if (startEndMarkers) {
|
||||||
startEndMarkers.remove();
|
startEndMarkers.remove();
|
||||||
}
|
}
|
||||||
createPopups($map);
|
createPopups($map);
|
||||||
distanceMarkers = new DistanceMarkers($map);
|
distanceMarkers = new DistanceMarkers($map);
|
||||||
startEndMarkers = new StartEndMarkers($map);
|
startEndMarkers = new StartEndMarkers($map);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
gpxLayers.forEach((layer) => layer.remove());
|
gpxLayers.forEach((layer) => layer.remove());
|
||||||
gpxLayers.clear();
|
gpxLayers.clear();
|
||||||
removePopups();
|
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>
|
||||||
|
|||||||
@ -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,9 +44,9 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,51 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TrackPoint } from 'gpx';
|
import type { TrackPoint } from 'gpx';
|
||||||
import type { PopupItem } from '$lib/components/MapPopup';
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
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 WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { Compass, Mountain, Timer, ClipboardCopy } from 'lucide-svelte';
|
import { Compass, Mountain, Timer, ClipboardCopy } from 'lucide-svelte';
|
||||||
import { df } from '$lib/utils';
|
import { df } from '$lib/utils';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let trackpoint: PopupItem<TrackPoint>;
|
export let trackpoint: PopupItem<TrackPoint>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root class="border-none shadow-md text-base p-2">
|
<Card.Root class="border-none shadow-md text-base p-2">
|
||||||
<Card.Header class="p-0">
|
<Card.Header class="p-0">
|
||||||
<Card.Title class="text-md"></Card.Title>
|
<Card.Title class="text-md"></Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex flex-col p-0 text-xs gap-1">
|
<Card.Content class="flex flex-col p-0 text-xs gap-1">
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<Compass size="14" />
|
<Compass size="14" />
|
||||||
{trackpoint.item.getLatitude().toFixed(6)}° {trackpoint.item
|
{trackpoint.item.getLatitude().toFixed(6)}° {trackpoint.item
|
||||||
.getLongitude()
|
.getLongitude()
|
||||||
.toFixed(6)}°
|
.toFixed(6)}°
|
||||||
</div>
|
</div>
|
||||||
{#if trackpoint.item.ele !== undefined}
|
{#if trackpoint.item.ele !== undefined}
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<Mountain size="14" />
|
<Mountain size="14" />
|
||||||
<WithUnits value={trackpoint.item.ele} type="elevation" />
|
<WithUnits value={trackpoint.item.ele} type="elevation" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if trackpoint.item.time}
|
{#if trackpoint.item.time}
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<Timer size="14" />
|
<Timer size="14" />
|
||||||
{df.format(trackpoint.item.time)}
|
{df.format(trackpoint.item.time)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
class="w-full px-2 py-1 h-6 justify-start mt-0.5"
|
class="w-full px-2 py-1 h-6 justify-start mt-0.5"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`${trackpoint.item.getLatitude().toFixed(6)}, ${trackpoint.item.getLongitude().toFixed(6)}`
|
`${trackpoint.item.getLatitude().toFixed(6)}, ${trackpoint.item.getLongitude().toFixed(6)}`
|
||||||
);
|
);
|
||||||
trackpoint.hide?.();
|
trackpoint.hide?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClipboardCopy size="16" class="mr-1" />
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
{$_('menu.copy_coordinates')}
|
{$_('menu.copy_coordinates')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@ -1,102 +1,104 @@
|
|||||||
<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 Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import { deleteWaypoint } from './GPXLayerPopup';
|
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 { 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 'svelte-i18n';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import type { Waypoint } from 'gpx';
|
import type { Waypoint } from 'gpx';
|
||||||
import type { PopupItem } from '$lib/components/MapPopup';
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
|
||||||
export let waypoint: PopupItem<Waypoint>;
|
export let waypoint: PopupItem<Waypoint>;
|
||||||
|
|
||||||
$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
|
$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
|
||||||
|
|
||||||
function sanitize(text: string | undefined): string {
|
function sanitize(text: string | undefined): string {
|
||||||
if (text === undefined) {
|
if (text === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return sanitizeHtml(text, {
|
return sanitizeHtml(text, {
|
||||||
allowedTags: ['a', 'br', 'img'],
|
allowedTags: ['a', 'br', 'img'],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'target'],
|
a: ['href', 'target'],
|
||||||
img: ['src']
|
img: ['src'],
|
||||||
}
|
},
|
||||||
}).trim();
|
}).trim();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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">
|
||||||
{#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
|
{#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
|
||||||
<a href={waypoint.item.link.attributes.href} target="_blank">
|
<a href={waypoint.item.link.attributes.href} target="_blank">
|
||||||
{waypoint.item.name ?? waypoint.item.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}
|
||||||
{waypoint.item.name ?? $_('gpx.waypoint')}
|
{waypoint.item.name ?? $_('gpx.waypoint')}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex flex-col text-sm p-0">
|
<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>
|
||||||
{#if symbols[symbolKey].icon}
|
{#if symbols[symbolKey].icon}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={symbols[symbolKey].icon}
|
this={symbols[symbolKey].icon}
|
||||||
size="12"
|
size="12"
|
||||||
class="inline-block mb-0.5"
|
class="inline-block mb-0.5"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="w-4 inline-block" />
|
<span class="w-4 inline-block" />
|
||||||
{/if}
|
{/if}
|
||||||
{$_(`gpx.symbol.${symbolKey}`)}
|
{$_(`gpx.symbol.${symbolKey}`)}
|
||||||
</span>
|
</span>
|
||||||
<Dot size="16" />
|
<Dot size="16" />
|
||||||
{/if}
|
{/if}
|
||||||
{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item.getLongitude().toFixed(6)}°
|
{waypoint.item.getLatitude().toFixed(6)}° {waypoint.item
|
||||||
{#if waypoint.item.ele !== undefined}
|
.getLongitude()
|
||||||
<Dot size="16" />
|
.toFixed(6)}°
|
||||||
<WithUnits value={waypoint.item.ele} type="elevation" />
|
{#if waypoint.item.ele !== undefined}
|
||||||
{/if}
|
<Dot size="16" />
|
||||||
</div>
|
<WithUnits value={waypoint.item.ele} type="elevation" />
|
||||||
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
|
{/if}
|
||||||
{#if waypoint.item.desc}
|
</div>
|
||||||
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
|
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
|
||||||
{/if}
|
{#if waypoint.item.desc}
|
||||||
{#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
|
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
|
||||||
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
|
{/if}
|
||||||
{/if}
|
{#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
|
||||||
</ScrollArea>
|
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
|
||||||
{#if $currentTool === Tool.WAYPOINT}
|
{/if}
|
||||||
<Button
|
</ScrollArea>
|
||||||
class="mt-2 w-full px-2 py-1 h-8 justify-start"
|
{#if $currentTool === Tool.WAYPOINT}
|
||||||
variant="outline"
|
<Button
|
||||||
on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
|
class="mt-2 w-full px-2 py-1 h-8 justify-start"
|
||||||
>
|
variant="outline"
|
||||||
<Trash2 size="16" class="mr-1" />
|
on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
|
||||||
{$_('menu.delete')}
|
>
|
||||||
<Shortcut shift={true} click={true} />
|
<Trash2 size="16" class="mr-1" />
|
||||||
</Button>
|
{$_('menu.delete')}
|
||||||
{/if}
|
<Shortcut shift={true} click={true} />
|
||||||
</Card.Content>
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(a) {
|
div :global(a) {
|
||||||
@apply text-link;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(img) {
|
div :global(img) {
|
||||||
@apply my-0;
|
@apply my-0;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,422 +1,436 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import {
|
import {
|
||||||
CirclePlus,
|
CirclePlus,
|
||||||
CircleX,
|
CircleX,
|
||||||
Minus,
|
Minus,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
Move,
|
Move,
|
||||||
Map,
|
Map,
|
||||||
Layers2
|
Layers2,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
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';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { customBasemapUpdate } from './utils';
|
import { customBasemapUpdate } from './utils';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customLayers,
|
customLayers,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
previousOverlays,
|
previousOverlays,
|
||||||
customBasemapOrder,
|
customBasemapOrder,
|
||||||
customOverlayOrder
|
customOverlayOrder,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let name: string = '';
|
let name: string = '';
|
||||||
let tileUrls: string[] = [''];
|
let tileUrls: string[] = [''];
|
||||||
let maxZoom: number = 20;
|
let maxZoom: number = 20;
|
||||||
let layerType: 'basemap' | 'overlay' = 'basemap';
|
let layerType: 'basemap' | 'overlay' = 'basemap';
|
||||||
let resourceType: 'raster' | 'vector' = 'raster';
|
let resourceType: 'raster' | 'vector' = 'raster';
|
||||||
|
|
||||||
let basemapContainer: HTMLElement;
|
let basemapContainer: HTMLElement;
|
||||||
let overlayContainer: HTMLElement;
|
let overlayContainer: HTMLElement;
|
||||||
|
|
||||||
let basemapSortable: Sortable;
|
let basemapSortable: Sortable;
|
||||||
let overlaySortable: Sortable;
|
let overlaySortable: Sortable;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($customBasemapOrder.length === 0) {
|
if ($customBasemapOrder.length === 0) {
|
||||||
$customBasemapOrder = Object.keys($customLayers).filter(
|
$customBasemapOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'basemap'
|
(id) => $customLayers[id].layerType === 'basemap'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($customOverlayOrder.length === 0) {
|
if ($customOverlayOrder.length === 0) {
|
||||||
$customOverlayOrder = Object.keys($customLayers).filter(
|
$customOverlayOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'overlay'
|
(id) => $customLayers[id].layerType === 'overlay'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
basemapSortable = Sortable.create(basemapContainer, {
|
basemapSortable = Sortable.create(basemapContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customBasemapOrder = basemapSortable.toArray();
|
$customBasemapOrder = basemapSortable.toArray();
|
||||||
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
overlaySortable = Sortable.create(overlayContainer, {
|
overlaySortable = Sortable.create(overlayContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customOverlayOrder = overlaySortable.toArray();
|
$customOverlayOrder = overlaySortable.toArray();
|
||||||
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
basemapSortable.sort($customBasemapOrder);
|
basemapSortable.sort($customBasemapOrder);
|
||||||
overlaySortable.sort($customOverlayOrder);
|
overlaySortable.sort($customOverlayOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
basemapSortable.destroy();
|
basemapSortable.destroy();
|
||||||
overlaySortable.destroy();
|
overlaySortable.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (tileUrls[0].length > 0) {
|
$: if (tileUrls[0].length > 0) {
|
||||||
if (
|
if (
|
||||||
tileUrls[0].includes('.json') ||
|
tileUrls[0].includes('.json') ||
|
||||||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
||||||
) {
|
) {
|
||||||
resourceType = 'vector';
|
resourceType = 'vector';
|
||||||
} else {
|
} else {
|
||||||
resourceType = 'raster';
|
resourceType = 'raster';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLayer() {
|
function createLayer() {
|
||||||
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
||||||
deleteLayer(selectedLayerId);
|
deleteLayer(selectedLayerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof maxZoom === 'string') {
|
if (typeof maxZoom === 'string') {
|
||||||
maxZoom = parseInt(maxZoom);
|
maxZoom = parseInt(maxZoom);
|
||||||
}
|
}
|
||||||
let is512 = tileUrls.some((url) => url.includes('512'));
|
let is512 = tileUrls.some((url) => url.includes('512'));
|
||||||
|
|
||||||
let layerId = selectedLayerId ?? getLayerId();
|
let layerId = selectedLayerId ?? getLayerId();
|
||||||
let layer: CustomLayer = {
|
let layer: CustomLayer = {
|
||||||
id: layerId,
|
id: layerId,
|
||||||
name: name,
|
name: name,
|
||||||
tileUrls: tileUrls.map((url) => decodeURI(url.trim())),
|
tileUrls: tileUrls.map((url) => decodeURI(url.trim())),
|
||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
layerType: layerType,
|
layerType: layerType,
|
||||||
resourceType: resourceType,
|
resourceType: resourceType,
|
||||||
value: ''
|
value: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resourceType === 'vector') {
|
if (resourceType === 'vector') {
|
||||||
layer.value = layer.tileUrls[0];
|
layer.value = layer.tileUrls[0];
|
||||||
} else {
|
} else {
|
||||||
layer.value = {
|
layer.value = {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
[layerId]: {
|
[layerId]: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: layer.tileUrls,
|
tiles: layer.tileUrls,
|
||||||
tileSize: is512 ? 512 : 256,
|
tileSize: is512 ? 512 : 256,
|
||||||
maxzoom: maxZoom
|
maxzoom: maxZoom,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: layerId,
|
id: layerId,
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: layerId
|
source: layerId,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
$customLayers[layerId] = layer;
|
$customLayers[layerId] = layer;
|
||||||
addLayer(layerId);
|
addLayer(layerId);
|
||||||
selectedLayerId = undefined;
|
selectedLayerId = undefined;
|
||||||
setDataFromSelectedLayer();
|
setDataFromSelectedLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLayerId() {
|
function getLayerId() {
|
||||||
for (let id = 0; ; id++) {
|
for (let id = 0; ; id++) {
|
||||||
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
||||||
return `custom-${id}`;
|
return `custom-${id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLayer(layerId: string) {
|
function addLayer(layerId: string) {
|
||||||
if (layerType === 'basemap') {
|
if (layerType === 'basemap') {
|
||||||
selectedBasemapTree.update(($tree) => {
|
selectedBasemapTree.update(($tree) => {
|
||||||
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
||||||
$tree.basemaps['custom'] = {};
|
$tree.basemaps['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.basemaps['custom'][layerId] = true;
|
$tree.basemaps['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($currentBasemap === layerId) {
|
if ($currentBasemap === layerId) {
|
||||||
$customBasemapUpdate++;
|
$customBasemapUpdate++;
|
||||||
} else {
|
} else {
|
||||||
$currentBasemap = layerId;
|
$currentBasemap = layerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$customBasemapOrder.includes(layerId)) {
|
if (!$customBasemapOrder.includes(layerId)) {
|
||||||
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedOverlayTree.update(($tree) => {
|
selectedOverlayTree.update(($tree) => {
|
||||||
if (!$tree.overlays.hasOwnProperty('custom')) {
|
if (!$tree.overlays.hasOwnProperty('custom')) {
|
||||||
$tree.overlays['custom'] = {};
|
$tree.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.overlays['custom'][layerId] = true;
|
$tree.overlays['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$currentOverlays.overlays['custom'] &&
|
$currentOverlays.overlays['custom'] &&
|
||||||
$currentOverlays.overlays['custom'][layerId] &&
|
$currentOverlays.overlays['custom'][layerId] &&
|
||||||
$map
|
$map
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
$map.removeImport(layerId);
|
$map.removeImport(layerId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
||||||
$currentOverlays.overlays['custom'] = {};
|
$currentOverlays.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$currentOverlays.overlays['custom'][layerId] = true;
|
$currentOverlays.overlays['custom'][layerId] = true;
|
||||||
|
|
||||||
if (!$customOverlayOrder.includes(layerId)) {
|
if (!$customOverlayOrder.includes(layerId)) {
|
||||||
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryDeleteLayer(node: any, id: string): any {
|
function tryDeleteLayer(node: any, id: string): any {
|
||||||
if (node.hasOwnProperty(id)) {
|
if (node.hasOwnProperty(id)) {
|
||||||
delete node[id];
|
delete node[id];
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteLayer(layerId: string) {
|
function deleteLayer(layerId: string) {
|
||||||
let layer = $customLayers[layerId];
|
let layer = $customLayers[layerId];
|
||||||
if (layer.layerType === 'basemap') {
|
if (layer.layerType === 'basemap') {
|
||||||
if (layerId === $currentBasemap) {
|
if (layerId === $currentBasemap) {
|
||||||
$currentBasemap = defaultBasemap;
|
$currentBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
if (layerId === $previousBasemap) {
|
if (layerId === $previousBasemap) {
|
||||||
$previousBasemap = defaultBasemap;
|
$previousBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
||||||
$selectedBasemapTree.basemaps['custom'],
|
$selectedBasemapTree.basemaps['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
||||||
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
|
$selectedBasemapTree.basemaps = tryDeleteLayer(
|
||||||
}
|
$selectedBasemapTree.basemaps,
|
||||||
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
} else {
|
);
|
||||||
$currentOverlays.overlays['custom'][layerId] = false;
|
}
|
||||||
if ($previousOverlays.overlays['custom']) {
|
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
||||||
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
} else {
|
||||||
$previousOverlays.overlays['custom'],
|
$currentOverlays.overlays['custom'][layerId] = false;
|
||||||
layerId
|
if ($previousOverlays.overlays['custom']) {
|
||||||
);
|
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
||||||
}
|
$previousOverlays.overlays['custom'],
|
||||||
|
layerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
||||||
$selectedOverlayTree.overlays['custom'],
|
$selectedOverlayTree.overlays['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
||||||
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
|
$selectedOverlayTree.overlays = tryDeleteLayer(
|
||||||
}
|
$selectedOverlayTree.overlays,
|
||||||
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$currentOverlays.overlays['custom'] &&
|
$currentOverlays.overlays['custom'] &&
|
||||||
$currentOverlays.overlays['custom'][layerId] &&
|
$currentOverlays.overlays['custom'][layerId] &&
|
||||||
$map
|
$map
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
$map.removeImport(layerId);
|
$map.removeImport(layerId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$customLayers = tryDeleteLayer($customLayers, layerId);
|
$customLayers = tryDeleteLayer($customLayers, layerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedLayerId: string | undefined = undefined;
|
let selectedLayerId: string | undefined = undefined;
|
||||||
|
|
||||||
function setDataFromSelectedLayer() {
|
function setDataFromSelectedLayer() {
|
||||||
if (selectedLayerId) {
|
if (selectedLayerId) {
|
||||||
const layer = $customLayers[selectedLayerId];
|
const layer = $customLayers[selectedLayerId];
|
||||||
name = layer.name;
|
name = layer.name;
|
||||||
tileUrls = layer.tileUrls;
|
tileUrls = layer.tileUrls;
|
||||||
maxZoom = layer.maxZoom;
|
maxZoom = layer.maxZoom;
|
||||||
layerType = layer.layerType;
|
layerType = layer.layerType;
|
||||||
resourceType = layer.resourceType;
|
resourceType = layer.resourceType;
|
||||||
} else {
|
} else {
|
||||||
name = '';
|
name = '';
|
||||||
tileUrls = [''];
|
tileUrls = [''];
|
||||||
maxZoom = 20;
|
maxZoom = 20;
|
||||||
layerType = 'basemap';
|
layerType = 'basemap';
|
||||||
resourceType = 'raster';
|
resourceType = 'raster';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedLayerId, setDataFromSelectedLayer();
|
$: selectedLayerId, setDataFromSelectedLayer();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if $customBasemapOrder.length > 0}
|
{#if $customBasemapOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Map size="16" />
|
<Map size="16" />
|
||||||
{$_('layers.label.basemaps')}
|
{$_('layers.label.basemaps')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={basemapContainer}
|
bind:this={basemapContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customBasemapOrder as id (id)}
|
{#each $customBasemapOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if $customOverlayOrder.length > 0}
|
{#if $customOverlayOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Layers2 size="16" />
|
<Layers2 size="16" />
|
||||||
{$_('layers.label.overlays')}
|
{$_('layers.label.overlays')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={overlayContainer}
|
bind:this={overlayContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customOverlayOrder as id (id)}
|
{#each $customOverlayOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header class="p-3">
|
<Card.Header class="p-3">
|
||||||
<Card.Title class="text-base">
|
<Card.Title class="text-base">
|
||||||
{#if selectedLayerId}
|
{#if selectedLayerId}
|
||||||
{$_('layers.custom_layers.edit')}
|
{$_('layers.custom_layers.edit')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('layers.custom_layers.new')}
|
{$_('layers.custom_layers.new')}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="p-3 pt-0">
|
<Card.Content class="p-3 pt-0">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="h-8" />
|
<Input bind:value={name} id="name" class="h-8" />
|
||||||
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
||||||
{#each tileUrls as url, i}
|
{#each tileUrls as url, i}
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
bind:value={tileUrls[i]}
|
bind:value={tileUrls[i]}
|
||||||
id="url"
|
id="url"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
placeholder={$_('layers.custom_layers.url_placeholder')}
|
placeholder={$_('layers.custom_layers.url_placeholder')}
|
||||||
/>
|
/>
|
||||||
{#if tileUrls.length > 1}
|
{#if tileUrls.length > 1}
|
||||||
<Button
|
<Button
|
||||||
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
|
on:click={() =>
|
||||||
variant="outline"
|
(tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Minus size="16" />
|
>
|
||||||
</Button>
|
<Minus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
{#if i === tileUrls.length - 1}
|
{/if}
|
||||||
<Button
|
{#if i === tileUrls.length - 1}
|
||||||
on:click={() => (tileUrls = [...tileUrls, ''])}
|
<Button
|
||||||
variant="outline"
|
on:click={() => (tileUrls = [...tileUrls, ''])}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Plus size="16" />
|
>
|
||||||
</Button>
|
<Plus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
{#if resourceType === 'raster'}
|
{/each}
|
||||||
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
{#if resourceType === 'raster'}
|
||||||
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
|
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
||||||
{/if}
|
<Input
|
||||||
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
type="number"
|
||||||
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
bind:value={maxZoom}
|
||||||
<div class="flex items-center space-x-2">
|
id="maxZoom"
|
||||||
<RadioGroup.Item value="basemap" id="basemap" />
|
min={0}
|
||||||
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
max={22}
|
||||||
</div>
|
class="h-8"
|
||||||
<div class="flex items-center space-x-2">
|
/>
|
||||||
<RadioGroup.Item value="overlay" id="overlay" />
|
{/if}
|
||||||
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
||||||
</div>
|
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
||||||
</RadioGroup.Root>
|
<div class="flex items-center space-x-2">
|
||||||
{#if selectedLayerId}
|
<RadioGroup.Item value="basemap" id="basemap" />
|
||||||
<div class="mt-2 flex flex-row gap-2">
|
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
||||||
<Button variant="outline" on:click={createLayer} class="grow">
|
</div>
|
||||||
<Save size="16" class="mr-1" />
|
<div class="flex items-center space-x-2">
|
||||||
{$_('layers.custom_layers.update')}
|
<RadioGroup.Item value="overlay" id="overlay" />
|
||||||
</Button>
|
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
</div>
|
||||||
<CircleX size="16" />
|
</RadioGroup.Root>
|
||||||
</Button>
|
{#if selectedLayerId}
|
||||||
</div>
|
<div class="mt-2 flex flex-row gap-2">
|
||||||
{:else}
|
<Button variant="outline" on:click={createLayer} class="grow">
|
||||||
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
<Save size="16" class="mr-1" />
|
||||||
<CirclePlus size="16" class="mr-1" />
|
{$_('layers.custom_layers.update')}
|
||||||
{$_('layers.custom_layers.create')}
|
</Button>
|
||||||
</Button>
|
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
||||||
{/if}
|
<CircleX size="16" />
|
||||||
</fieldset>
|
</Button>
|
||||||
</Card.Content>
|
</div>
|
||||||
</Card.Root>
|
{:else}
|
||||||
|
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
||||||
|
<CirclePlus size="16" class="mr-1" />
|
||||||
|
{$_('layers.custom_layers.create')}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,222 +1,222 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||||
import LayerTree from './LayerTree.svelte';
|
import LayerTree from './LayerTree.svelte';
|
||||||
|
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
|
||||||
import { Layers } from 'lucide-svelte';
|
import { Layers } from 'lucide-svelte';
|
||||||
|
|
||||||
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
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';
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let overpassLayer: OverpassLayer;
|
let overpassLayer: OverpassLayer;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
currentOverpassQueries,
|
currentOverpassQueries,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
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');
|
||||||
} else {
|
} else {
|
||||||
$map.addImport(
|
$map.addImport(
|
||||||
{
|
{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
data: basemap
|
data: basemap,
|
||||||
},
|
},
|
||||||
'overlays'
|
'overlays'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
||||||
setStyle();
|
setStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOverlay(id: string) {
|
function addOverlay(id: string) {
|
||||||
try {
|
try {
|
||||||
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
if (typeof overlay === 'string') {
|
if (typeof overlay === 'string') {
|
||||||
$map.addImport({ id, url: overlay });
|
$map.addImport({ id, url: overlay });
|
||||||
} else {
|
} else {
|
||||||
if ($opacities.hasOwnProperty(id)) {
|
if ($opacities.hasOwnProperty(id)) {
|
||||||
overlay = {
|
overlay = {
|
||||||
...overlay,
|
...overlay,
|
||||||
layers: overlay.layers.map((layer) => {
|
layers: overlay.layers.map((layer) => {
|
||||||
if (layer.type === 'raster') {
|
if (layer.type === 'raster') {
|
||||||
if (!layer.paint) {
|
if (!layer.paint) {
|
||||||
layer.paint = {};
|
layer.paint = {};
|
||||||
}
|
}
|
||||||
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) {
|
||||||
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOverlays() {
|
function updateOverlays() {
|
||||||
if ($map && $currentOverlays && $opacities) {
|
if ($map && $currentOverlays && $opacities) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
try {
|
try {
|
||||||
let activeOverlays = $map.getStyle().imports.reduce((acc, i) => {
|
let activeOverlays = $map.getStyle().imports.reduce((acc, i) => {
|
||||||
if (!['basemap', 'overlays', 'glyphs-and-sprite'].includes(i.id)) {
|
if (!['basemap', 'overlays', 'glyphs-and-sprite'].includes(i.id)) {
|
||||||
acc[i.id] = i;
|
acc[i.id] = i;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
|
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
|
||||||
toRemove.forEach((id) => {
|
toRemove.forEach((id) => {
|
||||||
$map.removeImport(id);
|
$map.removeImport(id);
|
||||||
});
|
});
|
||||||
let toAdd = Object.entries(overlayLayers)
|
let toAdd = Object.entries(overlayLayers)
|
||||||
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
|
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
toAdd.forEach((id) => {
|
toAdd.forEach((id) => {
|
||||||
addOverlay(id);
|
addOverlay(id);
|
||||||
});
|
});
|
||||||
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && $currentOverlays && $opacities) {
|
$: if ($map && $currentOverlays && $opacities) {
|
||||||
updateOverlays();
|
updateOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
if (overpassLayer) {
|
if (overpassLayer) {
|
||||||
overpassLayer.remove();
|
overpassLayer.remove();
|
||||||
}
|
}
|
||||||
overpassLayer = new OverpassLayer($map);
|
overpassLayer = new OverpassLayer($map);
|
||||||
overpassLayer.add();
|
overpassLayer.add();
|
||||||
$map.on('style.import.load', updateOverlays);
|
$map.on('style.import.load', updateOverlays);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedBasemap = writable(get(currentBasemap));
|
let selectedBasemap = writable(get(currentBasemap));
|
||||||
selectedBasemap.subscribe((value) => {
|
selectedBasemap.subscribe((value) => {
|
||||||
// Updates coming from radio buttons
|
// Updates coming from radio buttons
|
||||||
if (value !== get(currentBasemap)) {
|
if (value !== get(currentBasemap)) {
|
||||||
previousBasemap.set(get(currentBasemap));
|
previousBasemap.set(get(currentBasemap));
|
||||||
currentBasemap.set(value);
|
currentBasemap.set(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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)) {
|
if (value !== get(selectedBasemap)) {
|
||||||
selectedBasemap.set(value);
|
selectedBasemap.set(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
function openLayerControl() {
|
function openLayerControl() {
|
||||||
open = true;
|
open = true;
|
||||||
}
|
}
|
||||||
function closeLayerControl() {
|
function closeLayerControl() {
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
let cancelEvents = false;
|
let cancelEvents = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
|
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
on:mouseenter={openLayerControl}
|
on:mouseenter={openLayerControl}
|
||||||
on:mouseleave={closeLayerControl}
|
on:mouseleave={closeLayerControl}
|
||||||
on:pointerenter={() => {
|
on:pointerenter={() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
cancelEvents = true;
|
cancelEvents = true;
|
||||||
openLayerControl();
|
openLayerControl();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
cancelEvents = false;
|
cancelEvents = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
|
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
|
||||||
? 'opacity-0 w-0 h-0 delay-0'
|
? 'opacity-0 w-0 h-0 delay-0'
|
||||||
: 'w-[29px] h-[29px]'}"
|
: 'w-[29px] h-[29px]'}"
|
||||||
>
|
>
|
||||||
<Layers size="20" />
|
<Layers size="20" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
|
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
|
||||||
? 'grid-rows-[1fr] grid-cols-[1fr]'
|
? 'grid-rows-[1fr] grid-cols-[1fr]'
|
||||||
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
|
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
|
||||||
>
|
>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div class="h-fit">
|
<div class="h-fit">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedBasemapTree}
|
layerTree={$selectedBasemapTree}
|
||||||
name="basemaps"
|
name="basemaps"
|
||||||
bind:selected={$selectedBasemap}
|
bind:selected={$selectedBasemap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator class="w-full" />
|
<Separator class="w-full" />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
{#if $currentOverlays}
|
{#if $currentOverlays}
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedOverlayTree}
|
layerTree={$selectedOverlayTree}
|
||||||
name="overlays"
|
name="overlays"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$currentOverlays}
|
bind:checked={$currentOverlays}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Separator class="w-full" />
|
<Separator class="w-full" />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
{#if $currentOverpassQueries}
|
{#if $currentOverpassQueries}
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={$selectedOverpassTree}
|
layerTree={$selectedOverpassTree}
|
||||||
name="overpass"
|
name="overpass"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$currentOverpassQueries}
|
bind:checked={$currentOverpassQueries}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
if (open && !cancelEvents && !container.contains(e.target)) {
|
if (open && !cancelEvents && !container.contains(e.target)) {
|
||||||
closeLayerControl();
|
closeLayerControl();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,189 +1,197 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LayerTree from './LayerTree.svelte';
|
import LayerTree from './LayerTree.svelte';
|
||||||
|
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
import * as Sheet from '$lib/components/ui/sheet';
|
||||||
import * as Accordion from '$lib/components/ui/accordion';
|
import * as Accordion from '$lib/components/ui/accordion';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
basemapTree,
|
basemapTree,
|
||||||
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 'svelte-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';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
selectedOverpassTree,
|
selectedOverpassTree,
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities
|
opacities,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
export let open: boolean;
|
export let open: boolean;
|
||||||
let accordionValue: string | string[] | undefined = undefined;
|
let accordionValue: string | string[] | undefined = undefined;
|
||||||
|
|
||||||
let selectedOverlay = writable(undefined);
|
let selectedOverlay = writable(undefined);
|
||||||
let overlayOpacity = writable([1]);
|
let overlayOpacity = writable([1]);
|
||||||
|
|
||||||
function setOpacityFromSelection() {
|
function setOpacityFromSelection() {
|
||||||
if ($selectedOverlay) {
|
if ($selectedOverlay) {
|
||||||
let overlayId = $selectedOverlay.value;
|
let overlayId = $selectedOverlay.value;
|
||||||
if ($opacities.hasOwnProperty(overlayId)) {
|
if ($opacities.hasOwnProperty(overlayId)) {
|
||||||
$overlayOpacity = [$opacities[overlayId]];
|
$overlayOpacity = [$opacities[overlayId]];
|
||||||
} else {
|
} else {
|
||||||
$overlayOpacity = [1];
|
$overlayOpacity = [1];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$overlayOpacity = [1];
|
$overlayOpacity = [1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selectedBasemapTree && $currentBasemap) {
|
$: if ($selectedBasemapTree && $currentBasemap) {
|
||||||
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
||||||
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
||||||
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
||||||
}
|
}
|
||||||
$currentBasemap = defaultBasemap;
|
$currentBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selectedOverlayTree && $currentOverlays) {
|
$: if ($selectedOverlayTree && $currentOverlays) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
let toRemove = Object.entries(overlayLayers).filter(
|
let toRemove = Object.entries(overlayLayers).filter(
|
||||||
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
|
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
|
||||||
);
|
);
|
||||||
if (toRemove.length > 0) {
|
if (toRemove.length > 0) {
|
||||||
currentOverlays.update((tree) => {
|
currentOverlays.update((tree) => {
|
||||||
toRemove.forEach(([id]) => {
|
toRemove.forEach(([id]) => {
|
||||||
toggle(tree, id);
|
toggle(tree, id);
|
||||||
});
|
});
|
||||||
return tree;
|
return tree;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($selectedOverlay) {
|
$: if ($selectedOverlay) {
|
||||||
setOpacityFromSelection();
|
setOpacityFromSelection();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sheet.Root bind:open>
|
<Sheet.Root bind:open>
|
||||||
<Sheet.Trigger class="hidden" />
|
<Sheet.Trigger class="hidden" />
|
||||||
<Sheet.Content>
|
<Sheet.Content>
|
||||||
<Sheet.Header class="h-full">
|
<Sheet.Header class="h-full">
|
||||||
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
|
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
|
||||||
<ScrollArea class="w-[105%] min-h-full pr-4">
|
<ScrollArea class="w-[105%] min-h-full pr-4">
|
||||||
<Sheet.Description>
|
<Sheet.Description>
|
||||||
{$_('layers.settings_help')}
|
{$_('layers.settings_help')}
|
||||||
</Sheet.Description>
|
</Sheet.Description>
|
||||||
<Accordion.Root class="flex flex-col" bind:value={accordionValue}>
|
<Accordion.Root class="flex flex-col" bind:value={accordionValue}>
|
||||||
<Accordion.Item value="layer-selection" class="flex flex-col">
|
<Accordion.Item value="layer-selection" class="flex flex-col">
|
||||||
<Accordion.Trigger>{$_('layers.selection')}</Accordion.Trigger>
|
<Accordion.Trigger>{$_('layers.selection')}</Accordion.Trigger>
|
||||||
<Accordion.Content class="grow flex flex-col border rounded">
|
<Accordion.Content class="grow flex flex-col border rounded">
|
||||||
<div class="py-2 pl-1 pr-2">
|
<div class="py-2 pl-1 pr-2">
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={basemapTree}
|
layerTree={basemapTree}
|
||||||
name="basemapSettings"
|
name="basemapSettings"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$selectedBasemapTree}
|
bind:checked={$selectedBasemapTree}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div class="py-2 pl-1 pr-2">
|
<div class="py-2 pl-1 pr-2">
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={overlayTree}
|
layerTree={overlayTree}
|
||||||
name="overlaySettings"
|
name="overlaySettings"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$selectedOverlayTree}
|
bind:checked={$selectedOverlayTree}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div class="py-2 pl-1 pr-2">
|
<div class="py-2 pl-1 pr-2">
|
||||||
<LayerTree
|
<LayerTree
|
||||||
layerTree={overpassTree}
|
layerTree={overpassTree}
|
||||||
name="overpassSettings"
|
name="overpassSettings"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
bind:checked={$selectedOverpassTree}
|
bind:checked={$selectedOverpassTree}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
<Accordion.Item value="overlay-opacity">
|
<Accordion.Item value="overlay-opacity">
|
||||||
<Accordion.Trigger>{$_('layers.opacity')}</Accordion.Trigger>
|
<Accordion.Trigger>{$_('layers.opacity')}</Accordion.Trigger>
|
||||||
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
|
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
|
||||||
<div class="flex flex-row gap-6 items-center">
|
<div class="flex flex-row gap-6 items-center">
|
||||||
<Label>
|
<Label>
|
||||||
{$_('layers.custom_layers.overlay')}
|
{$_('layers.custom_layers.overlay')}
|
||||||
</Label>
|
</Label>
|
||||||
<Select.Root bind:selected={$selectedOverlay}>
|
<Select.Root bind:selected={$selectedOverlay}>
|
||||||
<Select.Trigger class="h-8 mr-1">
|
<Select.Trigger class="h-8 mr-1">
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<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}
|
||||||
{/if}
|
>{$_(`layers.label.${id}`)}</Select.Item
|
||||||
{/each}
|
>
|
||||||
{#each Object.entries($customLayers) as [id, layer]}
|
{/if}
|
||||||
{#if layer.layerType === 'overlay'}
|
{/each}
|
||||||
<Select.Item value={id}>{layer.name}</Select.Item>
|
{#each Object.entries($customLayers) as [id, layer]}
|
||||||
{/if}
|
{#if layer.layerType === 'overlay'}
|
||||||
{/each}
|
<Select.Item value={id}>{layer.name}</Select.Item>
|
||||||
</Select.Content>
|
{/if}
|
||||||
</Select.Root>
|
{/each}
|
||||||
</div>
|
</Select.Content>
|
||||||
<Label class="flex flex-row gap-6 items-center">
|
</Select.Root>
|
||||||
{$_('menu.style.opacity')}
|
</div>
|
||||||
<div class="p-2 pr-3 grow">
|
<Label class="flex flex-row gap-6 items-center">
|
||||||
<Slider
|
{$_('menu.style.opacity')}
|
||||||
bind:value={$overlayOpacity}
|
<div class="p-2 pr-3 grow">
|
||||||
min={0.1}
|
<Slider
|
||||||
max={1}
|
bind:value={$overlayOpacity}
|
||||||
step={0.1}
|
min={0.1}
|
||||||
disabled={$selectedOverlay === undefined}
|
max={1}
|
||||||
onValueChange={(value) => {
|
step={0.1}
|
||||||
if ($selectedOverlay) {
|
disabled={$selectedOverlay === undefined}
|
||||||
if ($map && isSelected($currentOverlays, $selectedOverlay.value)) {
|
onValueChange={(value) => {
|
||||||
try {
|
if ($selectedOverlay) {
|
||||||
$map.removeImport($selectedOverlay.value);
|
if (
|
||||||
} catch (e) {
|
$map &&
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
isSelected(
|
||||||
}
|
$currentOverlays,
|
||||||
}
|
$selectedOverlay.value
|
||||||
$opacities[$selectedOverlay.value] = value[0];
|
)
|
||||||
}
|
) {
|
||||||
}}
|
try {
|
||||||
/>
|
$map.removeImport($selectedOverlay.value);
|
||||||
</div>
|
} catch (e) {
|
||||||
</Label>
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
</Accordion.Content>
|
}
|
||||||
</Accordion.Item>
|
}
|
||||||
<Accordion.Item value="custom-layers">
|
$opacities[$selectedOverlay.value] = value[0];
|
||||||
<Accordion.Trigger>{$_('layers.custom_layers.title')}</Accordion.Trigger>
|
}
|
||||||
<Accordion.Content>
|
}}
|
||||||
<ScrollArea>
|
/>
|
||||||
<CustomLayers />
|
</div>
|
||||||
</ScrollArea>
|
</Label>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion.Root>
|
<Accordion.Item value="custom-layers">
|
||||||
</ScrollArea>
|
<Accordion.Trigger>{$_('layers.custom_layers.title')}</Accordion.Trigger>
|
||||||
</Sheet.Header>
|
<Accordion.Content>
|
||||||
</Sheet.Content>
|
<ScrollArea>
|
||||||
|
<CustomLayers />
|
||||||
|
</ScrollArea>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion.Root>
|
||||||
|
</ScrollArea>
|
||||||
|
</Sheet.Header>
|
||||||
|
</Sheet.Content>
|
||||||
</Sheet.Root>
|
</Sheet.Root>
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LayerTreeNode from './LayerTreeNode.svelte';
|
import LayerTreeNode from './LayerTreeNode.svelte';
|
||||||
import { type LayerTreeType } from '$lib/assets/layers';
|
import { type LayerTreeType } from '$lib/assets/layers';
|
||||||
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
|
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
|
||||||
|
|
||||||
export let layerTree: LayerTreeType;
|
export let layerTree: LayerTreeType;
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let selected: string | undefined = undefined;
|
export let selected: string | undefined = undefined;
|
||||||
export let multiple: boolean = false;
|
export let multiple: boolean = false;
|
||||||
|
|
||||||
export let checked: LayerTreeType = {};
|
export let checked: LayerTreeType = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<fieldset class="min-w-64 mb-1">
|
<fieldset class="min-w-64 mb-1">
|
||||||
<CollapsibleTree nohover={true}>
|
<CollapsibleTree nohover={true}>
|
||||||
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
|
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
|
||||||
</CollapsibleTree>
|
</CollapsibleTree>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,86 +1,98 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import CollapsibleTreeNode from '../collapsible-tree/CollapsibleTreeNode.svelte';
|
import CollapsibleTreeNode from '../collapsible-tree/CollapsibleTreeNode.svelte';
|
||||||
|
|
||||||
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 'svelte-i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { beforeUpdate } from 'svelte';
|
import { beforeUpdate } from 'svelte';
|
||||||
|
|
||||||
export let name: string;
|
export let name: string;
|
||||||
export let node: LayerTreeType;
|
export let node: LayerTreeType;
|
||||||
export let selected: string | undefined = undefined;
|
export let selected: string | undefined = undefined;
|
||||||
export let multiple: boolean = false;
|
export let multiple: boolean = false;
|
||||||
|
|
||||||
export let checked: LayerTreeType;
|
export let checked: LayerTreeType;
|
||||||
|
|
||||||
const { customLayers } = settings;
|
const { customLayers } = settings;
|
||||||
|
|
||||||
beforeUpdate(() => {
|
beforeUpdate(() => {
|
||||||
if (checked !== undefined) {
|
if (checked !== undefined) {
|
||||||
Object.keys(node).forEach((id) => {
|
Object.keys(node).forEach((id) => {
|
||||||
if (!checked.hasOwnProperty(id)) {
|
if (!checked.hasOwnProperty(id)) {
|
||||||
if (typeof node[id] == 'boolean') {
|
if (typeof node[id] == 'boolean') {
|
||||||
checked[id] = false;
|
checked[id] = false;
|
||||||
} else {
|
} else {
|
||||||
checked[id] = {};
|
checked[id] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-[3px]">
|
<div class="flex flex-col gap-[3px]">
|
||||||
{#each Object.keys(node) as id}
|
{#each Object.keys(node) as id}
|
||||||
{#if typeof node[id] == 'boolean'}
|
{#if typeof node[id] == 'boolean'}
|
||||||
{#if node[id]}
|
{#if node[id]}
|
||||||
<div class="flex flex-row items-center gap-2 first:mt-0.5 h-4">
|
<div class="flex flex-row items-center gap-2 first:mt-0.5 h-4">
|
||||||
{#if multiple}
|
{#if multiple}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="{name}-{id}"
|
id="{name}-{id}"
|
||||||
{name}
|
{name}
|
||||||
value={id}
|
value={id}
|
||||||
bind:checked={checked[id]}
|
bind:checked={checked[id]}
|
||||||
class="scale-90"
|
class="scale-90"
|
||||||
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
|
||||||
{/if}
|
id="{name}-{id}"
|
||||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
type="radio"
|
||||||
{#if $customLayers.hasOwnProperty(id)}
|
{name}
|
||||||
{$customLayers[id].name}
|
value={id}
|
||||||
{:else}
|
bind:group={selected}
|
||||||
{$_(`layers.label.${id}`)}
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Label>
|
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||||
</div>
|
{#if $customLayers.hasOwnProperty(id)}
|
||||||
{/if}
|
{$customLayers[id].name}
|
||||||
{:else if anySelectedLayer(node[id])}
|
{:else}
|
||||||
<CollapsibleTreeNode {id}>
|
{$_(`layers.label.${id}`)}
|
||||||
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
|
{/if}
|
||||||
<div slot="content">
|
</Label>
|
||||||
<svelte:self node={node[id]} {name} bind:selected {multiple} bind:checked={checked[id]} />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</CollapsibleTreeNode>
|
{:else if anySelectedLayer(node[id])}
|
||||||
{/if}
|
<CollapsibleTreeNode {id}>
|
||||||
{/each}
|
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
|
||||||
|
<div slot="content">
|
||||||
|
<svelte:self
|
||||||
|
node={node[id]}
|
||||||
|
{name}
|
||||||
|
bind:selected
|
||||||
|
{multiple}
|
||||||
|
bind:checked={checked[id]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTreeNode>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(input[type='radio']) {
|
div :global(input[type='radio']) {
|
||||||
@apply appearance-none;
|
@apply appearance-none;
|
||||||
@apply w-4 h-4;
|
@apply w-4 h-4;
|
||||||
@apply border-[1.5px] border-primary;
|
@apply border-[1.5px] border-primary;
|
||||||
@apply rounded-full;
|
@apply rounded-full;
|
||||||
@apply ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
|
@apply ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
@apply checked:bg-primary;
|
@apply checked:bg-primary;
|
||||||
@apply checked:bg-clip-content;
|
@apply checked:bg-clip-content;
|
||||||
@apply checked:p-0.5;
|
@apply checked:p-0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import SphericalMercator from "@mapbox/sphericalmercator";
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
import { getLayers } from "./utils";
|
import { getLayers } from './utils';
|
||||||
import { get, writable } from "svelte/store";
|
import { get, writable } from 'svelte/store';
|
||||||
import { liveQuery } from "dexie";
|
import { liveQuery } from 'dexie';
|
||||||
import { db, settings } from "$lib/db";
|
import { db, settings } from '$lib/db';
|
||||||
import { overpassQueryData } from "$lib/assets/layers";
|
import { overpassQueryData } from '$lib/assets/layers';
|
||||||
import { MapPopup } from "$lib/components/MapPopup";
|
import { MapPopup } from '$lib/components/MapPopup';
|
||||||
|
|
||||||
const {
|
const { currentOverpassQueries } = settings;
|
||||||
currentOverpassQueries
|
|
||||||
} = settings;
|
|
||||||
|
|
||||||
const mercator = new SphericalMercator({
|
const mercator = new SphericalMercator({
|
||||||
size: 256,
|
size: 256,
|
||||||
@ -29,7 +27,7 @@ export class OverpassLayer {
|
|||||||
popup: MapPopup;
|
popup: MapPopup;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map();
|
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
|
||||||
|
|
||||||
unsubscribes: (() => void)[] = [];
|
unsubscribes: (() => void)[] = [];
|
||||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||||
@ -50,10 +48,12 @@ export class OverpassLayer {
|
|||||||
this.map.on('moveend', this.queryIfNeededBinded);
|
this.map.on('moveend', this.queryIfNeededBinded);
|
||||||
this.map.on('style.import.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
this.unsubscribes.push(
|
||||||
this.updateBinded();
|
currentOverpassQueries.subscribe(() => {
|
||||||
this.queryIfNeededBinded();
|
this.updateBinded();
|
||||||
}));
|
this.queryIfNeededBinded();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@ -126,8 +126,8 @@ export class OverpassLayer {
|
|||||||
this.popup.setItem({
|
this.popup.setItem({
|
||||||
item: {
|
item: {
|
||||||
...e.features[0].properties,
|
...e.features[0].properties,
|
||||||
sym: overpassQueryData[e.features[0].properties.query].symbol ?? ''
|
sym: overpassQueryData[e.features[0].properties.query].symbol ?? '',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +146,23 @@ 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]')
|
||||||
if (missingQueries.length > 0) {
|
.equals([x, y])
|
||||||
this.queryTile(x, y, missingQueries);
|
.toArray()
|
||||||
}
|
.then((querytiles) => {
|
||||||
});
|
let missingQueries = queries.filter(
|
||||||
|
(query) =>
|
||||||
|
!querytiles.some(
|
||||||
|
(querytile) =>
|
||||||
|
querytile.query === query &&
|
||||||
|
time - querytile.time < this.expirationTime
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (missingQueries.length > 0) {
|
||||||
|
this.queryTile(x, y, missingQueries);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,13 +176,16 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
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(
|
||||||
if (response.ok) {
|
(response) => {
|
||||||
return response.json();
|
if (response.ok) {
|
||||||
}
|
return response.json();
|
||||||
this.currentQueries.delete(`${x},${y}`);
|
}
|
||||||
return Promise.reject();
|
this.currentQueries.delete(`${x},${y}`);
|
||||||
}, () => (this.currentQueries.delete(`${x},${y}`)))
|
return Promise.reject();
|
||||||
|
},
|
||||||
|
() => this.currentQueries.delete(`${x},${y}`)
|
||||||
|
)
|
||||||
.then((data) => this.storeOverpassData(x, y, queries, data))
|
.then((data) => this.storeOverpassData(x, y, queries, data))
|
||||||
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
||||||
}
|
}
|
||||||
@ -179,7 +193,7 @@ export class OverpassLayer {
|
|||||||
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
||||||
let time = Date.now();
|
let time = Date.now();
|
||||||
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
||||||
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = [];
|
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
|
||||||
|
|
||||||
if (data.elements === undefined) {
|
if (data.elements === undefined) {
|
||||||
return;
|
return;
|
||||||
@ -195,7 +209,9 @@ export class OverpassLayer {
|
|||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: element.center ? [element.center.lon, element.center.lat] : [element.lon, element.lat],
|
coordinates: element.center
|
||||||
|
? [element.center.lon, element.center.lat]
|
||||||
|
: [element.lon, element.lat],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
id: element.id,
|
id: element.id,
|
||||||
@ -203,9 +219,9 @@ export class OverpassLayer {
|
|||||||
lon: element.center ? element.center.lon : element.lon,
|
lon: element.center ? element.center.lon : element.lon,
|
||||||
query: query,
|
query: query,
|
||||||
icon: `overpass-${query}`,
|
icon: `overpass-${query}`,
|
||||||
tags: element.tags
|
tags: element.tags,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,11 +244,13 @@ export class OverpassLayer {
|
|||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
this.map.addImage(`overpass-${query}`, icon);
|
this.map.addImage(`overpass-${query}`, icon);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
// Lucide icons are SVG files with a 24x24 viewBox
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||||
icon.src = 'data:image/svg+xml,' + encodeURIComponent(`
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
@ -264,9 +282,14 @@ function getQuery(query: string) {
|
|||||||
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
||||||
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
|
||||||
if (arrayEntry !== undefined) {
|
if (arrayEntry !== undefined) {
|
||||||
return arrayEntry[1].map((val) => `nwr${Object.entries(tags)
|
return arrayEntry[1]
|
||||||
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
.map(
|
||||||
.join('')};`).join('');
|
(val) =>
|
||||||
|
`nwr${Object.entries(tags)
|
||||||
|
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
|
||||||
|
.join('')};`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
} else {
|
} else {
|
||||||
return `nwr${Object.entries(tags)
|
return `nwr${Object.entries(tags)
|
||||||
.map(([tag, value]) => `[${tag}=${value}]`)
|
.map(([tag, value]) => `[${tag}=${value}]`)
|
||||||
@ -283,8 +306,9 @@ function belongsToQuery(element: any, query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
||||||
return Object.entries(tags)
|
return Object.entries(tags).every(([tag, value]) =>
|
||||||
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value);
|
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentQueries() {
|
function getCurrentQueries() {
|
||||||
@ -293,5 +317,7 @@ function getCurrentQueries() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query);
|
return Object.entries(getLayers(currentQueries))
|
||||||
}
|
.filter(([_, selected]) => selected)
|
||||||
|
.map(([query, _]) => query);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,95 +1,95 @@
|
|||||||
<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 { 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 { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import type { PopupItem } from '$lib/components/MapPopup';
|
import type { PopupItem } from '$lib/components/MapPopup';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
|
|
||||||
export let poi: PopupItem<any>;
|
export let poi: PopupItem<any>;
|
||||||
|
|
||||||
let tags = {};
|
let tags = {};
|
||||||
let name = '';
|
let name = '';
|
||||||
$: if (poi) {
|
$: if (poi) {
|
||||||
tags = JSON.parse(poi.item.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.${poi.item.query}`);
|
name = $_(`layers.label.${poi.item.query}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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">
|
||||||
<div class="flex flex-row gap-3">
|
<div class="flex flex-row gap-3">
|
||||||
<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">
|
||||||
{poi.item.lat.toFixed(6)}° {poi.item.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={poi.item.id}"
|
href="https://www.openstreetmap.org/edit?editor=id&node={poi.item.id}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<PencilLine size="16" />
|
<PencilLine size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</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">
|
<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]">
|
<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}
|
||||||
<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.startsWith('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>
|
||||||
{:else if key === 'email' || key === 'contact:email'}
|
{:else if key === 'email' || key === 'contact:email'}
|
||||||
<a href={'mailto:' + value} class="text-link underline">{value}</a>
|
<a href={'mailto:' + value} class="text-link underline">{value}</a>
|
||||||
{:else}
|
{:else}
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</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={() => {
|
||||||
let desc = Object.entries(tags)
|
let desc = Object.entries(tags)
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
dbUtils.addOrUpdateWaypoint({
|
dbUtils.addOrUpdateWaypoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: poi.item.lat,
|
lat: poi.item.lat,
|
||||||
lon: poi.item.lon
|
lon: poi.item.lon,
|
||||||
},
|
},
|
||||||
name: name,
|
name: name,
|
||||||
desc: desc,
|
desc: desc,
|
||||||
cmt: desc,
|
cmt: desc,
|
||||||
sym: poi.item.sym
|
sym: poi.item.sym,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
@ -1,24 +1,29 @@
|
|||||||
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 (node[id]) {
|
if (typeof node[id] == 'boolean') {
|
||||||
return true;
|
if (node[id]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (anySelectedLayer(node[id])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return false;
|
||||||
if (anySelectedLayer(node[id])) {
|
}) !== undefined
|
||||||
return true;
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}) !== 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,11 +48,11 @@ 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customBasemapUpdate = writable(0);
|
export const customBasemapUpdate = writable(0);
|
||||||
|
|||||||
@ -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;
|
||||||
@ -30,4 +30,4 @@ export class GoogleRedirect {
|
|||||||
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from "mapbox-gl";
|
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
|
||||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { resetCursor, setPointerCursor } from "$lib/utils";
|
import { resetCursor, setPointerCursor } from '$lib/utils';
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
tiles: ['https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011'],
|
tiles: [
|
||||||
|
'https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||||
|
],
|
||||||
minzoom: 6,
|
minzoom: 6,
|
||||||
maxzoom: 14,
|
maxzoom: 14,
|
||||||
};
|
};
|
||||||
@ -70,7 +72,7 @@ export class MapillaryLayer {
|
|||||||
|
|
||||||
this.marker = new mapboxgl.Marker({
|
this.marker = new mapboxgl.Marker({
|
||||||
rotationAlignment: 'map',
|
rotationAlignment: 'map',
|
||||||
element
|
element,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.viewer.on('position', async () => {
|
this.viewer.on('position', async () => {
|
||||||
@ -145,4 +147,4 @@ export class MapillaryLayer {
|
|||||||
onMouseLeave() {
|
onMouseLeave() {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +1,74 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import { Toggle } from '$lib/components/ui/toggle';
|
import { Toggle } from '$lib/components/ui/toggle';
|
||||||
import { PersonStanding, X } from 'lucide-svelte';
|
import { PersonStanding, X } from 'lucide-svelte';
|
||||||
import { MapillaryLayer } from './Mapillary';
|
import { MapillaryLayer } from './Mapillary';
|
||||||
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 'svelte-i18n';
|
||||||
import { writable } from 'svelte/store';
|
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 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, mapillaryOpen);
|
mapillaryLayer = new MapillaryLayer($map, container, mapillaryOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (mapillaryLayer) {
|
$: if (mapillaryLayer) {
|
||||||
if ($streetViewSource === 'mapillary') {
|
if ($streetViewSource === 'mapillary') {
|
||||||
googleRedirect.remove();
|
googleRedirect.remove();
|
||||||
if ($streetViewEnabled) {
|
if ($streetViewEnabled) {
|
||||||
mapillaryLayer.add();
|
mapillaryLayer.add();
|
||||||
} else {
|
} else {
|
||||||
mapillaryLayer.remove();
|
mapillaryLayer.remove();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mapillaryLayer.remove();
|
mapillaryLayer.remove();
|
||||||
if ($streetViewEnabled) {
|
if ($streetViewEnabled) {
|
||||||
googleRedirect.add();
|
googleRedirect.add();
|
||||||
} else {
|
} else {
|
||||||
googleRedirect.remove();
|
googleRedirect.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CustomControl class="w-[29px] h-[29px] shrink-0">
|
<CustomControl class="w-[29px] h-[29px] shrink-0">
|
||||||
<Tooltip class="w-full h-full" side="left" label={$_('menu.toggle_street_view')}>
|
<Tooltip class="w-full h-full" side="left" label={$_('menu.toggle_street_view')}>
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:pressed={$streetViewEnabled}
|
bind:pressed={$streetViewEnabled}
|
||||||
class="w-full h-full rounded p-0"
|
class="w-full h-full rounded p-0"
|
||||||
aria-label={$_('menu.toggle_street_view')}
|
aria-label={$_('menu.toggle_street_view')}
|
||||||
>
|
>
|
||||||
<PersonStanding size="22" />
|
<PersonStanding size="22" />
|
||||||
</Toggle>
|
</Toggle>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="{$mapillaryOpen
|
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"
|
: '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 -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 right-0 z-10 bg-background p-1 rounded-bl-md cursor-pointer"
|
class="absolute top-0 right-0 z-10 bg-background p-1 rounded-bl-md cursor-pointer"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (mapillaryLayer) {
|
if (mapillaryLayer) {
|
||||||
mapillaryLayer.closePopup();
|
mapillaryLayer.closePopup();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size="16" />
|
<X size="16" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,54 +1,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tool } from '$lib/stores';
|
import { Tool } from '$lib/stores';
|
||||||
import ToolbarItem from './ToolbarItem.svelte';
|
import ToolbarItem from './ToolbarItem.svelte';
|
||||||
import {
|
import {
|
||||||
Group,
|
Group,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
Pencil,
|
Pencil,
|
||||||
SquareDashedMousePointer,
|
SquareDashedMousePointer,
|
||||||
Ungroup,
|
Ungroup,
|
||||||
MapPin,
|
MapPin,
|
||||||
Filter,
|
Filter,
|
||||||
Scissors,
|
Scissors,
|
||||||
MountainSnow
|
MountainSnow,
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import ToolbarItemMenu from './ToolbarItemMenu.svelte';
|
import ToolbarItemMenu from './ToolbarItemMenu.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row w-full items-center pr-12">
|
<div class="flex flex-row w-full items-center pr-12">
|
||||||
<div
|
<div
|
||||||
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ??
|
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ??
|
||||||
''}"
|
''}"
|
||||||
>
|
>
|
||||||
<ToolbarItem tool={Tool.ROUTING} label={$_('toolbar.routing.tooltip')}>
|
<ToolbarItem tool={Tool.ROUTING} label={$_('toolbar.routing.tooltip')}>
|
||||||
<Pencil slot="icon" size="18" />
|
<Pencil slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.WAYPOINT} label={$_('toolbar.waypoint.tooltip')}>
|
<ToolbarItem tool={Tool.WAYPOINT} label={$_('toolbar.waypoint.tooltip')}>
|
||||||
<MapPin slot="icon" size="18" />
|
<MapPin slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.SCISSORS} label={$_('toolbar.scissors.tooltip')}>
|
<ToolbarItem tool={Tool.SCISSORS} label={$_('toolbar.scissors.tooltip')}>
|
||||||
<Scissors slot="icon" size="18" />
|
<Scissors slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.TIME} label={$_('toolbar.time.tooltip')}>
|
<ToolbarItem tool={Tool.TIME} label={$_('toolbar.time.tooltip')}>
|
||||||
<CalendarClock slot="icon" size="18" />
|
<CalendarClock slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.MERGE} label={$_('toolbar.merge.tooltip')}>
|
<ToolbarItem tool={Tool.MERGE} label={$_('toolbar.merge.tooltip')}>
|
||||||
<Group slot="icon" size="18" />
|
<Group slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.EXTRACT} label={$_('toolbar.extract.tooltip')}>
|
<ToolbarItem tool={Tool.EXTRACT} label={$_('toolbar.extract.tooltip')}>
|
||||||
<Ungroup slot="icon" size="18" />
|
<Ungroup slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.ELEVATION} label={$_('toolbar.elevation.button')}>
|
<ToolbarItem tool={Tool.ELEVATION} label={$_('toolbar.elevation.button')}>
|
||||||
<MountainSnow slot="icon" size="18" />
|
<MountainSnow slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.REDUCE} label={$_('toolbar.reduce.tooltip')}>
|
<ToolbarItem tool={Tool.REDUCE} label={$_('toolbar.reduce.tooltip')}>
|
||||||
<Filter slot="icon" size="18" />
|
<Filter slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.CLEAN} label={$_('toolbar.clean.tooltip')}>
|
<ToolbarItem tool={Tool.CLEAN} label={$_('toolbar.clean.tooltip')}>
|
||||||
<SquareDashedMousePointer slot="icon" size="18" />
|
<SquareDashedMousePointer slot="icon" size="18" />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</div>
|
</div>
|
||||||
<ToolbarItemMenu class={$$props.class ?? ''} />
|
<ToolbarItemMenu class={$$props.class ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import { currentTool, type Tool } from '$lib/stores';
|
import { currentTool, type Tool } from '$lib/stores';
|
||||||
|
|
||||||
export let tool: Tool;
|
export let tool: Tool;
|
||||||
export let label: string;
|
export let label: string;
|
||||||
|
|
||||||
function toggleTool() {
|
function toggleTool() {
|
||||||
currentTool.update((current) => (current === tool ? null : tool));
|
currentTool.update((current) => (current === tool ? null : tool));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root openDelay={300}>
|
<Tooltip.Root openDelay={300}>
|
||||||
<Tooltip.Trigger asChild let:builder>
|
<Tooltip.Trigger asChild let:builder>
|
||||||
<Button
|
<Button
|
||||||
builders={[builder]}
|
builders={[builder]}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}"
|
class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}"
|
||||||
on:click={toggleTool}
|
on:click={toggleTool}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<slot name="icon" />
|
<slot name="icon" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side="right">
|
<Tooltip.Content side="right">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
|||||||
@ -1,75 +1,75 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tool, currentTool } from '$lib/stores';
|
import { Tool, currentTool } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { flyAndScale } from '$lib/utils';
|
import { flyAndScale } from '$lib/utils';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||||
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
|
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
|
||||||
import Time from '$lib/components/toolbar/tools/Time.svelte';
|
import Time from '$lib/components/toolbar/tools/Time.svelte';
|
||||||
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||||
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
||||||
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
|
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
|
||||||
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||||
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
|
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
|
||||||
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
const { minimizeRoutingMenu } = settings;
|
const { minimizeRoutingMenu } = settings;
|
||||||
|
|
||||||
let popupElement: HTMLElement;
|
let popupElement: HTMLElement;
|
||||||
let popup: mapboxgl.Popup;
|
let popup: mapboxgl.Popup;
|
||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $currentTool !== null}
|
{#if $currentTool !== null}
|
||||||
<div
|
<div
|
||||||
in:flyAndScale={{ x: -2, y: 0, duration: 100 }}
|
in:flyAndScale={{ x: -2, y: 0, duration: 100 }}
|
||||||
class="translate-x-1 h-full {$$props.class ?? ''}"
|
class="translate-x-1 h-full {$$props.class ?? ''}"
|
||||||
>
|
>
|
||||||
<div class="rounded-md shadow-md pointer-events-auto">
|
<div class="rounded-md shadow-md pointer-events-auto">
|
||||||
<Card.Root class="rounded-md border-none">
|
<Card.Root class="rounded-md border-none">
|
||||||
<Card.Content class="p-2.5">
|
<Card.Content class="p-2.5">
|
||||||
{#if $currentTool === Tool.ROUTING}
|
{#if $currentTool === Tool.ROUTING}
|
||||||
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
|
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
|
||||||
{:else if $currentTool === Tool.SCISSORS}
|
{:else if $currentTool === Tool.SCISSORS}
|
||||||
<Scissors />
|
<Scissors />
|
||||||
{:else if $currentTool === Tool.WAYPOINT}
|
{:else if $currentTool === Tool.WAYPOINT}
|
||||||
<Waypoint />
|
<Waypoint />
|
||||||
{:else if $currentTool === Tool.TIME}
|
{:else if $currentTool === Tool.TIME}
|
||||||
<Time />
|
<Time />
|
||||||
{:else if $currentTool === Tool.MERGE}
|
{:else if $currentTool === Tool.MERGE}
|
||||||
<Merge />
|
<Merge />
|
||||||
{:else if $currentTool === Tool.ELEVATION}
|
{:else if $currentTool === Tool.ELEVATION}
|
||||||
<Elevation />
|
<Elevation />
|
||||||
{:else if $currentTool === Tool.EXTRACT}
|
{:else if $currentTool === Tool.EXTRACT}
|
||||||
<Extract />
|
<Extract />
|
||||||
{:else if $currentTool === Tool.CLEAN}
|
{:else if $currentTool === Tool.CLEAN}
|
||||||
<Clean />
|
<Clean />
|
||||||
{:else if $currentTool === Tool.REDUCE}
|
{:else if $currentTool === Tool.REDUCE}
|
||||||
<Reduce />
|
<Reduce />
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if ($currentTool !== null && e.key === 'Escape') {
|
if ($currentTool !== null && e.key === 'Escape') {
|
||||||
currentTool.set(null);
|
currentTool.set(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RoutingControlPopup bind:element={popupElement} />
|
<RoutingControlPopup bind:element={popupElement} />
|
||||||
|
|||||||
@ -1,188 +1,188 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
enum CleanType {
|
enum CleanType {
|
||||||
INSIDE = 'inside',
|
INSIDE = 'inside',
|
||||||
OUTSIDE = 'outside'
|
OUTSIDE = 'outside',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 { 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 { 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 'svelte-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';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
|
|
||||||
let cleanType = CleanType.INSIDE;
|
let cleanType = CleanType.INSIDE;
|
||||||
let deleteTrackpoints = true;
|
let deleteTrackpoints = true;
|
||||||
let deleteWaypoints = true;
|
let deleteWaypoints = true;
|
||||||
let rectangleCoordinates: mapboxgl.LngLat[] = [];
|
let rectangleCoordinates: mapboxgl.LngLat[] = [];
|
||||||
|
|
||||||
function updateRectangle() {
|
function updateRectangle() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
if (rectangleCoordinates.length != 2) {
|
if (rectangleCoordinates.length != 2) {
|
||||||
if ($map.getLayer('rectangle')) {
|
if ($map.getLayer('rectangle')) {
|
||||||
$map.removeLayer('rectangle');
|
$map.removeLayer('rectangle');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let data = {
|
let data = {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Polygon',
|
type: 'Polygon',
|
||||||
coordinates: [
|
coordinates: [
|
||||||
[
|
[
|
||||||
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
|
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
|
||||||
[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) {
|
||||||
source.setData(data);
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
$map.addSource('rectangle', {
|
$map.addSource('rectangle', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: data
|
data: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('rectangle')) {
|
if (!$map.getLayer('rectangle')) {
|
||||||
$map.addLayer({
|
$map.addLayer({
|
||||||
id: 'rectangle',
|
id: 'rectangle',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'rectangle',
|
source: 'rectangle',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': 'SteelBlue',
|
'fill-color': 'SteelBlue',
|
||||||
'fill-opacity': 0.5
|
'fill-opacity': 0.5,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (rectangleCoordinates) {
|
$: if (rectangleCoordinates) {
|
||||||
updateRectangle();
|
updateRectangle();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mousedown = false;
|
let mousedown = false;
|
||||||
function onMouseDown(e: any) {
|
function onMouseDown(e: any) {
|
||||||
mousedown = true;
|
mousedown = true;
|
||||||
rectangleCoordinates = [e.lngLat, e.lngLat];
|
rectangleCoordinates = [e.lngLat, e.lngLat];
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(e: any) {
|
function onMouseMove(e: any) {
|
||||||
if (mousedown) {
|
if (mousedown) {
|
||||||
rectangleCoordinates[1] = e.lngLat;
|
rectangleCoordinates[1] = e.lngLat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp(e: any) {
|
function onMouseUp(e: any) {
|
||||||
mousedown = false;
|
mousedown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setCrosshairCursor();
|
setCrosshairCursor();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
$map.on('mousedown', onMouseDown);
|
$map.on('mousedown', onMouseDown);
|
||||||
$map.on('mousemove', onMouseMove);
|
$map.on('mousemove', onMouseMove);
|
||||||
$map.on('mouseup', onMouseUp);
|
$map.on('mouseup', onMouseUp);
|
||||||
$map.on('touchstart', onMouseDown);
|
$map.on('touchstart', onMouseDown);
|
||||||
$map.on('touchmove', onMouseMove);
|
$map.on('touchmove', onMouseMove);
|
||||||
$map.on('touchend', onMouseUp);
|
$map.on('touchend', onMouseUp);
|
||||||
$map.dragPan.disable();
|
$map.dragPan.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.off('mousedown', onMouseDown);
|
$map.off('mousedown', onMouseDown);
|
||||||
$map.off('mousemove', onMouseMove);
|
$map.off('mousemove', onMouseMove);
|
||||||
$map.off('mouseup', onMouseUp);
|
$map.off('mouseup', onMouseUp);
|
||||||
$map.off('touchstart', onMouseDown);
|
$map.off('touchstart', onMouseDown);
|
||||||
$map.off('touchmove', onMouseMove);
|
$map.off('touchmove', onMouseMove);
|
||||||
$map.off('touchend', onMouseUp);
|
$map.off('touchend', onMouseUp);
|
||||||
$map.dragPan.enable();
|
$map.dragPan.enable();
|
||||||
|
|
||||||
if ($map.getLayer('rectangle')) {
|
if ($map.getLayer('rectangle')) {
|
||||||
$map.removeLayer('rectangle');
|
$map.removeLayer('rectangle');
|
||||||
}
|
}
|
||||||
if ($map.getSource('rectangle')) {
|
if ($map.getSource('rectangle')) {
|
||||||
$map.removeSource('rectangle');
|
$map.removeSource('rectangle');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: validSelection = $selection.size > 0;
|
$: validSelection = $selection.size > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-80 items-center {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-80 items-center {$$props.class ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-3">
|
<fieldset class="flex flex-col gap-3">
|
||||||
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
||||||
<Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" />
|
<Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" />
|
||||||
<Label for="delete-trkpt">
|
<Label for="delete-trkpt">
|
||||||
{$_('toolbar.clean.delete_trackpoints')}
|
{$_('toolbar.clean.delete_trackpoints')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
<div class="flex flex-row items-center gap-[6.4px] h-3">
|
||||||
<Checkbox id="delete-wpt" bind:checked={deleteWaypoints} class="scale-90" />
|
<Checkbox id="delete-wpt" bind:checked={deleteWaypoints} class="scale-90" />
|
||||||
<Label for="delete-wpt">
|
<Label for="delete-wpt">
|
||||||
{$_('toolbar.clean.delete_waypoints')}
|
{$_('toolbar.clean.delete_waypoints')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<RadioGroup.Root bind:value={cleanType}>
|
<RadioGroup.Root bind:value={cleanType}>
|
||||||
<Label class="flex flex-row items-center gap-2">
|
<Label class="flex flex-row items-center gap-2">
|
||||||
<RadioGroup.Item value={CleanType.INSIDE} />
|
<RadioGroup.Item value={CleanType.INSIDE} />
|
||||||
{$_('toolbar.clean.delete_inside')}
|
{$_('toolbar.clean.delete_inside')}
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row items-center gap-2">
|
<Label class="flex flex-row items-center gap-2">
|
||||||
<RadioGroup.Item value={CleanType.OUTSIDE} />
|
<RadioGroup.Item value={CleanType.OUTSIDE} />
|
||||||
{$_('toolbar.clean.delete_outside')}
|
{$_('toolbar.clean.delete_outside')}
|
||||||
</Label>
|
</Label>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
disabled={!validSelection || rectangleCoordinates.length != 2}
|
disabled={!validSelection || rectangleCoordinates.length != 2}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dbUtils.cleanSelection(
|
dbUtils.cleanSelection(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
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,
|
||||||
deleteWaypoints
|
deleteWaypoints
|
||||||
);
|
);
|
||||||
rectangleCoordinates = [];
|
rectangleCoordinates = [];
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('toolbar.clean.button')}
|
{$_('toolbar.clean.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.clean.help')}
|
{$_('toolbar.clean.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.clean.help_no_selection')}
|
{$_('toolbar.clean.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
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 'svelte-i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection = $selection.size > 0;
|
$: validSelection = $selection.size > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 ?? ''}">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
dbUtils.addElevationToSelection($map);
|
dbUtils.addElevationToSelection($map);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MountainSnow size="16" class="mr-1 shrink-0" />
|
<MountainSnow size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.elevation.button')}
|
{$_('toolbar.elevation.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.elevation.help')}
|
{$_('toolbar.elevation.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.elevation.help_no_selection')}
|
{$_('toolbar.elevation.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,53 +1,53 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Ungroup } from 'lucide-svelte';
|
import { Ungroup } from 'lucide-svelte';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
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 'svelte-i18n';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection =
|
$: validSelection =
|
||||||
$selection.size > 0 &&
|
$selection.size > 0 &&
|
||||||
$selection.getSelected().every((item) => {
|
$selection.getSelected().every((item) => {
|
||||||
if (
|
if (
|
||||||
item instanceof ListWaypointsItem ||
|
item instanceof ListWaypointsItem ||
|
||||||
item instanceof ListWaypointItem ||
|
item instanceof ListWaypointItem ||
|
||||||
item instanceof ListTrackSegmentItem
|
item instanceof ListTrackSegmentItem
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
if (file) {
|
if (file) {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
return file.getSegments().length > 1;
|
return file.getSegments().length > 1;
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
if (item.getTrackIndex() < file.trk.length) {
|
if (item.getTrackIndex() < file.trk.length) {
|
||||||
return file.trk[item.getTrackIndex()].getSegments().length > 1;
|
return file.trk[item.getTrackIndex()].getSegments().length > 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 ?? ''}">
|
||||||
<Button variant="outline" disabled={!validSelection} on:click={dbUtils.extractSelection}>
|
<Button variant="outline" disabled={!validSelection} on:click={dbUtils.extractSelection}>
|
||||||
<Ungroup size="16" class="mr-1" />
|
<Ungroup size="16" class="mr-1" />
|
||||||
{$_('toolbar.extract.button')}
|
{$_('toolbar.extract.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.extract.help')}
|
{$_('toolbar.extract.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.extract.help_invalid_selection')}
|
{$_('toolbar.extract.help_invalid_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,117 +1,117 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
enum MergeType {
|
enum MergeType {
|
||||||
TRACES = 'traces',
|
TRACES = 'traces',
|
||||||
CONTENTS = 'contents'
|
CONTENTS = 'contents',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
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 { 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 'svelte-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';
|
import { gpxStatistics } from '$lib/stores';
|
||||||
|
|
||||||
let canMergeTraces = false;
|
let canMergeTraces = false;
|
||||||
let canMergeContents = false;
|
let canMergeContents = false;
|
||||||
let removeGaps = false;
|
let removeGaps = false;
|
||||||
|
|
||||||
$: if ($selection.size > 1) {
|
$: if ($selection.size > 1) {
|
||||||
canMergeTraces = true;
|
canMergeTraces = true;
|
||||||
} else if ($selection.size === 1) {
|
} else if ($selection.size === 1) {
|
||||||
let selected = $selection.getSelected()[0];
|
let selected = $selection.getSelected()[0];
|
||||||
if (selected instanceof ListFileItem) {
|
if (selected instanceof ListFileItem) {
|
||||||
let file = getFile(selected.getFileId());
|
let file = getFile(selected.getFileId());
|
||||||
if (file) {
|
if (file) {
|
||||||
canMergeTraces = file.getSegments().length > 1;
|
canMergeTraces = file.getSegments().length > 1;
|
||||||
} else {
|
} else {
|
||||||
canMergeTraces = false;
|
canMergeTraces = false;
|
||||||
}
|
}
|
||||||
} else if (selected instanceof ListTrackItem) {
|
} else if (selected instanceof ListTrackItem) {
|
||||||
let trackIndex = selected.getTrackIndex();
|
let trackIndex = selected.getTrackIndex();
|
||||||
let file = getFile(selected.getFileId());
|
let file = getFile(selected.getFileId());
|
||||||
if (file && trackIndex < file.trk.length) {
|
if (file && trackIndex < file.trk.length) {
|
||||||
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
|
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
|
||||||
} else {
|
} else {
|
||||||
canMergeTraces = false;
|
canMergeTraces = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
canMergeContents = false;
|
canMergeContents = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canMergeContents =
|
$: canMergeContents =
|
||||||
$selection.size > 1 &&
|
$selection.size > 1 &&
|
||||||
$selection
|
$selection
|
||||||
.getSelected()
|
.getSelected()
|
||||||
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
|
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem);
|
||||||
|
|
||||||
let mergeType = MergeType.TRACES;
|
let mergeType = MergeType.TRACES;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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-1.5 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-1.5 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}
|
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
|
||||||
<div class="flex flex-row items-center gap-1.5">
|
<div class="flex flex-row items-center gap-1.5">
|
||||||
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
|
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
|
||||||
<Label for="remove-gaps">{$_('toolbar.merge.remove_gaps')}</Label>
|
<Label for="remove-gaps">{$_('toolbar.merge.remove_gaps')}</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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(
|
dbUtils.mergeSelection(
|
||||||
mergeType === MergeType.TRACES,
|
mergeType === MergeType.TRACES,
|
||||||
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
|
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" />
|
||||||
{$_('toolbar.merge.merge_selection')}
|
{$_('toolbar.merge.merge_selection')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
|
||||||
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
||||||
{$_('toolbar.merge.help_merge_traces')}
|
{$_('toolbar.merge.help_merge_traces')}
|
||||||
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
||||||
{$_('toolbar.merge.help_cannot_merge_traces')}
|
{$_('toolbar.merge.help_cannot_merge_traces')}
|
||||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||||
<Shortcut
|
<Shortcut
|
||||||
ctrl={true}
|
ctrl={true}
|
||||||
click={true}
|
click={true}
|
||||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||||
/>
|
/>
|
||||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||||
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
||||||
{$_('toolbar.merge.help_merge_contents')}
|
{$_('toolbar.merge.help_merge_contents')}
|
||||||
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
||||||
{$_('toolbar.merge.help_cannot_merge_contents')}
|
{$_('toolbar.merge.help_cannot_merge_contents')}
|
||||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||||
<Shortcut
|
<Shortcut
|
||||||
ctrl={true}
|
ctrl={true}
|
||||||
click={true}
|
click={true}
|
||||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||||
/>
|
/>
|
||||||
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,178 +1,187 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
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 {
|
||||||
import Help from '$lib/components/Help.svelte';
|
ListItem,
|
||||||
import { Filter } from 'lucide-svelte';
|
ListRootItem,
|
||||||
import { _, locale } from 'svelte-i18n';
|
ListTrackSegmentItem,
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
} from '$lib/components/file-list/FileList';
|
||||||
import { dbUtils, fileObservers } from '$lib/db';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { map } from '$lib/stores';
|
import { Filter } from 'lucide-svelte';
|
||||||
import { onDestroy } from 'svelte';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { derived } from 'svelte/store';
|
import { dbUtils, fileObservers } from '$lib/db';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { map } from '$lib/stores';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
|
import { derived } from 'svelte/store';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let sliderValue = [50];
|
let sliderValue = [50];
|
||||||
let maxPoints = 0;
|
let maxPoints = 0;
|
||||||
let currentPoints = 0;
|
let currentPoints = 0;
|
||||||
const minTolerance = 0.1;
|
const minTolerance = 0.1;
|
||||||
const maxTolerance = 10000;
|
const maxTolerance = 10000;
|
||||||
|
|
||||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
|
|
||||||
$: tolerance =
|
$: tolerance =
|
||||||
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)));
|
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>();
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
maxPoints = 0;
|
maxPoints = 0;
|
||||||
currentPoints = 0;
|
currentPoints = 0;
|
||||||
|
|
||||||
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) => {
|
||||||
maxPoints += maxPts;
|
maxPoints += maxPts;
|
||||||
|
|
||||||
let current = points.filter(
|
let current = points.filter(
|
||||||
(point) => point.distance === undefined || point.distance >= tolerance
|
(point) => point.distance === undefined || point.distance >= tolerance
|
||||||
);
|
);
|
||||||
currentPoints += current.length;
|
currentPoints += current.length;
|
||||||
|
|
||||||
data.features.push({
|
data.features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
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: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($map) {
|
if ($map) {
|
||||||
let source = $map.getSource('simplified');
|
let source = $map.getSource('simplified');
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(data);
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
$map.addSource('simplified', {
|
$map.addSource('simplified', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: data
|
data: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('simplified')) {
|
if (!$map.getLayer('simplified')) {
|
||||||
$map.addLayer({
|
$map.addLayer({
|
||||||
id: 'simplified',
|
id: 'simplified',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($fileObservers) {
|
$: if ($fileObservers) {
|
||||||
unsubscribes.forEach((unsubscribe, fileId) => {
|
unsubscribes.forEach((unsubscribe, fileId) => {
|
||||||
if (!$fileObservers.has(fileId)) {
|
if (!$fileObservers.has(fileId)) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribes.delete(fileId);
|
unsubscribes.delete(fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$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,
|
||||||
if (fs) {
|
sel,
|
||||||
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
]).subscribe(([fs, sel]) => {
|
||||||
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
|
if (fs) {
|
||||||
if (sel.hasAnyParent(segmentItem)) {
|
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
let segmentItem = new ListTrackSegmentItem(
|
||||||
simplified.set(segmentItem.getFullId(), [
|
fileId,
|
||||||
segmentItem,
|
trackIndex,
|
||||||
statistics.local.points.length,
|
segmentIndex
|
||||||
ramerDouglasPeucker(statistics.local.points, minTolerance)
|
);
|
||||||
]);
|
if (sel.hasAnyParent(segmentItem)) {
|
||||||
update();
|
let statistics = fs.statistics.getStatisticsFor(segmentItem);
|
||||||
} else if (simplified.has(segmentItem.getFullId())) {
|
simplified.set(segmentItem.getFullId(), [
|
||||||
simplified.delete(segmentItem.getFullId());
|
segmentItem,
|
||||||
update();
|
statistics.local.points.length,
|
||||||
}
|
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||||
});
|
]);
|
||||||
}
|
update();
|
||||||
}
|
} else if (simplified.has(segmentItem.getFullId())) {
|
||||||
);
|
simplified.delete(segmentItem.getFullId());
|
||||||
unsubscribes.set(fileId, unsubscribe);
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
unsubscribes.set(fileId, unsubscribe);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$: if (tolerance) {
|
$: if (tolerance) {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
if ($map.getLayer('simplified')) {
|
if ($map.getLayer('simplified')) {
|
||||||
$map.removeLayer('simplified');
|
$map.removeLayer('simplified');
|
||||||
}
|
}
|
||||||
if ($map.getSource('simplified')) {
|
if ($map.getSource('simplified')) {
|
||||||
$map.removeSource('simplified');
|
$map.removeSource('simplified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
simplified.clear();
|
simplified.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
function reduce() {
|
function reduce() {
|
||||||
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
|
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
|
||||||
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
simplified.forEach(([item, maxPts, points], itemFullId) => {
|
||||||
itemsAndPoints.set(
|
itemsAndPoints.set(
|
||||||
item,
|
item,
|
||||||
points
|
points
|
||||||
.filter((point) => point.distance === undefined || point.distance >= tolerance)
|
.filter((point) => point.distance === undefined || point.distance >= tolerance)
|
||||||
.map((point) => point.point)
|
.map((point) => point.point)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
dbUtils.reduce(itemsAndPoints);
|
dbUtils.reduce(itemsAndPoints);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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={sliderValue} min={0} max={100} step={1} />
|
<Slider bind:value={sliderValue} min={0} max={100} step={1} />
|
||||||
</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={4} 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>
|
||||||
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
||||||
<Filter size="16" class="mr-1" />
|
<Filter size="16" class="mr-1" />
|
||||||
{$_('toolbar.reduce.button')}
|
{$_('toolbar.reduce.button')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.reduce.help')}
|
{$_('toolbar.reduce.help')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.reduce.help_no_selection')}
|
{$_('toolbar.reduce.help_no_selection')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,393 +1,404 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
|
import DatePicker from '$lib/components/ui/date-picker/DatePicker.svelte';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
||||||
import { dbUtils, settings } from '$lib/db';
|
import { dbUtils, settings } from '$lib/db';
|
||||||
import { gpxStatistics } from '$lib/stores';
|
import { gpxStatistics } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
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 'svelte-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';
|
||||||
|
|
||||||
let startDate: DateValue | undefined = undefined;
|
let startDate: DateValue | undefined = undefined;
|
||||||
let startTime: string | undefined = undefined;
|
let startTime: string | undefined = undefined;
|
||||||
let endDate: DateValue | undefined = undefined;
|
let endDate: DateValue | undefined = undefined;
|
||||||
let endTime: string | undefined = undefined;
|
let endTime: string | undefined = undefined;
|
||||||
let movingTime: number | undefined = undefined;
|
let movingTime: number | undefined = undefined;
|
||||||
let speed: number | undefined = undefined;
|
let speed: number | undefined = undefined;
|
||||||
let artificial = false;
|
let artificial = false;
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTimeString(date: Date): string {
|
function toTimeString(date: Date): string {
|
||||||
return date.toTimeString().split(' ')[0];
|
return date.toTimeString().split(' ')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { velocityUnits, distanceUnits } = settings;
|
const { velocityUnits, distanceUnits } = settings;
|
||||||
|
|
||||||
function setSpeed(value: number) {
|
function setSpeed(value: number) {
|
||||||
let speedValue = getConvertedVelocity(value);
|
let speedValue = getConvertedVelocity(value);
|
||||||
if ($velocityUnits === 'speed') {
|
if ($velocityUnits === 'speed') {
|
||||||
speedValue = parseFloat(speedValue.toFixed(2));
|
speedValue = parseFloat(speedValue.toFixed(2));
|
||||||
}
|
}
|
||||||
speed = speedValue;
|
speed = speedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGPXData() {
|
function setGPXData() {
|
||||||
if ($gpxStatistics.global.time.start) {
|
if ($gpxStatistics.global.time.start) {
|
||||||
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
||||||
startTime = toTimeString($gpxStatistics.global.time.start);
|
startTime = toTimeString($gpxStatistics.global.time.start);
|
||||||
} else {
|
} else {
|
||||||
startDate = undefined;
|
startDate = undefined;
|
||||||
startTime = undefined;
|
startTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.end) {
|
if ($gpxStatistics.global.time.end) {
|
||||||
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
||||||
endTime = toTimeString($gpxStatistics.global.time.end);
|
endTime = toTimeString($gpxStatistics.global.time.end);
|
||||||
} else {
|
} else {
|
||||||
endDate = undefined;
|
endDate = undefined;
|
||||||
endTime = undefined;
|
endTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.moving) {
|
if ($gpxStatistics.global.time.moving) {
|
||||||
movingTime = $gpxStatistics.global.time.moving;
|
movingTime = $gpxStatistics.global.time.moving;
|
||||||
} else {
|
} else {
|
||||||
movingTime = undefined;
|
movingTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.speed.moving) {
|
if ($gpxStatistics.global.speed.moving) {
|
||||||
setSpeed($gpxStatistics.global.speed.moving);
|
setSpeed($gpxStatistics.global.speed.moving);
|
||||||
} else {
|
} else {
|
||||||
speed = undefined;
|
speed = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
||||||
setGPXData();
|
setGPXData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDate(date: DateValue, time: string): Date {
|
function getDate(date: DateValue, time: string): Date {
|
||||||
if (date === undefined) {
|
if (date === undefined) {
|
||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
||||||
if (seconds === undefined) {
|
if (seconds === undefined) {
|
||||||
seconds = 0;
|
seconds = 0;
|
||||||
}
|
}
|
||||||
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEnd() {
|
function updateEnd() {
|
||||||
if (startDate && movingTime !== undefined) {
|
if (startDate && movingTime !== undefined) {
|
||||||
if (startTime === undefined) {
|
if (startTime === undefined) {
|
||||||
startTime = '00:00:00';
|
startTime = '00:00:00';
|
||||||
}
|
}
|
||||||
let start = getDate(startDate, startTime);
|
let start = getDate(startDate, startTime);
|
||||||
let ratio =
|
let ratio =
|
||||||
$gpxStatistics.global.time.moving > 0
|
$gpxStatistics.global.time.moving > 0
|
||||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||||
: 1;
|
: 1;
|
||||||
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
||||||
endDate = toCalendarDate(end);
|
endDate = toCalendarDate(end);
|
||||||
endTime = toTimeString(end);
|
endTime = toTimeString(end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStart() {
|
function updateStart() {
|
||||||
if (endDate && movingTime !== undefined) {
|
if (endDate && movingTime !== undefined) {
|
||||||
if (endTime === undefined) {
|
if (endTime === undefined) {
|
||||||
endTime = '00:00:00';
|
endTime = '00:00:00';
|
||||||
}
|
}
|
||||||
let end = getDate(endDate, endTime);
|
let end = getDate(endDate, endTime);
|
||||||
let ratio =
|
let ratio =
|
||||||
$gpxStatistics.global.time.moving > 0
|
$gpxStatistics.global.time.moving > 0
|
||||||
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
? $gpxStatistics.global.time.total / $gpxStatistics.global.time.moving
|
||||||
: 1;
|
: 1;
|
||||||
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
||||||
startDate = toCalendarDate(start);
|
startDate = toCalendarDate(start);
|
||||||
startTime = toTimeString(start);
|
startTime = toTimeString(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpeed() {
|
function getSpeed() {
|
||||||
if (speed === undefined) {
|
if (speed === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let speedValue = speed;
|
let speedValue = speed;
|
||||||
if ($velocityUnits === 'pace') {
|
if ($velocityUnits === 'pace') {
|
||||||
speedValue = distancePerHourToSecondsPerDistance(speed);
|
speedValue = distancePerHourToSecondsPerDistance(speed);
|
||||||
}
|
}
|
||||||
if ($distanceUnits === 'imperial') {
|
if ($distanceUnits === 'imperial') {
|
||||||
speedValue = milesToKilometers(speedValue);
|
speedValue = milesToKilometers(speedValue);
|
||||||
} else if ($distanceUnits === 'nautical') {
|
} else if ($distanceUnits === 'nautical') {
|
||||||
speedValue = nauticalMilesToKilometers(speedValue);
|
speedValue = nauticalMilesToKilometers(speedValue);
|
||||||
}
|
}
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDataFromSpeed() {
|
function updateDataFromSpeed() {
|
||||||
let speedValue = getSpeed();
|
let speedValue = getSpeed();
|
||||||
if (speedValue === undefined) {
|
if (speedValue === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let distance =
|
let distance =
|
||||||
$gpxStatistics.global.distance.moving > 0
|
$gpxStatistics.global.distance.moving > 0
|
||||||
? $gpxStatistics.global.distance.moving
|
? $gpxStatistics.global.distance.moving
|
||||||
: $gpxStatistics.global.distance.total;
|
: $gpxStatistics.global.distance.total;
|
||||||
movingTime = (distance / speedValue) * 3600;
|
movingTime = (distance / speedValue) * 3600;
|
||||||
|
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDataFromTotalTime() {
|
function updateDataFromTotalTime() {
|
||||||
if (movingTime === undefined) {
|
if (movingTime === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let distance =
|
let distance =
|
||||||
$gpxStatistics.global.distance.moving > 0
|
$gpxStatistics.global.distance.moving > 0
|
||||||
? $gpxStatistics.global.distance.moving
|
? $gpxStatistics.global.distance.moving
|
||||||
: $gpxStatistics.global.distance.total;
|
: $gpxStatistics.global.distance.total;
|
||||||
setSpeed(distance / (movingTime / 3600));
|
setSpeed(distance / (movingTime / 3600));
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canUpdate =
|
$: canUpdate =
|
||||||
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 justify-center">
|
<div class="flex flex-row gap-2 justify-center">
|
||||||
<div class="flex flex-col gap-2 grow">
|
<div class="flex flex-col gap-2 grow">
|
||||||
<Label for="speed" class="flex flex-row">
|
<Label for="speed" class="flex flex-row">
|
||||||
<Zap size="16" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
{#if $velocityUnits === 'speed'}
|
{#if $velocityUnits === 'speed'}
|
||||||
{$_('quantities.speed')}
|
{$_('quantities.speed')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('quantities.pace')}
|
{$_('quantities.pace')}
|
||||||
{/if}
|
{/if}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-1 items-center">
|
<div class="flex flex-row gap-1 items-center">
|
||||||
{#if $velocityUnits === 'speed'}
|
{#if $velocityUnits === 'speed'}
|
||||||
<Input
|
<Input
|
||||||
id="speed"
|
id="speed"
|
||||||
type="number"
|
type="number"
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0.01}
|
min={0.01}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={speed}
|
bind:value={speed}
|
||||||
on:change={updateDataFromSpeed}
|
on:change={updateDataFromSpeed}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.miles_per_hour')}
|
{$_('units.miles_per_hour')}
|
||||||
{:else if $distanceUnits === 'metric'}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.kilometers_per_hour')}
|
{$_('units.kilometers_per_hour')}
|
||||||
{:else if $distanceUnits === 'nautical'}
|
{:else if $distanceUnits === 'nautical'}
|
||||||
{$_('units.knots')}
|
{$_('units.knots')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<TimePicker
|
<TimePicker
|
||||||
bind:value={speed}
|
bind:value={speed}
|
||||||
showHours={false}
|
showHours={false}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
onChange={updateDataFromSpeed}
|
onChange={updateDataFromSpeed}
|
||||||
/>
|
/>
|
||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.minutes_per_mile')}
|
{$_('units.minutes_per_mile')}
|
||||||
{:else if $distanceUnits === 'metric'}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.minutes_per_kilometer')}
|
{$_('units.minutes_per_kilometer')}
|
||||||
{:else if $distanceUnits === 'nautical'}
|
{:else if $distanceUnits === 'nautical'}
|
||||||
{$_('units.minutes_per_nautical_mile')}
|
{$_('units.minutes_per_nautical_mile')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 grow">
|
<div class="flex flex-col gap-2 grow">
|
||||||
<Label for="duration" class="flex flex-row">
|
<Label for="duration" class="flex flex-row">
|
||||||
<Timer size="16" class="mr-1" />
|
<Timer size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.total_time')}
|
{$_('toolbar.time.total_time')}
|
||||||
</Label>
|
</Label>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
bind:value={movingTime}
|
bind:value={movingTime}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
onChange={updateDataFromTotalTime}
|
onChange={updateDataFromTotalTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row">
|
<Label class="flex flex-row">
|
||||||
<CirclePlay size="16" class="mr-1" />
|
<CirclePlay size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.start')}
|
{$_('toolbar.time.start')}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
bind:value={startDate}
|
bind:value={startDate}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
locale={get(locale) ?? 'en'}
|
locale={get(locale) ?? 'en'}
|
||||||
placeholder={$_('toolbar.time.pick_date')}
|
placeholder={$_('toolbar.time.pick_date')}
|
||||||
class="w-fit grow"
|
class="w-fit grow"
|
||||||
onValueChange={async () => {
|
onValueChange={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
updateEnd();
|
updateEnd();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={1}
|
step={1}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={startTime}
|
bind:value={startTime}
|
||||||
class="w-fit"
|
class="w-fit"
|
||||||
on:change={updateEnd}
|
on:change={updateEnd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row">
|
<Label class="flex flex-row">
|
||||||
<CircleStop size="16" class="mr-1" />
|
<CircleStop size="16" class="mr-1" />
|
||||||
{$_('toolbar.time.end')}
|
{$_('toolbar.time.end')}
|
||||||
</Label>
|
</Label>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
bind:value={endDate}
|
bind:value={endDate}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
locale={get(locale) ?? 'en'}
|
locale={get(locale) ?? 'en'}
|
||||||
placeholder={$_('toolbar.time.pick_date')}
|
placeholder={$_('toolbar.time.pick_date')}
|
||||||
class="w-fit grow"
|
class="w-fit grow"
|
||||||
onValueChange={async () => {
|
onValueChange={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
updateStart();
|
updateStart();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
step={1}
|
step={1}
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
bind:value={endTime}
|
bind:value={endTime}
|
||||||
class="w-fit"
|
class="w-fit"
|
||||||
on:change={updateStart}
|
on:change={updateStart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
||||||
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
||||||
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
||||||
<Label for="artificial-time">
|
<Label for="artificial-time">
|
||||||
{$_('toolbar.time.artificial')}
|
{$_('toolbar.time.artificial')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
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 (
|
||||||
return;
|
startDate === undefined ||
|
||||||
}
|
startTime === undefined ||
|
||||||
|
effectiveSpeed === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
|
if (Math.abs(effectiveSpeed - $gpxStatistics.global.speed.moving) < 0.01) {
|
||||||
effectiveSpeed = $gpxStatistics.global.speed.moving;
|
effectiveSpeed = $gpxStatistics.global.speed.moving;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ratio = 1;
|
let ratio = 1;
|
||||||
if (
|
if (
|
||||||
$gpxStatistics.global.speed.moving > 0 &&
|
$gpxStatistics.global.speed.moving > 0 &&
|
||||||
$gpxStatistics.global.speed.moving !== effectiveSpeed
|
$gpxStatistics.global.speed.moving !== effectiveSpeed
|
||||||
) {
|
) {
|
||||||
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = $selection.getSelected()[0];
|
let item = $selection.getSelected()[0];
|
||||||
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) {
|
||||||
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
|
file.createArtificialTimestamps(
|
||||||
} else {
|
getDate(startDate, startTime),
|
||||||
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
movingTime
|
||||||
}
|
);
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else {
|
||||||
if (artificial) {
|
file.changeTimestamps(
|
||||||
file.createArtificialTimestamps(
|
getDate(startDate, startTime),
|
||||||
getDate(startDate, startTime),
|
effectiveSpeed,
|
||||||
movingTime,
|
ratio
|
||||||
item.getTrackIndex()
|
);
|
||||||
);
|
}
|
||||||
} else {
|
} else if (item instanceof ListTrackItem) {
|
||||||
file.changeTimestamps(
|
if (artificial) {
|
||||||
getDate(startDate, startTime),
|
file.createArtificialTimestamps(
|
||||||
effectiveSpeed,
|
getDate(startDate, startTime),
|
||||||
ratio,
|
movingTime,
|
||||||
item.getTrackIndex()
|
item.getTrackIndex()
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
file.changeTimestamps(
|
||||||
if (artificial) {
|
getDate(startDate, startTime),
|
||||||
file.createArtificialTimestamps(
|
effectiveSpeed,
|
||||||
getDate(startDate, startTime),
|
ratio,
|
||||||
movingTime,
|
item.getTrackIndex()
|
||||||
item.getTrackIndex(),
|
);
|
||||||
item.getSegmentIndex()
|
}
|
||||||
);
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
} else {
|
if (artificial) {
|
||||||
file.changeTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate, startTime),
|
getDate(startDate, startTime),
|
||||||
effectiveSpeed,
|
movingTime,
|
||||||
ratio,
|
item.getTrackIndex(),
|
||||||
item.getTrackIndex(),
|
item.getSegmentIndex()
|
||||||
item.getSegmentIndex()
|
);
|
||||||
);
|
} else {
|
||||||
}
|
file.changeTimestamps(
|
||||||
}
|
getDate(startDate, startTime),
|
||||||
});
|
effectiveSpeed,
|
||||||
}}
|
ratio,
|
||||||
>
|
item.getTrackIndex(),
|
||||||
<CalendarClock size="16" class="mr-1 shrink-0" />
|
item.getSegmentIndex()
|
||||||
{$_('toolbar.time.update')}
|
);
|
||||||
</Button>
|
}
|
||||||
<Button variant="outline" on:click={setGPXData}>
|
}
|
||||||
<CircleX size="16" />
|
});
|
||||||
</Button>
|
}}
|
||||||
</div>
|
>
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
|
<CalendarClock size="16" class="mr-1 shrink-0" />
|
||||||
{#if canUpdate}
|
{$_('toolbar.time.update')}
|
||||||
{$_('toolbar.time.help')}
|
</Button>
|
||||||
{:else}
|
<Button variant="outline" on:click={setGPXData}>
|
||||||
{$_('toolbar.time.help_invalid_selection')}
|
<CircleX size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
</Help>
|
</div>
|
||||||
|
<Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
|
||||||
|
{#if canUpdate}
|
||||||
|
{$_('toolbar.time.help')}
|
||||||
|
{:else}
|
||||||
|
{$_('toolbar.time.help_invalid_selection')}
|
||||||
|
{/if}
|
||||||
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(input[type='time']) {
|
div :global(input[type='time']) {
|
||||||
/*
|
/*
|
||||||
Style copy-pasted from shadcn-svelte Input.
|
Style copy-pasted from shadcn-svelte Input.
|
||||||
Needed to use native time input to avoid a bug with 2-level bind:value.
|
Needed to use native time input to avoid a bug with 2-level bind:value.
|
||||||
*/
|
*/
|
||||||
@apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
@apply flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,283 +1,292 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
|
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
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 'svelte-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';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { MapPin, CircleX, Save } from 'lucide-svelte';
|
import { MapPin, CircleX, Save } from 'lucide-svelte';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
let name: string;
|
let name: string;
|
||||||
let description: string;
|
let description: string;
|
||||||
let link: string;
|
let link: string;
|
||||||
let longitude: number;
|
let longitude: number;
|
||||||
let latitude: number;
|
let latitude: number;
|
||||||
|
|
||||||
let selectedSymbol = {
|
let selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { treeFileView } = settings;
|
const { treeFileView } = settings;
|
||||||
|
|
||||||
$: canCreate = $selection.size > 0;
|
$: canCreate = $selection.size > 0;
|
||||||
|
|
||||||
$: if ($treeFileView && $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];
|
||||||
if (item instanceof ListWaypointItem) {
|
if (item instanceof ListWaypointItem) {
|
||||||
let file = getFile(item.getFileId());
|
let file = getFile(item.getFileId());
|
||||||
let waypoint = file?.wpt[item.getWaypointIndex()];
|
let waypoint = file?.wpt[item.getWaypointIndex()];
|
||||||
if (waypoint) {
|
if (waypoint) {
|
||||||
return [waypoint, item.getFileId()];
|
return [waypoint, item.getFileId()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let unsubscribe: (() => void) | undefined = undefined;
|
let unsubscribe: (() => void) | undefined = undefined;
|
||||||
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
|
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
if (fileStore) {
|
if (fileStore) {
|
||||||
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
|
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
|
||||||
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
|
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
|
||||||
name = $selectedWaypoint[0].name ?? '';
|
name = $selectedWaypoint[0].name ?? '';
|
||||||
description = $selectedWaypoint[0].desc ?? '';
|
description = $selectedWaypoint[0].desc ?? '';
|
||||||
if (
|
if (
|
||||||
$selectedWaypoint[0].cmt !== undefined &&
|
$selectedWaypoint[0].cmt !== undefined &&
|
||||||
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
|
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
|
||||||
) {
|
) {
|
||||||
description += '\n\n' + $selectedWaypoint[0].cmt;
|
description += '\n\n' + $selectedWaypoint[0].cmt;
|
||||||
}
|
}
|
||||||
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
|
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
|
||||||
let symbol = $selectedWaypoint[0].sym ?? '';
|
let symbol = $selectedWaypoint[0].sym ?? '';
|
||||||
let symbolKey = getSymbolKey(symbol);
|
let symbolKey = getSymbolKey(symbol);
|
||||||
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));
|
||||||
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
|
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
|
||||||
} else {
|
} else {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWaypointData() {
|
function resetWaypointData() {
|
||||||
name = '';
|
name = '';
|
||||||
description = '';
|
description = '';
|
||||||
link = '';
|
link = '';
|
||||||
selectedSymbol = {
|
selectedSymbol = {
|
||||||
value: '',
|
value: '',
|
||||||
label: ''
|
label: '',
|
||||||
};
|
};
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribe = undefined;
|
unsubscribe = undefined;
|
||||||
}
|
}
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
|
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
|
||||||
if (fileStore) {
|
if (fileStore) {
|
||||||
unsubscribe = fileStore.subscribe(updateWaypointData);
|
unsubscribe = fileStore.subscribe(updateWaypointData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOrUpdateWaypoint() {
|
function createOrUpdateWaypoint() {
|
||||||
if (typeof latitude === 'string') {
|
if (typeof latitude === 'string') {
|
||||||
latitude = parseFloat(latitude);
|
latitude = parseFloat(latitude);
|
||||||
}
|
}
|
||||||
if (typeof longitude === 'string') {
|
if (typeof longitude === 'string') {
|
||||||
longitude = parseFloat(longitude);
|
longitude = parseFloat(longitude);
|
||||||
}
|
}
|
||||||
latitude = parseFloat(latitude.toFixed(6));
|
latitude = parseFloat(latitude.toFixed(6));
|
||||||
longitude = parseFloat(longitude.toFixed(6));
|
longitude = parseFloat(longitude.toFixed(6));
|
||||||
|
|
||||||
dbUtils.addOrUpdateWaypoint(
|
dbUtils.addOrUpdateWaypoint(
|
||||||
{
|
{
|
||||||
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)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCoordinates(e: any) {
|
function setCoordinates(e: any) {
|
||||||
latitude = e.lngLat.lat.toFixed(6);
|
latitude = e.lngLat.lat.toFixed(6);
|
||||||
longitude = e.lngLat.lng.toFixed(6);
|
longitude = e.lngLat.lng.toFixed(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
|
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
|
||||||
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
|
return $_(`gpx.symbol.${a[0]}`).localeCompare($_(`gpx.symbol.${b[0]}`), $locale ?? 'en');
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let m = get(map);
|
let m = get(map);
|
||||||
m?.on('click', setCoordinates);
|
m?.on('click', setCoordinates);
|
||||||
setCrosshairCursor();
|
setCrosshairCursor();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
let m = get(map);
|
let m = get(map);
|
||||||
m?.off('click', setCoordinates);
|
m?.off('click', setCoordinates);
|
||||||
resetCursor();
|
resetCursor();
|
||||||
|
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
unsubscribe = undefined;
|
unsubscribe = undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
id="name"
|
id="name"
|
||||||
class="font-semibold h-8"
|
class="font-semibold h-8"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
/>
|
/>
|
||||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
id="description"
|
id="description"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
/>
|
/>
|
||||||
<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
|
||||||
<Select.Value />
|
id="symbol"
|
||||||
</Select.Trigger>
|
class="w-full h-8"
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
{#each sortedSymbols as [key, symbol]}
|
>
|
||||||
<Select.Item value={symbol.value}>
|
<Select.Value />
|
||||||
<span>
|
</Select.Trigger>
|
||||||
{#if symbol.icon}
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
<svelte:component
|
{#each sortedSymbols as [key, symbol]}
|
||||||
this={symbol.icon}
|
<Select.Item value={symbol.value}>
|
||||||
size="14"
|
<span>
|
||||||
class="inline-block align-sub mr-0.5"
|
{#if symbol.icon}
|
||||||
/>
|
<svelte:component
|
||||||
{:else}
|
this={symbol.icon}
|
||||||
<span class="w-4 inline-block" />
|
size="14"
|
||||||
{/if}
|
class="inline-block align-sub mr-0.5"
|
||||||
{$_(`gpx.symbol.${key}`)}
|
/>
|
||||||
</span>
|
{:else}
|
||||||
</Select.Item>
|
<span class="w-4 inline-block" />
|
||||||
{/each}
|
{/if}
|
||||||
</Select.Content>
|
{$_(`gpx.symbol.${key}`)}
|
||||||
</Select.Root>
|
</span>
|
||||||
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
</Select.Item>
|
||||||
<Input bind:value={link} id="link" class="h-8" disabled={!canCreate && !$selectedWaypoint} />
|
{/each}
|
||||||
<div class="flex flex-row gap-2">
|
</Select.Content>
|
||||||
<div class="grow">
|
</Select.Root>
|
||||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={latitude}
|
bind:value={link}
|
||||||
type="number"
|
id="link"
|
||||||
id="latitude"
|
class="h-8"
|
||||||
step={1e-6}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
min={-90}
|
/>
|
||||||
max={90}
|
<div class="flex flex-row gap-2">
|
||||||
class="text-xs h-8"
|
<div class="grow">
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||||
/>
|
<Input
|
||||||
</div>
|
bind:value={latitude}
|
||||||
<div class="grow">
|
type="number"
|
||||||
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
id="latitude"
|
||||||
<Input
|
step={1e-6}
|
||||||
bind:value={longitude}
|
min={-90}
|
||||||
type="number"
|
max={90}
|
||||||
id="longitude"
|
class="text-xs h-8"
|
||||||
step={1e-6}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
min={-180}
|
/>
|
||||||
max={180}
|
</div>
|
||||||
class="text-xs h-8"
|
<div class="grow">
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
||||||
/>
|
<Input
|
||||||
</div>
|
bind:value={longitude}
|
||||||
</div>
|
type="number"
|
||||||
</fieldset>
|
id="longitude"
|
||||||
<div class="flex flex-row gap-2 items-center">
|
step={1e-6}
|
||||||
<Button
|
min={-180}
|
||||||
variant="outline"
|
max={180}
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
class="text-xs h-8"
|
||||||
class="grow whitespace-normal h-fit"
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
on:click={createOrUpdateWaypoint}
|
/>
|
||||||
>
|
</div>
|
||||||
{#if $selectedWaypoint}
|
</div>
|
||||||
<Save size="16" class="mr-1 shrink-0" />
|
</fieldset>
|
||||||
{$_('menu.metadata.save')}
|
<div class="flex flex-row gap-2 items-center">
|
||||||
{:else}
|
<Button
|
||||||
<MapPin size="16" class="mr-1 shrink-0" />
|
variant="outline"
|
||||||
{$_('toolbar.waypoint.create')}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
{/if}
|
class="grow whitespace-normal h-fit"
|
||||||
</Button>
|
on:click={createOrUpdateWaypoint}
|
||||||
<Button
|
>
|
||||||
variant="outline"
|
{#if $selectedWaypoint}
|
||||||
on:click={() => {
|
<Save size="16" class="mr-1 shrink-0" />
|
||||||
selectedWaypoint.set(undefined);
|
{$_('menu.metadata.save')}
|
||||||
resetWaypointData();
|
{:else}
|
||||||
}}
|
<MapPin size="16" class="mr-1 shrink-0" />
|
||||||
>
|
{$_('toolbar.waypoint.create')}
|
||||||
<CircleX size="16" />
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
|
variant="outline"
|
||||||
{#if $selectedWaypoint || canCreate}
|
on:click={() => {
|
||||||
{$_('toolbar.waypoint.help')}
|
selectedWaypoint.set(undefined);
|
||||||
{:else}
|
resetWaypointData();
|
||||||
{$_('toolbar.waypoint.help_no_selection')}
|
}}
|
||||||
{/if}
|
>
|
||||||
</Help>
|
<CircleX size="16" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
|
||||||
|
{#if $selectedWaypoint || canCreate}
|
||||||
|
{$_('toolbar.waypoint.help')}
|
||||||
|
{:else}
|
||||||
|
{$_('toolbar.waypoint.help_no_selection')}
|
||||||
|
{/if}
|
||||||
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,249 +1,253 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
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 ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import {
|
import {
|
||||||
Bike,
|
Bike,
|
||||||
Footprints,
|
Footprints,
|
||||||
Waves,
|
Waves,
|
||||||
TrainFront,
|
TrainFront,
|
||||||
Route,
|
Route,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
ArrowRightLeft,
|
ArrowRightLeft,
|
||||||
Home,
|
Home,
|
||||||
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 'svelte-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';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
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';
|
||||||
import { TrackPoint } from 'gpx';
|
import { TrackPoint } from 'gpx';
|
||||||
|
|
||||||
export let minimized = false;
|
export let minimized = false;
|
||||||
export let minimizable = true;
|
export let minimizable = true;
|
||||||
export let popup: mapboxgl.Popup | undefined = undefined;
|
export let popup: mapboxgl.Popup | undefined = undefined;
|
||||||
export let popupElement: HTMLElement | undefined = undefined;
|
export let popupElement: HTMLElement | undefined = undefined;
|
||||||
let selectedItem: ListItem | null = null;
|
let selectedItem: ListItem | null = null;
|
||||||
|
|
||||||
const { privateRoads, routing } = settings;
|
const { privateRoads, routing } = settings;
|
||||||
|
|
||||||
$: if ($map && popup && popupElement) {
|
$: if ($map && popup && popupElement) {
|
||||||
// remove controls for deleted files
|
// remove controls for deleted files
|
||||||
routingControls.forEach((controls, fileId) => {
|
routingControls.forEach((controls, fileId) => {
|
||||||
if (!$fileObservers.has(fileId)) {
|
if (!$fileObservers.has(fileId)) {
|
||||||
controls.destroy();
|
controls.destroy();
|
||||||
routingControls.delete(fileId);
|
routingControls.delete(fileId);
|
||||||
|
|
||||||
if (selectedItem && selectedItem.getFileId() === fileId) {
|
if (selectedItem && selectedItem.getFileId() === fileId) {
|
||||||
selectedItem = null;
|
selectedItem = null;
|
||||||
}
|
}
|
||||||
} else if ($map !== controls.map) {
|
} else if ($map !== controls.map) {
|
||||||
controls.updateMap($map);
|
controls.updateMap($map);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 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)
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
||||||
|
|
||||||
function createFileWithPoint(e: any) {
|
function createFileWithPoint(e: any) {
|
||||||
if ($selection.size === 0) {
|
if ($selection.size === 0) {
|
||||||
let file = newGPXFile();
|
let file = newGPXFile();
|
||||||
file.replaceTrackPoints(0, 0, 0, 0, [
|
file.replaceTrackPoints(0, 0, 0, 0, [
|
||||||
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);
|
||||||
selectFileWhenLoaded(file._data.id);
|
selectFileWhenLoaded(file._data.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setCrosshairCursor();
|
setCrosshairCursor();
|
||||||
$map?.on('click', createFileWithPoint);
|
$map?.on('click', createFileWithPoint);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
$map?.off('click', createFileWithPoint);
|
$map?.off('click', createFileWithPoint);
|
||||||
|
|
||||||
routingControls.forEach((controls) => controls.destroy());
|
routingControls.forEach((controls) => controls.destroy());
|
||||||
routingControls.clear();
|
routingControls.clear();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if minimizable && minimized}
|
{#if minimizable && minimized}
|
||||||
<div class="-m-1.5 -mb-2">
|
<div class="-m-1.5 -mb-2">
|
||||||
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
|
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
|
||||||
<SquareArrowOutDownRight size="18" />
|
<SquareArrowOutDownRight size="18" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"
|
class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"
|
||||||
in:flyAndScale={{ x: -2, y: 0, duration: 50 }}
|
in:flyAndScale={{ x: -2, y: 0, duration: 50 }}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<Label class="flex flex-row justify-between items-center gap-2">
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<span class="flex flex-row items-center gap-1">
|
<span class="flex flex-row items-center gap-1">
|
||||||
{#if $routing}
|
{#if $routing}
|
||||||
<Route size="16" />
|
<Route size="16" />
|
||||||
{:else}
|
{:else}
|
||||||
<RouteOff size="16" />
|
<RouteOff size="16" />
|
||||||
{/if}
|
{/if}
|
||||||
{$_('toolbar.routing.use_routing')}
|
{$_('toolbar.routing.use_routing')}
|
||||||
</span>
|
</span>
|
||||||
<Tooltip label={$_('toolbar.routing.use_routing_tooltip')}>
|
<Tooltip label={$_('toolbar.routing.use_routing_tooltip')}>
|
||||||
<Switch class="scale-90" bind:checked={$routing} />
|
<Switch class="scale-90" bind:checked={$routing} />
|
||||||
<Shortcut slot="extra" key="F5" />
|
<Shortcut slot="extra" key="F5" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
{#if $routing}
|
{#if $routing}
|
||||||
<div class="flex flex-col gap-3" in:slide>
|
<div class="flex flex-col gap-3" in:slide>
|
||||||
<Label class="flex flex-row justify-between items-center gap-2">
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<span class="shrink-0 flex flex-row items-center gap-1">
|
<span class="shrink-0 flex flex-row items-center gap-1">
|
||||||
{#if $routingProfileSelectItem.value.includes('bike') || $routingProfileSelectItem.value.includes('motorcycle')}
|
{#if $routingProfileSelectItem.value.includes('bike') || $routingProfileSelectItem.value.includes('motorcycle')}
|
||||||
<Bike size="16" />
|
<Bike size="16" />
|
||||||
{:else if $routingProfileSelectItem.value.includes('foot')}
|
{:else if $routingProfileSelectItem.value.includes('foot')}
|
||||||
<Footprints size="16" />
|
<Footprints size="16" />
|
||||||
{:else if $routingProfileSelectItem.value.includes('water')}
|
{:else if $routingProfileSelectItem.value.includes('water')}
|
||||||
<Waves size="16" />
|
<Waves size="16" />
|
||||||
{:else if $routingProfileSelectItem.value.includes('railway')}
|
{:else if $routingProfileSelectItem.value.includes('railway')}
|
||||||
<TrainFront size="16" />
|
<TrainFront size="16" />
|
||||||
{/if}
|
{/if}
|
||||||
{$_('toolbar.routing.activity')}
|
{$_('toolbar.routing.activity')}
|
||||||
</span>
|
</span>
|
||||||
<Select.Root bind:selected={$routingProfileSelectItem}>
|
<Select.Root bind:selected={$routingProfileSelectItem}>
|
||||||
<Select.Trigger class="h-8 grow">
|
<Select.Trigger class="h-8 grow">
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each Object.keys(brouterProfiles) as profile}
|
{#each Object.keys(brouterProfiles) as profile}
|
||||||
<Select.Item value={profile}
|
<Select.Item value={profile}
|
||||||
>{$_(`toolbar.routing.activities.${profile}`)}</Select.Item
|
>{$_(`toolbar.routing.activities.${profile}`)}</Select.Item
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row justify-between items-center gap-2">
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<span class="flex flex-row gap-1">
|
<span class="flex flex-row gap-1">
|
||||||
<TriangleAlert size="16" />
|
<TriangleAlert size="16" />
|
||||||
{$_('toolbar.routing.allow_private')}
|
{$_('toolbar.routing.allow_private')}
|
||||||
</span>
|
</span>
|
||||||
<Switch class="scale-90" bind:checked={$privateRoads} />
|
<Switch class="scale-90" bind:checked={$privateRoads} />
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-wrap justify-center gap-1">
|
<div class="flex flex-row flex-wrap justify-center gap-1">
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
label={$_('toolbar.routing.reverse.tooltip')}
|
label={$_('toolbar.routing.reverse.tooltip')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
on:click={dbUtils.reverseSelection}
|
on:click={dbUtils.reverseSelection}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft size="12" />{$_('toolbar.routing.reverse.button')}
|
<ArrowRightLeft size="12" />{$_('toolbar.routing.reverse.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
label={$_('toolbar.routing.route_back_to_start.tooltip')}
|
label={$_('toolbar.routing.route_back_to_start.tooltip')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const selected = getOrderedSelection();
|
const selected = getOrderedSelection();
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
const firstFileId = selected[0].getFileId();
|
const firstFileId = selected[0].getFileId();
|
||||||
const firstFile = getFile(firstFileId);
|
const firstFile = getFile(firstFileId);
|
||||||
if (firstFile) {
|
if (firstFile) {
|
||||||
let start = (() => {
|
let start = (() => {
|
||||||
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]
|
||||||
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
?.trkpt[0];
|
||||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
||||||
selected[0].getSegmentIndex()
|
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
||||||
]?.trkpt[0];
|
selected[0].getSegmentIndex()
|
||||||
}
|
]?.trkpt[0];
|
||||||
})();
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
if (start !== undefined) {
|
if (start !== undefined) {
|
||||||
const lastFileId = selected[selected.length - 1].getFileId();
|
const lastFileId = selected[selected.length - 1].getFileId();
|
||||||
routingControls
|
routingControls
|
||||||
.get(lastFileId)
|
.get(lastFileId)
|
||||||
?.appendAnchorWithCoordinates(start.getCoordinates());
|
?.appendAnchorWithCoordinates(start.getCoordinates());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Home size="12" />{$_('toolbar.routing.route_back_to_start.button')}
|
<Home size="12" />{$_('toolbar.routing.route_back_to_start.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
label={$_('toolbar.routing.round_trip.tooltip')}
|
label={$_('toolbar.routing.round_trip.tooltip')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
on:click={dbUtils.createRoundTripForSelection}
|
on:click={dbUtils.createRoundTripForSelection}
|
||||||
>
|
>
|
||||||
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
|
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
||||||
<Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
|
<Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
|
||||||
{#if !validSelection}
|
{#if !validSelection}
|
||||||
{$_('toolbar.routing.help_no_file')}
|
{$_('toolbar.routing.help_no_file')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('toolbar.routing.help')}
|
{$_('toolbar.routing.help')}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="px-1 h-6"
|
class="px-1 h-6"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (minimizable) {
|
if (minimizable) {
|
||||||
minimized = true;
|
minimized = true;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SquareArrowUpLeft size="18" />
|
<SquareArrowUpLeft size="18" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user