kopia lustrzana https://github.com/FacilMap/facilmap
Porównaj commity
13 Commity
bc3cf1098b
...
b7eaa44324
Autor | SHA1 | Data |
---|---|---|
Candid Dauth | b7eaa44324 | |
Candid Dauth | bc4d97d998 | |
Candid Dauth | db8d7d6080 | |
Candid Dauth | 3d3110607c | |
Candid Dauth | 21e7aa70e3 | |
Candid Dauth | e7c377105f | |
Candid Dauth | c97e76a6b4 | |
Candid Dauth | 1b5ad5838d | |
Candid Dauth | b42e54245d | |
Candid Dauth | 26753061a7 | |
Candid Dauth | 5071e5b417 | |
Candid Dauth | 16546e1585 | |
Candid Dauth | e3660ba870 |
|
@ -40,6 +40,7 @@ module.exports = {
|
|||
"@typescript-eslint/no-base-to-string": ["error"],
|
||||
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
|
||||
"vue/return-in-computed-property": ["off"],
|
||||
"@typescript-eslint/no-floating-promises": ["error"],
|
||||
|
||||
"constructor-super": ["error"],
|
||||
"for-direction": ["error"],
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class Client {
|
|||
this.on(i, this._handlers[i] as EventHandler<ClientEvents, typeof i>);
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
this._simulateEvent("loadStart");
|
||||
});
|
||||
|
||||
|
|
|
@ -24,10 +24,6 @@
|
|||
# Get an API key on https://www.mapbox.com/signup/
|
||||
#MAPBOX_TOKEN=
|
||||
|
||||
# MapZen is used for getting elevation information
|
||||
# Get an API key on https://mapzen.com/developers/sign_up
|
||||
#MAPZEN_TOKEN=
|
||||
|
||||
# Maxmind configuration. If specified, the maxmind GeoLite2 database will be downloaded
|
||||
# for Geo IP lookup (to show the initial map state) and kept in memory.
|
||||
# Sign up here: https://www.maxmind.com/en/geolite2/signup
|
||||
|
|
|
@ -18,17 +18,17 @@ The config of the FacilMap server can be set either by using environment variabl
|
|||
| `DB_PASSWORD` | | `facilmap` | The password to connect to the database with. |
|
||||
| `ORS_TOKEN` | * | | [OpenRouteService API key](https://openrouteservice.org/). |
|
||||
| `MAPBOX_TOKEN` | * | | [Mapbox API key](https://www.mapbox.com/signup/). |
|
||||
| `MAPZEN_TOKEN` | | | [Mapzen API key](https://mapzen.com/developers/sign_up). |
|
||||
| `MAXMIND_USER_ID` | | | [MaxMind user ID](https://www.maxmind.com/en/geolite2/signup). |
|
||||
| `MAXMIND_LICENSE_KEY` | | | MaxMind license key. |
|
||||
| `LIMA_LABS_TOKEN` | | | [Lima Labs](https://maps.lima-labs.com/) API key |
|
||||
| `HIDE_COMMERCIAL_MAP_LINKS` | | | Set to `1` to hide the links to Google/Bing Maps in the “Map style” menu. |
|
||||
| `CUSTOM_CSS_FILE` | | | The path of a CSS file that should be included ([see more details below](#custom-css-file)).
|
||||
| `CUSTOM_CSS_FILE` | | | The path of a CSS file that should be included ([see more details below](#custom-css-file)). |
|
||||
| `NOMINATIM_URL` | | `https://nominatim.openstreetmap.org` | The URL to the Nominatim server (used to search for places). |
|
||||
| `OPEN_ELEVATION_URL` | | `https://api.open-elevation.com` | The URL to the Open Elevation server (used to look up the elevation for markers). |
|
||||
|
||||
FacilMap makes use of several third-party services that require you to register (for free) and generate an API key:
|
||||
* Mapbox and OpenRouteService are used for calculating routes. Mapbox is used for basic routes, OpenRouteService is used when custom route mode settings are made. If these API keys are not defined, calculating routes will fail.
|
||||
* Maxmind provides a free database that maps IP addresses to approximate locations. FacilMap downloads this database to decide the initial map view for users (IP addresses are looked up in FacilMap’s copy of the database, on IP addresses are sent to Maxmind). This API key is optional, if it is not set, the default view will be the whole world.
|
||||
* Mapzen is used to look up the elevation info for search results. The API key is optional, if it is not set, no elevation info will be available for search results.
|
||||
* Lima Labs is used for nicer and higher resolution map tiles than Mapnik. The API key is optional, if it is not set, Mapnik will be the default map style instead.
|
||||
|
||||
## Custom CSS file
|
||||
|
|
|
@ -34,7 +34,6 @@ services:
|
|||
DB_PASSWORD: password
|
||||
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
|
||||
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
|
||||
MAPZEN_TOKEN: # Get an API key on https://mapzen.com/developers/sign_up (needed for elevation info)
|
||||
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
|
||||
MAXMIND_LICENSE_KEY:
|
||||
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
|
||||
|
@ -76,7 +75,6 @@ services:
|
|||
DB_PASSWORD: password
|
||||
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
|
||||
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
|
||||
MAPZEN_TOKEN: # Get an API key on https://mapzen.com/developers/sign_up (needed for elevation info)
|
||||
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
|
||||
MAXMIND_LICENSE_KEY:
|
||||
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
|
||||
|
@ -100,5 +98,5 @@ To manually create the necessary docker containers, use these commands:
|
|||
|
||||
```bash
|
||||
docker create --name=facilmap_db -e MYSQL_DATABASE=facilmap -e MYSQL_USER=facilmap -e MYSQL_PASSWORD=password -e MYSQL_RANDOM_ROOT_PASSWORD=true --restart=unless-stopped mariadb --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
docker create --link=facilmap_db -p 8080 --name=facilmap -e "USER_AGENT=My FacilMap (https://facilmap.example.org/, facilmap@example.org)" -e TRUST_PROXY=true -e DB_TYPE=mysql -e DB_HOST=facilmap_db -e DB_NAME=facilmap -e DB_USER=facilmap -e DB_PASSWORD=facilmap -e ORS_TOKEN= -e MAPBOX_TOKEN= -e MAPZEN_TOKEN= -e MAXMIND_USER_ID= -e MAXMIND_LICENSE_KEY= -e LIMA_LABS_TOKEN= --restart=unless-stopped facilmap/facilmap
|
||||
docker create --link=facilmap_db -p 8080 --name=facilmap -e "USER_AGENT=My FacilMap (https://facilmap.example.org/, facilmap@example.org)" -e TRUST_PROXY=true -e DB_TYPE=mysql -e DB_HOST=facilmap_db -e DB_NAME=facilmap -e DB_USER=facilmap -e DB_PASSWORD=facilmap -e ORS_TOKEN= -e MAPBOX_TOKEN= -e MAXMIND_USER_ID= -e MAXMIND_LICENSE_KEY= -e LIMA_LABS_TOKEN= --restart=unless-stopped facilmap/facilmap
|
||||
```
|
|
@ -57,7 +57,7 @@
|
|||
function handleMapDrop(event: Event): void {
|
||||
event.preventDefault();
|
||||
|
||||
importFiles((event as DragEvent).dataTransfer?.files);
|
||||
void importFiles((event as DragEvent).dataTransfer?.files);
|
||||
}
|
||||
|
||||
async function importFiles(fileList: FileList | undefined): Promise<void> {
|
||||
|
|
|
@ -242,7 +242,7 @@ function useSelectionHandler(map: Ref<Map>, context: FacilMapContext, mapContext
|
|||
});
|
||||
|
||||
selectionHandler.on("fmLongClick", (event: any) => {
|
||||
context.components.clickMarkerTab?.openClickMarker({ lat: event.latlng.lat, lon: event.latlng.lng });
|
||||
void context.components.clickMarkerTab?.openClickMarker({ lat: event.latlng.lat, lon: event.latlng.lng });
|
||||
});
|
||||
|
||||
return selectionHandler;
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
toasts.showToast(`fm${context.id}-line-info-move`, `Edit waypoints`, "Use the routing form or drag the line around to change it. Click “Finish” to save the changes.", {
|
||||
noCloseButton: true,
|
||||
actions: [
|
||||
{ label: "Finish", variant: "primary", onClick: () => { done(true); }},
|
||||
{ label: "Cancel", onClick: () => { done(false); } }
|
||||
{ label: "Finish", variant: "primary", onClick: () => { void done(true); }},
|
||||
{ label: "Cancel", onClick: () => { void done(false); } }
|
||||
]
|
||||
});
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
<template v-if="marker.ele != null">
|
||||
<dt class="elevation">Elevation</dt>
|
||||
<dd class="elevation">{{marker.ele}}^m</dd>
|
||||
<dd class="elevation">{{marker.ele}} m</dd>
|
||||
</template>
|
||||
|
||||
<template v-for="field in client.types[marker.typeId].fields" :key="field.name">
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
draggable.on({
|
||||
insert: (e: any) => {
|
||||
destinations.value.splice(e.idx, 0, makeCoordDestination(e.latlng));
|
||||
reroute(false);
|
||||
void reroute(false);
|
||||
},
|
||||
dragstart: (e: any) => {
|
||||
hoverDestinationIdx.value = e.idx;
|
||||
|
@ -153,12 +153,12 @@
|
|||
}, 300),
|
||||
dragend: (e: any) => {
|
||||
destinations.value[e.idx] = makeCoordDestination(e.to);
|
||||
reroute(false);
|
||||
void reroute(false);
|
||||
},
|
||||
remove: (e: any) => {
|
||||
hoverDestinationIdx.value = undefined;
|
||||
destinations.value.splice(e.idx, 1);
|
||||
reroute(false);
|
||||
void reroute(false);
|
||||
},
|
||||
dragmouseover: (e: any) => {
|
||||
destinationMouseOver(e.idx);
|
||||
|
@ -227,7 +227,7 @@
|
|||
});
|
||||
|
||||
watch(routeMode, () => {
|
||||
reroute(false);
|
||||
void reroute(false);
|
||||
});
|
||||
|
||||
function addDestination(): void {
|
||||
|
@ -388,7 +388,7 @@
|
|||
|
||||
const marker = routeLayer._draggableLines?.dragMarkers[idx];
|
||||
if (marker) {
|
||||
Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
// If mouseout event is directly followed by a dragend event, the marker will be removed. Only update the icon if the marker is not removed.
|
||||
if (marker["_map"])
|
||||
marker.setIcon(getIcon(idx, routeLayer._draggableLines!.dragMarkers.length));
|
||||
|
@ -456,7 +456,7 @@
|
|||
const points = destinations.value.map((dest) => getSelectedSuggestion(dest));
|
||||
|
||||
if(!points.some((point) => point == null))
|
||||
route(zoom, smooth);
|
||||
await route(zoom, smooth);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -485,7 +485,7 @@
|
|||
|
||||
function handleSubmit(event: Event): void {
|
||||
submitButton.value?.focus();
|
||||
route(true);
|
||||
void route(true);
|
||||
}
|
||||
|
||||
const linesWithTags = computed((): LineWithTags[] | undefined => routeObj.value && [{
|
||||
|
@ -504,22 +504,22 @@
|
|||
while (destinations.value.length < 2)
|
||||
destinations.value.push({ query: "" });
|
||||
routeMode.value = split.mode ?? "car";
|
||||
route(zoom, smooth);
|
||||
void route(zoom, smooth);
|
||||
}
|
||||
|
||||
function setFrom(data: Parameters<typeof makeDestination>[0]): void {
|
||||
destinations.value[0] = makeDestination(data);
|
||||
reroute(true);
|
||||
void reroute(true);
|
||||
}
|
||||
|
||||
function addVia(data: Parameters<typeof makeDestination>[0]): void {
|
||||
destinations.value.splice(destinations.value.length - 1, 0, makeDestination(data));
|
||||
reroute(true);
|
||||
void reroute(true);
|
||||
}
|
||||
|
||||
function setTo(data: Parameters<typeof makeDestination>[0]): void {
|
||||
destinations.value[destinations.value.length - 1] = makeDestination(data);
|
||||
reroute(true);
|
||||
void reroute(true);
|
||||
}
|
||||
|
||||
defineExpose({ setQuery, setFrom, addVia, setTo });
|
||||
|
|
|
@ -48,13 +48,13 @@
|
|||
restoreHeight.value = undefined;
|
||||
|
||||
if (expand) {
|
||||
nextTick(() => {
|
||||
void nextTick(() => {
|
||||
doExpand();
|
||||
});
|
||||
}
|
||||
|
||||
if (autofocus && !context.isNarrow) {
|
||||
nextTick(() => {
|
||||
void nextTick(() => {
|
||||
containerRef.value?.querySelector<HTMLElement>(":scope > .card-body.active [autofocus],:scope > .card-body.active .fm-autofocus")?.focus();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
function handleSubmit(): void {
|
||||
searchInput.value?.blur();
|
||||
|
||||
search(storage.autoZoom, storage.zoomToAll);
|
||||
void search(storage.autoZoom, storage.zoomToAll);
|
||||
}
|
||||
|
||||
async function search(zoom: boolean, zoomToAll?: boolean, smooth = true): Promise<void> {
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
await sleep(0); // For some reason this is necessary for the dropdown to close itself
|
||||
drawMarker(type, context, toasts);
|
||||
} else if(type.type == "line") {
|
||||
drawLine(type, context, toasts);
|
||||
await drawLine(type, context, toasts);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
const newVal = arrowNavigation(colours, value.value, gridRef.value, event);
|
||||
if (newVal) {
|
||||
emit('update:modelValue', newVal);
|
||||
nextTick(() => {
|
||||
void nextTick(() => {
|
||||
(gridRef.value?.querySelector(".active a") as HTMLAnchorElement | undefined)?.focus();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
const newVal = arrowNavigation(Object.keys(items), props.modelValue, gridRef.value!.containerRef!, event);
|
||||
if (newVal) {
|
||||
emit("update:modelValue", newVal);
|
||||
nextTick(() => {
|
||||
void nextTick(() => {
|
||||
gridRef.value?.containerRef?.querySelector<HTMLElement>(".active > a")?.focus();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -79,12 +79,12 @@
|
|||
try {
|
||||
const res = callback(...args);
|
||||
Promise.resolve(res).catch((err) => {
|
||||
result.showErrorToast(undefined, 'Unexpected error', err);
|
||||
void result.showErrorToast(undefined, 'Unexpected error', err);
|
||||
throw err;
|
||||
});
|
||||
return res;
|
||||
} catch (err: any) {
|
||||
result.showErrorToast(undefined, 'Unexpected error', err);
|
||||
void result.showErrorToast(undefined, 'Unexpected error', err);
|
||||
}
|
||||
}) as C;
|
||||
},
|
||||
|
@ -92,7 +92,7 @@
|
|||
showToast: async (id, title, message, options = {}) => {
|
||||
await appMountP;
|
||||
if (id != null) {
|
||||
result.hideToast(id);
|
||||
void result.hideToast(id);
|
||||
}
|
||||
|
||||
const toast: ToastInstance = { ...options, key: getUniqueId("fm-toast"), id, title, message, contextId };
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
function resolveValidationResult(results: ValidationResult[], signal: AbortSignal): AsyncValidationResult {
|
||||
return new Promise<SyncValidationResult>((resolve, reject) => {
|
||||
for (const result of results) {
|
||||
Promise.resolve(result).then((res) => {
|
||||
void Promise.resolve(result).then((res) => {
|
||||
if (res) {
|
||||
resolve(res);
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
if (validationPromises.get(element) !== promise) {
|
||||
validationPromises.set(element, promise);
|
||||
isValidating.set(element, true);
|
||||
promise.finally(() => {
|
||||
void promise.finally(() => {
|
||||
if (validationPromises.get(element) === promise) {
|
||||
isValidating.set(element, false);
|
||||
}
|
||||
|
@ -90,7 +90,7 @@
|
|||
|
||||
useDomEventListener(formRef, "submit", (e) => {
|
||||
e.preventDefault();
|
||||
data.submit();
|
||||
void data.submit();
|
||||
});
|
||||
|
||||
allForms.set(formRef, data);
|
||||
|
|
|
@ -73,13 +73,13 @@ export function moveMarker(markerId: ID, context: FacilMapContext, toasts: Toast
|
|||
label: "Save",
|
||||
variant: "primary",
|
||||
onClick: () => {
|
||||
finish(true);
|
||||
void finish(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
onClick: () => {
|
||||
finish(false);
|
||||
void finish(false);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -41,17 +41,15 @@ function load(): void {
|
|||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
async function save() {
|
||||
try {
|
||||
const currentItem = JSON.parse(localStorage.getItem("facilmap") || "null");
|
||||
if (!currentItem || !isEqual(currentItem, storage)) {
|
||||
localStorage.setItem("facilmap", JSON.stringify(storage));
|
||||
|
||||
if (storage.bookmarks.length > 0 && !isEqual(currentItem?.bookmarks, storage.bookmarks) && navigator.storage?.persist)
|
||||
navigator.storage.persist();
|
||||
await navigator.storage.persist();
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error saving to local storage", err);
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ export const extendableEventMixin: ExtendableEventMixin = {
|
|||
} else if (this._promises) {
|
||||
this._promises.push(promise);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this._promises = [promise];
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,8 +14,11 @@ if (import.meta.hot) {
|
|||
});
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator && location.hostname !== "localhost")
|
||||
navigator.serviceWorker.register('./_app/static/sw.js', { scope: "./" });
|
||||
if ('serviceWorker' in navigator && location.hostname !== "localhost") {
|
||||
navigator.serviceWorker.register('./_app/static/sw.js', { scope: "./" }).catch((err) => {
|
||||
console.error("Error registering service worker", err);
|
||||
});
|
||||
}
|
||||
|
||||
setLayerOptions({
|
||||
limaLabsToken: config.limaLabsToken
|
||||
|
|
|
@ -0,0 +1,598 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, emit, getTemporaryPadData, openClient, openSocket, retry } from "./utils";
|
||||
import { SocketVersion, CRU, type Line, type LinePointsEvent, type FindOnMapLine } from "facilmap-types";
|
||||
import type { LineWithTrackPoints } from "facilmap-client";
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
|
||||
test("Create line (using default values)", async () => {
|
||||
// client1: Creates the line and has it in its bbox
|
||||
// client2: Has the line in its bbox
|
||||
// client3: Does not have the line in its bbox
|
||||
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
const client3 = await openClient(padData.id);
|
||||
|
||||
const onLine1 = vi.fn();
|
||||
client1.on("line", onLine1);
|
||||
const onLinePoints1 = vi.fn();
|
||||
client1.on("linePoints", onLinePoints1);
|
||||
const onLine2 = vi.fn();
|
||||
client2.on("line", onLine2);
|
||||
const onLinePoints2 = vi.fn();
|
||||
client2.on("linePoints", onLinePoints2);
|
||||
const onLine3 = vi.fn();
|
||||
client3.on("line", onLine3);
|
||||
const onLinePoints3 = vi.fn();
|
||||
client3.on("linePoints", onLinePoints3);
|
||||
|
||||
const lineType = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
|
||||
await client1.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client2.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client3.updateBbox({ top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const line = await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const expectedLine = {
|
||||
id: line.id,
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id,
|
||||
padId: padData.id,
|
||||
name: "",
|
||||
mode: "",
|
||||
colour: "0000ff",
|
||||
width: 4,
|
||||
stroke: "",
|
||||
data: {},
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
distance: 1247.95,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
time: null,
|
||||
extraInfo: null
|
||||
} satisfies Line;
|
||||
|
||||
const expectedLinePointsEvent = {
|
||||
id: line.id,
|
||||
trackPoints: [
|
||||
{ lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
{ lat: 14, lon: 14, idx: 1, zoom: 1, ele: null }
|
||||
],
|
||||
reset: true
|
||||
} satisfies LinePointsEvent;
|
||||
|
||||
const expectedLineWithEmptyTrackPoints = {
|
||||
...expectedLine,
|
||||
trackPoints: {
|
||||
length: 0
|
||||
}
|
||||
} satisfies LineWithTrackPoints;
|
||||
|
||||
const expectedLineWithTrackPoints = {
|
||||
...expectedLine,
|
||||
trackPoints: {
|
||||
0: { lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
1: { lat: 14, lon: 14, idx: 1, zoom: 1, ele: null },
|
||||
length: 2
|
||||
}
|
||||
} satisfies LineWithTrackPoints;
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints1).toHaveBeenCalledTimes(1);
|
||||
expect(onLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints2).toHaveBeenCalledTimes(1);
|
||||
expect(onLine3).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onLine1).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLinePoints1).toHaveBeenCalledWith(expectedLinePointsEvent);
|
||||
expect(onLine2).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine3).toHaveBeenCalledWith(expectedLine);
|
||||
|
||||
expect(cloneDeep(client1.lines)).toEqual({ [expectedLine.id]: expectedLineWithTrackPoints });
|
||||
expect(cloneDeep(client2.lines)).toEqual({ [expectedLine.id]: expectedLineWithTrackPoints });
|
||||
expect(cloneDeep(client3.lines)).toEqual({ [expectedLine.id]: expectedLineWithEmptyTrackPoints });
|
||||
});
|
||||
});
|
||||
|
||||
test("Create line (using custom values)", async () => {
|
||||
const client = await openClient();
|
||||
await client.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
|
||||
await createTemporaryPad(client, {}, async (createPadData, padData) => {
|
||||
const lineType = Object.values(client.types).find((t) => t.type === "line")!;
|
||||
|
||||
const data: Line<CRU.CREATE> = {
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 12, lon: 12 }
|
||||
],
|
||||
typeId: lineType.id,
|
||||
name: "Test line",
|
||||
mode: "track",
|
||||
colour: "0000ff",
|
||||
width: 10,
|
||||
stroke: "dotted",
|
||||
data: {
|
||||
test: "value"
|
||||
},
|
||||
trackPoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 },
|
||||
{ lat: 12, lon: 12 }
|
||||
]
|
||||
};
|
||||
|
||||
const line = await client.addLine(data);
|
||||
|
||||
const expectedLine = {
|
||||
id: line.id,
|
||||
padId: padData.id,
|
||||
...omit(data, ["trackPoints"]),
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
distance: 1558.44,
|
||||
time: null,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
extraInfo: null
|
||||
};
|
||||
|
||||
const expectedLineWithTrackPoints = {
|
||||
...expectedLine,
|
||||
trackPoints: {
|
||||
0: { lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
1: { lat: 14, lon: 14, idx: 1, zoom: 1, ele: null },
|
||||
2: { lat: 12, lon: 12, idx: 2, zoom: 1, ele: null },
|
||||
length: 3
|
||||
}
|
||||
};
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
await retry(() => {
|
||||
expect(cloneDeep(client.lines)).toEqual({
|
||||
[expectedLine.id]: expectedLineWithTrackPoints
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Edit line", async () => {
|
||||
// client1: Creates the line and has it in its bbox
|
||||
// client2: Has the line in its bbox
|
||||
// client3: Does not have the line in its bbox
|
||||
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
const client3 = await openClient(padData.id);
|
||||
|
||||
const lineType = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
|
||||
await client1.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client2.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client3.updateBbox({ top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const createdLine = await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const secondType = await client1.addType({
|
||||
type: "line",
|
||||
name: "Second type"
|
||||
});
|
||||
|
||||
const onLine1 = vi.fn();
|
||||
client1.on("line", onLine1);
|
||||
const onLinePoints1 = vi.fn();
|
||||
client1.on("linePoints", onLinePoints1);
|
||||
const onLine2 = vi.fn();
|
||||
client2.on("line", onLine2);
|
||||
const onLinePoints2 = vi.fn();
|
||||
client2.on("linePoints", onLinePoints2);
|
||||
const onLine3 = vi.fn();
|
||||
client3.on("line", onLine3);
|
||||
const onLinePoints3 = vi.fn();
|
||||
client3.on("linePoints", onLinePoints3);
|
||||
|
||||
const newData = {
|
||||
id: createdLine.id,
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 12, lon: 12 }
|
||||
],
|
||||
typeId: secondType.id,
|
||||
name: "Test line",
|
||||
mode: "track",
|
||||
colour: "0000ff",
|
||||
width: 10,
|
||||
stroke: "dotted" as const,
|
||||
data: {
|
||||
test: "value"
|
||||
},
|
||||
trackPoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 },
|
||||
{ lat: 12, lon: 12 }
|
||||
]
|
||||
} satisfies Line<CRU.UPDATE>;
|
||||
const line = await client1.editLine(newData);
|
||||
|
||||
const expectedLine = {
|
||||
padId: padData.id,
|
||||
...omit(newData, ["trackPoints"]),
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
distance: 1558.44,
|
||||
time: null,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
extraInfo: null
|
||||
} satisfies Line;
|
||||
|
||||
const expectedLinePointsEvent = {
|
||||
id: line.id,
|
||||
trackPoints: [
|
||||
{ lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
{ lat: 14, lon: 14, idx: 1, zoom: 1, ele: null },
|
||||
{ lat: 12, lon: 12, idx: 2, zoom: 1, ele: null }
|
||||
],
|
||||
reset: true
|
||||
} satisfies LinePointsEvent;
|
||||
|
||||
const expectedLineWithEmptyTrackPoints = {
|
||||
...expectedLine,
|
||||
trackPoints: {
|
||||
length: 0
|
||||
}
|
||||
} satisfies LineWithTrackPoints;
|
||||
|
||||
const expectedLineWithTrackPoints = {
|
||||
...expectedLine,
|
||||
trackPoints: {
|
||||
0: { lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
1: { lat: 14, lon: 14, idx: 1, zoom: 1, ele: null },
|
||||
2: { lat: 12, lon: 12, idx: 2, zoom: 1, ele: null },
|
||||
length: 3
|
||||
}
|
||||
};
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints1).toHaveBeenCalledTimes(1);
|
||||
expect(onLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints2).toHaveBeenCalledTimes(1);
|
||||
expect(onLine3).toHaveBeenCalledTimes(1);
|
||||
expect(onLinePoints3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onLine1).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLinePoints1).toHaveBeenCalledWith(expectedLinePointsEvent);
|
||||
expect(onLine2).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine3).toHaveBeenCalledWith(expectedLine);
|
||||
|
||||
expect(cloneDeep(client1.lines)).toEqual({ [expectedLine.id]: expectedLineWithTrackPoints });
|
||||
expect(cloneDeep(client2.lines)).toEqual({ [expectedLine.id]: expectedLineWithTrackPoints });
|
||||
expect(cloneDeep(client3.lines)).toEqual({ [expectedLine.id]: expectedLineWithEmptyTrackPoints });
|
||||
});
|
||||
});
|
||||
|
||||
test("Delete line", async () => {
|
||||
// client1: Creates the line and has it in its bbox
|
||||
// client2: Has the line in its bbox
|
||||
// client3: Does not have the line in its bbox
|
||||
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
const client3 = await openClient(padData.id);
|
||||
|
||||
const lineType = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
|
||||
await client1.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client2.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await client3.updateBbox({ top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const createdLine = await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const onDeleteLine1 = vi.fn();
|
||||
client1.on("deleteLine", onDeleteLine1);
|
||||
const onDeleteLine2 = vi.fn();
|
||||
client2.on("deleteLine", onDeleteLine2);
|
||||
const onDeleteLine3 = vi.fn();
|
||||
client3.on("deleteLine", onDeleteLine3);
|
||||
|
||||
const deletedLine = await client1.deleteLine({ id: createdLine.id });
|
||||
|
||||
expect(deletedLine).toEqual(createdLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onDeleteLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onDeleteLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onDeleteLine3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onDeleteLine1).toHaveBeenCalledWith({ id: deletedLine.id });
|
||||
expect(onDeleteLine2).toHaveBeenCalledWith({ id: deletedLine.id });
|
||||
expect(onDeleteLine3).toHaveBeenCalledWith({ id: deletedLine.id });
|
||||
|
||||
const expectedLineRecord = { };
|
||||
expect(cloneDeep(client1.lines)).toEqual(expectedLineRecord);
|
||||
expect(cloneDeep(client2.lines)).toEqual(expectedLineRecord);
|
||||
expect(cloneDeep(client3.lines)).toEqual(expectedLineRecord);
|
||||
});
|
||||
});
|
||||
|
||||
test("Find line", async () => {
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
|
||||
const lineType = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
|
||||
const marker = await client1.addLine({
|
||||
name: "Line test",
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const expectedResult: FindOnMapLine = {
|
||||
id: marker.id,
|
||||
kind: "line",
|
||||
similarity: 1,
|
||||
typeId: lineType.id,
|
||||
name: "Line test",
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6
|
||||
};
|
||||
|
||||
expect(await client2.findOnMap({ query: "Test" })).toEqual([{ ...expectedResult, similarity: 0.4 }]);
|
||||
expect(await client2.findOnMap({ query: "T_st" })).toEqual([{ ...expectedResult, similarity: 0.2 }]);
|
||||
expect(await client2.findOnMap({ query: "L%e" })).toEqual([{ ...expectedResult, similarity: 0 }]);
|
||||
expect(await client2.findOnMap({ query: "Bla" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test("Try to create line with marker type", async () => {
|
||||
const client = await openClient();
|
||||
|
||||
await createTemporaryPad(client, {}, async (createPadData) => {
|
||||
const lineType = Object.values(client.types).find((t) => t.type === "marker")!;
|
||||
|
||||
await expect(async () => {
|
||||
await client.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
}).rejects.toThrowError("Cannot use marker type for line");
|
||||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(cloneDeep(client3.lines)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
test("Try to update line with line type", async () => {
|
||||
const client = await openClient();
|
||||
|
||||
await createTemporaryPad(client, {}, async (createPadData) => {
|
||||
const markerType = Object.values(client.types).find((t) => t.type === "marker")!;
|
||||
const lineType = Object.values(client.types).find((t) => t.type === "line")!;
|
||||
|
||||
const line = await client.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await client.editLine({
|
||||
id: line.id,
|
||||
typeId: markerType.id
|
||||
});
|
||||
}).rejects.toThrowError("Cannot use marker type for line");
|
||||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
expect(cloneDeep(client3.lines)).toEqual({
|
||||
[line.id]: {
|
||||
...line,
|
||||
trackPoints: {
|
||||
length: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Try to create line with line type from other pad", async () => {
|
||||
const client1 = await openClient();
|
||||
const client2 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData) => {
|
||||
await createTemporaryPad(client2, {}, async () => {
|
||||
const lineType2 = Object.values(client2.types).find((t) => t.type === "line")!;
|
||||
|
||||
await expect(async () => {
|
||||
await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType2.id
|
||||
});
|
||||
}).rejects.toThrowError("could not be found");
|
||||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(cloneDeep(client3.lines)).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Try to update line with line type from other pad", async () => {
|
||||
const client1 = await openClient();
|
||||
const client2 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData) => {
|
||||
await createTemporaryPad(client2, {}, async () => {
|
||||
const lineType1 = Object.values(client1.types).find((t) => t.type === "line")!;
|
||||
const lineType2 = Object.values(client2.types).find((t) => t.type === "line")!;
|
||||
|
||||
const line = await client1.addLine({
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType1.id
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await client1.editLine({
|
||||
id: line.id,
|
||||
typeId: lineType2.id
|
||||
});
|
||||
}).rejects.toThrowError("could not be found");
|
||||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(cloneDeep(client3.lines)).toEqual({
|
||||
[line.id]: {
|
||||
...line,
|
||||
trackPoints: {
|
||||
0: { lat: 6, lon: 6, idx: 0, zoom: 1, ele: null },
|
||||
1: { lat: 14, lon: 14, idx: 1, zoom: 1, ele: null },
|
||||
length: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Socket v1 line name", async () => {
|
||||
// socket1: Creates the line and has it in its bbox
|
||||
// socket2: Has the line in its bbox
|
||||
// socket3: Does not have the line in its bbox
|
||||
const socket1 = await openSocket(SocketVersion.V1);
|
||||
const socket2 = await openSocket(SocketVersion.V1);
|
||||
const socket3 = await openSocket(SocketVersion.V1);
|
||||
|
||||
const onLine1 = vi.fn();
|
||||
socket1.on("line", onLine1);
|
||||
const onLine2 = vi.fn();
|
||||
socket2.on("line", onLine2);
|
||||
const onLine3 = vi.fn();
|
||||
socket3.on("line", onLine3);
|
||||
|
||||
try {
|
||||
const padData = getTemporaryPadData({});
|
||||
const padResult = await emit(socket1, "createPad", padData);
|
||||
await emit(socket2, "setPadId", padData.adminId);
|
||||
await emit(socket3, "setPadId", padData.adminId);
|
||||
|
||||
const lineType = padResult.type!.find((t) => t.type === "line")!;
|
||||
|
||||
await emit(socket1, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket2, "updateBbox", { top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
await emit(socket3, "updateBbox", { top: 5, bottom: 0, left: 0, right: 5, zoom: 1 });
|
||||
|
||||
const line = await emit(socket1, "addLine", {
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id
|
||||
});
|
||||
|
||||
const expectedLine = {
|
||||
id: line.id,
|
||||
routePoints: [
|
||||
{ lat: 6, lon: 6 },
|
||||
{ lat: 14, lon: 14 }
|
||||
],
|
||||
typeId: lineType.id,
|
||||
padId: padData.id,
|
||||
name: "Untitled line",
|
||||
mode: "",
|
||||
colour: "0000ff",
|
||||
width: 4,
|
||||
stroke: "",
|
||||
distance: 1247.95,
|
||||
time: null,
|
||||
ascent: null,
|
||||
descent: null,
|
||||
extraInfo: null,
|
||||
top: 14,
|
||||
right: 14,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
data: {}
|
||||
} satisfies Line;
|
||||
|
||||
expect(line).toEqual(expectedLine);
|
||||
|
||||
await retry(() => {
|
||||
expect(onLine1).toHaveBeenCalledTimes(1);
|
||||
expect(onLine2).toHaveBeenCalledTimes(1);
|
||||
expect(onLine3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onLine1).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine2).toHaveBeenCalledWith(expectedLine);
|
||||
expect(onLine3).toHaveBeenCalledWith(expectedLine);
|
||||
} finally {
|
||||
await emit(socket1, "deletePad", undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// getLineTemplate
|
||||
// exportLine
|
||||
// findOnMap
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { createTemporaryPad, emit, getTemporaryPadData, openClient, openSocket, retry } from "./utils";
|
||||
import { SocketVersion, CRU, type Marker } from "facilmap-types";
|
||||
import { SocketVersion, CRU, type Marker, type FindOnMapMarker } from "facilmap-types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
test("Create marker (using default values)", async () => {
|
||||
// client1: Creates the marker and has it in its bbox
|
||||
|
@ -44,7 +45,7 @@ test("Create marker (using default values)", async () => {
|
|||
symbol: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
ele: expect.any(Number)
|
||||
} satisfies Marker;
|
||||
|
||||
expect(marker).toEqual(expectedMarker);
|
||||
|
@ -59,9 +60,9 @@ test("Create marker (using default values)", async () => {
|
|||
expect(onMarker2).toHaveBeenCalledWith(expectedMarker);
|
||||
|
||||
const expectedMarkerRecord = { [expectedMarker.id]: expectedMarker };
|
||||
expect(client1.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client2.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client3.markers).toEqual({});
|
||||
expect(cloneDeep(client1.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client2.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client3.markers)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -167,9 +168,9 @@ test("Edit marker", async () => {
|
|||
expect(onMarker2).toHaveBeenCalledWith(expectedMarker);
|
||||
|
||||
const expectedMarkerRecord = { [expectedMarker.id]: expectedMarker };
|
||||
expect(client1.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client2.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client3.markers).toEqual({});
|
||||
expect(cloneDeep(client1.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client2.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client3.markers)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -218,9 +219,76 @@ test("Delete marker", async () => {
|
|||
expect(onDeleteMarker3).toHaveBeenCalledWith({ id: deletedMarker.id });
|
||||
|
||||
const expectedMarkerRecord = { };
|
||||
expect(client1.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client2.markers).toEqual(expectedMarkerRecord);
|
||||
expect(client3.markers).toEqual({});
|
||||
expect(cloneDeep(client1.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client2.markers)).toEqual(expectedMarkerRecord);
|
||||
expect(cloneDeep(client3.markers)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
test("Get marker", async () => {
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
|
||||
const markerType = Object.values(client1.types).find((t) => t.type === "marker")!;
|
||||
|
||||
const marker = await client1.addMarker({
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id
|
||||
});
|
||||
|
||||
const expectedMarker = {
|
||||
id: marker.id,
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
padId: padData.id,
|
||||
name: "",
|
||||
colour: "ff0000",
|
||||
size: 30,
|
||||
symbol: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: expect.any(Number)
|
||||
} satisfies Marker;
|
||||
|
||||
expect(await client2.getMarker({ id: marker.id })).toEqual(expectedMarker);
|
||||
});
|
||||
});
|
||||
|
||||
test("Find marker", async () => {
|
||||
const client1 = await openClient();
|
||||
|
||||
await createTemporaryPad(client1, {}, async (createPadData, padData) => {
|
||||
const client2 = await openClient(padData.id);
|
||||
|
||||
const markerType = Object.values(client1.types).find((t) => t.type === "marker")!;
|
||||
|
||||
const marker = await client1.addMarker({
|
||||
name: "Marker test",
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
symbol: "a"
|
||||
});
|
||||
|
||||
const expectedResult: FindOnMapMarker = {
|
||||
id: marker.id,
|
||||
kind: "marker",
|
||||
similarity: 1,
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
typeId: markerType.id,
|
||||
name: "Marker test",
|
||||
symbol: "a"
|
||||
};
|
||||
|
||||
expect(await client2.findOnMap({ query: "Test" })).toEqual([{ ...expectedResult, similarity: 0.3333333333333333 }]);
|
||||
expect(await client2.findOnMap({ query: "T_st" })).toEqual([{ ...expectedResult, similarity: 0.16666666666666666 }]);
|
||||
expect(await client2.findOnMap({ query: "M%r" })).toEqual([{ ...expectedResult, similarity: 0 }]);
|
||||
expect(await client2.findOnMap({ query: "Bla" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -240,7 +308,7 @@ test("Try to create marker with line type", async () => {
|
|||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(client3.markers).toEqual({});
|
||||
expect(cloneDeep(client3.markers)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -266,7 +334,7 @@ test("Try to update marker with line type", async () => {
|
|||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(client3.markers).toEqual({
|
||||
expect(cloneDeep(client3.markers)).toEqual({
|
||||
[marker.id]: marker
|
||||
});
|
||||
});
|
||||
|
@ -290,7 +358,7 @@ test("Try to create marker with marker type from other pad", async () => {
|
|||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(client3.markers).toEqual({});
|
||||
expect(cloneDeep(client3.markers)).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -319,7 +387,7 @@ test("Try to update marker with marker type from other pad", async () => {
|
|||
|
||||
const client3 = await openClient(createPadData.adminId);
|
||||
await client3.updateBbox({ top: 20, bottom: 0, left: 0, right: 20, zoom: 1 });
|
||||
expect(client3.markers).toEqual({
|
||||
expect(cloneDeep(client3.markers)).toEqual({
|
||||
[marker.id]: marker
|
||||
});
|
||||
});
|
||||
|
@ -371,7 +439,7 @@ test("Socket v1 marker name", async () => {
|
|||
symbol: "",
|
||||
shape: "",
|
||||
data: {},
|
||||
ele: null
|
||||
ele: expect.any(Number)
|
||||
} satisfies Marker;
|
||||
|
||||
expect(marker).toEqual(expectedMarker);
|
||||
|
|
|
@ -93,7 +93,7 @@ export async function retry<R>(callback: () => R | Promise<R>): Promise<R> {
|
|||
try {
|
||||
return await callback();
|
||||
} catch (err: any) {
|
||||
if (i >= 10) {
|
||||
if (i >= 100) {
|
||||
throw err;
|
||||
} else {
|
||||
await sleep(10);
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"leaflet-auto-graticule": "^2.0.0",
|
||||
"leaflet-draggable-lines": "^2.0.0",
|
||||
"leaflet-freie-tonne": "^2.0.1",
|
||||
"leaflet-highlightable-layers": "^2.1.0",
|
||||
"leaflet-highlightable-layers": "^3.0.0",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class BboxHandler extends Handler {
|
|||
this._map.unproject(pixelBounds.getBottomLeft(), zoom),
|
||||
this._map.unproject(pixelBounds.getTopRight(), zoom)
|
||||
);
|
||||
this.client.updateBbox(leafletToFmBbox(bounds ?? this._map.getBounds(), zoom ?? this._map.getZoom()));
|
||||
void this.client.updateBbox(leafletToFmBbox(bounds ?? this._map.getBounds(), zoom ?? this._map.getZoom()));
|
||||
}
|
||||
|
||||
handleMoveEnd = (): void => {
|
||||
|
|
|
@ -107,7 +107,7 @@ export default class LinesLayer extends FeatureGroup {
|
|||
protected handleMoveEnd = (): void => {
|
||||
// Rerender all lines to recall disconnectSegmentsOutsideViewport()
|
||||
// Run it on next tick because the renderers need to run first
|
||||
Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
const lastMapBounds = this.lastMapBounds;
|
||||
const mapBounds = this.lastMapBounds = this._map.getBounds();
|
||||
for(const lineId of numberKeys(this.client.lines)) {
|
||||
|
@ -209,7 +209,7 @@ export default class LinesLayer extends FeatureGroup {
|
|||
const handleClick = (pos: Point) => {
|
||||
handler = undefined;
|
||||
if(routePoints.length > 0 && pos.lon == routePoints[routePoints.length-1].lon && pos.lat == routePoints[routePoints.length-1].lat)
|
||||
finishLine(true);
|
||||
void finishLine(true);
|
||||
else
|
||||
addPoint(pos);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class OverpassLayer extends FeatureGroup {
|
|||
}
|
||||
|
||||
onAdd(): this {
|
||||
this.redraw();
|
||||
void this.redraw();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ export default class OverpassLayer extends FeatureGroup {
|
|||
|
||||
setQuery(query?: string | ReadonlyArray<Readonly<OverpassPreset>>): void {
|
||||
this._query = query;
|
||||
this.redraw();
|
||||
void this.redraw();
|
||||
this.fire("setQuery", { query });
|
||||
|
||||
if (this.isEmpty())
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class RouteDragHandler extends DraggableLines {
|
|||
const routePoints = (e.layer as Polyline).getDraggableLinesRoutePoints();
|
||||
|
||||
if (routePoints) {
|
||||
this.client.setRoute({
|
||||
void this.client.setRoute({
|
||||
...route,
|
||||
routePoints: routePoints.map((p) => ({ lat: p.lat, lon: p.lng }))
|
||||
});
|
||||
|
|
|
@ -327,8 +327,7 @@ export class AsyncIcon extends Icon {
|
|||
this._asyncIconUrl.then((url) => {
|
||||
this.options.iconUrl = url;
|
||||
delete this._asyncIconUrl;
|
||||
});
|
||||
this._asyncIconUrl.catch((err) => {
|
||||
}).catch((err) => {
|
||||
console.error("Error loading async icon", err);
|
||||
});
|
||||
}
|
||||
|
@ -341,7 +340,7 @@ export class AsyncIcon extends Icon {
|
|||
icon._fmIconAbortController?.abort();
|
||||
const abortController = new AbortController();
|
||||
icon._fmIconAbortController = abortController;
|
||||
this._asyncIconUrl.then((url) => {
|
||||
void this._asyncIconUrl.then((url) => {
|
||||
if (!icon._fmIconAbortController!.signal.aborted) {
|
||||
icon.setAttribute("src", url);
|
||||
}
|
||||
|
|
|
@ -20,13 +20,14 @@ export interface Config {
|
|||
db: DbConfig;
|
||||
orsToken?: string;
|
||||
mapboxToken?: string;
|
||||
mapzenToken?: string;
|
||||
maxmindUserId?: string;
|
||||
maxmindLicenseKey?: string;
|
||||
limaLabsToken?: string;
|
||||
/** Hide the "Open this on Google/Bing Maps" links in the map style menu */
|
||||
hideCommercialMapLinks?: boolean;
|
||||
customCssFile?: string;
|
||||
nominatimUrl: string;
|
||||
openElevationApiUrl: string;
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
|
@ -51,7 +52,6 @@ const config: Config = {
|
|||
},
|
||||
orsToken: process.env.ORS_TOKEN || "", // Get a token on https://go.openrouteservice.org/
|
||||
mapboxToken: process.env.MAPBOX_TOKEN || "", // Get an API key on https://www.mapbox.com/signup/
|
||||
mapzenToken: process.env.MAPZEN_TOKEN || "", // Get an API key on https://mapzen.com/developers/sign_up
|
||||
|
||||
// Maxmind configuration. If specified, the maxmind GeoLite2 database will be downloaded for Geo IP lookup (to show the initial map state) and kept it in memory
|
||||
// Sign up here: https://www.maxmind.com/en/geolite2/signup
|
||||
|
@ -63,6 +63,9 @@ const config: Config = {
|
|||
hideCommercialMapLinks: process.env.HIDE_COMMERCIAL_MAP_LINKS === "1",
|
||||
|
||||
customCssFile: process.env.CUSTOM_CSS_FILE || undefined,
|
||||
|
||||
nominatimUrl: process.env.NOMINATIM_URL || "https://nominatim.openstreetmap.org",
|
||||
openElevationApiUrl: process.env.OPEN_ELEVATION_URL || "https://api.open-elevation.com",
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -2,10 +2,8 @@ import { type AssociationOptions, Model, type ModelAttributeColumnOptions, type
|
|||
import type { Line, Marker, PadId, ID, Type, Bbox } from "facilmap-types";
|
||||
import Database from "./database.js";
|
||||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import { calculateRouteForLine } from "../routing/routing.js";
|
||||
import type { PadModel } from "./pad";
|
||||
import { arrayToAsyncIterator } from "../utils/streams";
|
||||
import { applyLineStyles, applyMarkerStyles } from "facilmap-utils";
|
||||
|
||||
const ITEMS_PER_BATCH = 5000;
|
||||
|
||||
|
@ -130,30 +128,12 @@ export default class DatabaseHelpers {
|
|||
}
|
||||
|
||||
const type = types[object.typeId];
|
||||
const update = type.type === "marker" ? applyMarkerStyles(object as Marker, type) : applyLineStyles(object as Line, type);
|
||||
|
||||
const actions: Array<Promise<any>> = [ ];
|
||||
|
||||
if(Object.keys(update).length > 0) {
|
||||
Object.assign(object, update);
|
||||
|
||||
if(object.id) { // Objects from getLineTemplate() do not have an ID
|
||||
if (type.type === "line") {
|
||||
actions.push(this._db.lines.updateLine(padId, object.id, update, true));
|
||||
} else {
|
||||
actions.push(this._db.markers.updateMarker(padId, object.id, update, true));
|
||||
}
|
||||
}
|
||||
|
||||
if(object.id && type.type === "line" && "mode" in update) {
|
||||
actions.push(calculateRouteForLine(object as Line).then(async ({ trackPoints, ...routeInfo }) => {
|
||||
Object.assign(object, routeInfo);
|
||||
await this._db.lines._setLinePoints(padId, object.id, trackPoints);
|
||||
}));
|
||||
}
|
||||
if (type.type === "line") {
|
||||
await this._db.lines._updateLine(object as Line, {}, type, true);
|
||||
} else {
|
||||
await this._db.markers._updateMarker(object as Marker, {}, type, true);
|
||||
}
|
||||
|
||||
await Promise.all(actions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -401,11 +381,11 @@ export default class DatabaseHelpers {
|
|||
for await (const item of data) {
|
||||
slice.push(item);
|
||||
if (slice.length >= ITEMS_PER_BATCH) {
|
||||
createSlice();
|
||||
await createSlice();
|
||||
}
|
||||
}
|
||||
if (slice.length > 0) {
|
||||
createSlice();
|
||||
await createSlice();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type CreationAttributes, type CreationOptional, DataTypes, type ForeignKey, type HasManyGetAssociationsMixin, type InferAttributes, type InferCreationAttributes, Model, Op } from "sequelize";
|
||||
import type { BboxWithZoom, ID, Latitude, Line, ExtraInfo, Longitude, PadId, Point, Route, TrackPoint, CRU, RouteInfo, Stroke, Colour, RouteMode, Width } from "facilmap-types";
|
||||
import type { BboxWithZoom, ID, Latitude, Line, ExtraInfo, Longitude, PadId, Point, Route, TrackPoint, CRU, RouteInfo, Stroke, Colour, RouteMode, Width, Type } from "facilmap-types";
|
||||
import Database from "./database.js";
|
||||
import { type BboxWithExcept, createModel, dataDefinition, type DataModel, getDefaultIdType, getLatType, getLonType, getPosType, getVirtualLatType, getVirtualLonType, makeNotNullForeignKey } from "./helpers.js";
|
||||
import { chunk, groupBy, isEqual, mapValues, omit } from "lodash-es";
|
||||
|
@ -7,6 +7,7 @@ import { calculateRouteForLine } from "../routing/routing.js";
|
|||
import type { PadModel } from "./pad";
|
||||
import type { Point as GeoJsonPoint } from "geojson";
|
||||
import type { TypeModel } from "./type";
|
||||
import { resolveCreateLine, resolveUpdateLine } from "facilmap-utils";
|
||||
|
||||
export type LineWithTrackPoints = Line & {
|
||||
trackPoints: TrackPoint[];
|
||||
|
@ -97,7 +98,8 @@ export default class DatabaseLines {
|
|||
set: function(this: LineModel, v: number | null) {
|
||||
// Round number to avoid integer column error in Postgres
|
||||
this.setDataValue("time", v != null ? Math.round(v) : v);
|
||||
}
|
||||
},
|
||||
defaultValue: null
|
||||
},
|
||||
ascent : {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
|
@ -105,7 +107,8 @@ export default class DatabaseLines {
|
|||
set: function(this: LineModel, v: number | null) {
|
||||
// Round number to avoid integer column error in Postgres
|
||||
this.setDataValue("ascent", v != null ? Math.round(v) : v);
|
||||
}
|
||||
},
|
||||
defaultValue: null
|
||||
},
|
||||
descent : {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
|
@ -113,7 +116,8 @@ export default class DatabaseLines {
|
|||
set: function(this: LineModel, v: number | null) {
|
||||
// Round number to avoid integer column error in Postgres
|
||||
this.setDataValue("descent", v != null ? Math.round(v) : v);
|
||||
}
|
||||
},
|
||||
defaultValue: null
|
||||
},
|
||||
top: getLatType(),
|
||||
bottom: getLatType(),
|
||||
|
@ -127,8 +131,9 @@ export default class DatabaseLines {
|
|||
return extraInfo != null ? JSON.parse(extraInfo) : extraInfo;
|
||||
},
|
||||
set: function(this: LineModel, v: ExtraInfo) {
|
||||
this.setDataValue("extraInfo", JSON.stringify(v) as any);
|
||||
}
|
||||
this.setDataValue("extraInfo", v != null ? JSON.stringify(v) as any : v);
|
||||
},
|
||||
defaultValue: null
|
||||
}
|
||||
}, {
|
||||
sequelize: this._db._conn,
|
||||
|
@ -220,18 +225,11 @@ export default class DatabaseLines {
|
|||
throw new Error(`Cannot use ${type.type} type for line.`);
|
||||
}
|
||||
|
||||
const resolvedData = {
|
||||
...data,
|
||||
colour: data.colour ?? type.defaultColour,
|
||||
width: data.width ?? type.defaultWidth,
|
||||
stroke: data.stroke ?? type.defaultStroke,
|
||||
mode: data.mode ?? type.defaultMode
|
||||
};
|
||||
const resolvedData = resolveCreateLine(data, type);
|
||||
|
||||
const { trackPoints, ...routeInfo } = await calculateRouteForLine(resolvedData, trackPointsFromRoute);
|
||||
|
||||
const createdLine = await this._db.helpers._createPadObject<Line>("Line", padId, omit({ ...resolvedData, ...routeInfo }, "trackPoints" /* Part of data if mode is track */));
|
||||
await this._db.helpers._updateObjectStyles(createdLine);
|
||||
|
||||
// We have to emit this before calling _setLinePoints so that this event is sent to the client first
|
||||
this._db.emit("line", padId, createdLine);
|
||||
|
@ -241,48 +239,41 @@ export default class DatabaseLines {
|
|||
return createdLine;
|
||||
}
|
||||
|
||||
async updateLine(padId: PadId, lineId: ID, data: Omit<Line<CRU.UPDATE_VALIDATED>, "id">, doNotUpdateStyles?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
|
||||
async updateLine(padId: PadId, lineId: ID, data: Omit<Line<CRU.UPDATE_VALIDATED>, "id">, noHistory?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
|
||||
const originalLine = await this.getLine(padId, lineId);
|
||||
const newType = await this._db.types.getType(padId, data.typeId ?? originalLine.typeId);
|
||||
return await this._updateLine(originalLine, data, newType, noHistory, trackPointsFromRoute);
|
||||
}
|
||||
|
||||
async _updateLine(originalLine: Line, data: Omit<Line<CRU.UPDATE_VALIDATED>, "id">, newType: Type, noHistory?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
|
||||
if (newType.type !== "line") {
|
||||
throw new Error(`Cannot use ${newType.type} type for line.`);
|
||||
}
|
||||
|
||||
const update = {
|
||||
...data,
|
||||
...resolveUpdateLine(originalLine, data, newType),
|
||||
routePoints: data.routePoints || originalLine.routePoints,
|
||||
mode: (data.mode ?? originalLine.mode) || ""
|
||||
};
|
||||
|
||||
let routeInfo: RouteInfo | undefined;
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
if((update.mode == "track" && update.trackPoints) || !isEqual(update.routePoints, originalLine.routePoints) || update.mode != originalLine.mode)
|
||||
routeInfo = await calculateRouteForLine(update, trackPointsFromRoute);
|
||||
if((update.mode == "track" && update.trackPoints) || !isEqual(update.routePoints, originalLine.routePoints) || update.mode != originalLine.mode)
|
||||
routeInfo = await calculateRouteForLine(update, trackPointsFromRoute);
|
||||
|
||||
Object.assign(update, mapValues(routeInfo, (val) => val == null ? null : val)); // Use null instead of undefined
|
||||
delete update.trackPoints; // They came if mode is track
|
||||
})(),
|
||||
(async () => {
|
||||
if (update.typeId != null) {
|
||||
const type = await this._db.types.getType(padId, update.typeId);
|
||||
if (type.type !== "line") {
|
||||
throw new Error(`Cannot use ${type.type} type for line.`);
|
||||
}
|
||||
}
|
||||
})()
|
||||
]);
|
||||
Object.assign(update, mapValues(routeInfo, (val) => val == null ? null : val)); // Use null instead of undefined
|
||||
delete update.trackPoints; // They came if mode is track
|
||||
|
||||
const newLine = await this._db.helpers._updatePadObject<Line>("Line", padId, lineId, update, doNotUpdateStyles);
|
||||
const newLine = await this._db.helpers._updatePadObject<Line>("Line", originalLine.padId, originalLine.id, update, noHistory);
|
||||
|
||||
if(!doNotUpdateStyles)
|
||||
await this._db.helpers._updateObjectStyles(newLine); // Modifies newLine
|
||||
|
||||
this._db.emit("line", padId, newLine);
|
||||
this._db.emit("line", originalLine.padId, newLine);
|
||||
|
||||
if(routeInfo)
|
||||
await this._setLinePoints(padId, lineId, routeInfo.trackPoints);
|
||||
await this._setLinePoints(originalLine.padId, originalLine.id, routeInfo.trackPoints);
|
||||
|
||||
return newLine;
|
||||
}
|
||||
|
||||
async _setLinePoints(padId: PadId, lineId: ID, trackPoints: Point[], _noEvent?: boolean): Promise<void> {
|
||||
// First get elevation, so that if that fails, we don't update anything
|
||||
await this.LinePointModel.destroy({ where: { lineId: lineId } });
|
||||
|
||||
const create = [ ];
|
||||
|
@ -293,7 +284,7 @@ export default class DatabaseLines {
|
|||
const points = await this._db.helpers._bulkCreateInBatches<TrackPoint>(this.LinePointModel, create);
|
||||
|
||||
if(!_noEvent)
|
||||
this._db.emit("linePoints", padId, lineId, points.map((point) => omit(point, ["lineId", "pos"]) as TrackPoint));
|
||||
this._db.emit("linePoints", padId, lineId, points.map((point) => omit(point, ["id", "lineId", "pos"]) as TrackPoint));
|
||||
}
|
||||
|
||||
async deleteLine(padId: PadId, lineId: ID): Promise<Line> {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { type CreationOptional, DataTypes, type ForeignKey, type InferAttributes, type InferCreationAttributes, Model } from "sequelize";
|
||||
import type { BboxWithZoom, CRU, Colour, ID, Latitude, Longitude, Marker, PadId, Shape, Size, Symbol } from "facilmap-types";
|
||||
import type { BboxWithZoom, CRU, Colour, ID, Latitude, Longitude, Marker, PadId, Shape, Size, Symbol, Type } from "facilmap-types";
|
||||
import { type BboxWithExcept, createModel, dataDefinition, type DataModel, getDefaultIdType, getPosType, getVirtualLatType, getVirtualLonType, makeNotNullForeignKey } from "./helpers.js";
|
||||
import Database from "./database.js";
|
||||
import { getElevationForPoint } from "../elevation.js";
|
||||
import type { PadModel } from "./pad.js";
|
||||
import type { Point as GeoJsonPoint } from "geojson";
|
||||
import type { TypeModel } from "./type.js";
|
||||
import { resolveCreateMarker, resolveUpdateMarker } from "facilmap-utils";
|
||||
|
||||
export interface MarkerModel extends Model<InferAttributes<MarkerModel>, InferCreationAttributes<MarkerModel>> {
|
||||
id: CreationOptional<ID>;
|
||||
|
@ -94,45 +95,35 @@ export default class DatabaseMarkers {
|
|||
throw new Error(`Cannot use ${type.type} type for marker.`);
|
||||
}
|
||||
|
||||
const resolvedData = resolveCreateMarker(data, type);
|
||||
|
||||
const result = await this._db.helpers._createPadObject<Marker>("Marker", padId, {
|
||||
...data,
|
||||
colour: data.colour ?? type.defaultColour,
|
||||
size: data.size ?? type.defaultSize,
|
||||
symbol: data.symbol ?? type.defaultSymbol,
|
||||
shape: data.shape ?? type.defaultShape,
|
||||
...resolvedData,
|
||||
ele: data.ele === undefined ? await getElevationForPoint(data) : data.ele
|
||||
});
|
||||
|
||||
await this._db.helpers._updateObjectStyles(result);
|
||||
|
||||
this._db.emit("marker", padId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateMarker(padId: PadId, markerId: ID, data: Omit<Marker<CRU.UPDATE_VALIDATED>, "id">, doNotUpdateStyles = false): Promise<Marker> {
|
||||
const update = { ...data };
|
||||
async updateMarker(padId: PadId, markerId: ID, data: Omit<Marker<CRU.UPDATE_VALIDATED>, "id">, noHistory = false): Promise<Marker> {
|
||||
const originalMarker = await this.getMarker(padId, markerId);
|
||||
const newType = await this._db.types.getType(padId, data.typeId ?? originalMarker.typeId);
|
||||
return await this._updateMarker(originalMarker, data, newType, noHistory);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
if (update.lat != null && update.lon != null && update.ele === undefined)
|
||||
update.ele = await getElevationForPoint({ lat: update.lat, lon: update.lon });
|
||||
})(),
|
||||
(async () => {
|
||||
if (update.typeId != null) {
|
||||
const type = await this._db.types.getType(padId, update.typeId);
|
||||
if (type.type !== "marker") {
|
||||
throw new Error(`Cannot use ${type.type} type for marker.`);
|
||||
}
|
||||
}
|
||||
})()
|
||||
]);
|
||||
async _updateMarker(originalMarker: Marker, data: Omit<Marker<CRU.UPDATE_VALIDATED>, "id">, newType: Type, noHistory = false): Promise<Marker> {
|
||||
if (newType.type !== "marker") {
|
||||
throw new Error(`Cannot use ${newType.type} type for marker.`);
|
||||
}
|
||||
|
||||
const result = await this._db.helpers._updatePadObject<Marker>("Marker", padId, markerId, update, doNotUpdateStyles);
|
||||
const update = resolveUpdateMarker(originalMarker, data, newType);
|
||||
|
||||
if(!doNotUpdateStyles)
|
||||
await this._db.helpers._updateObjectStyles(result);
|
||||
if (update.lat != null && update.lon != null && update.ele === undefined)
|
||||
update.ele = await getElevationForPoint({ lat: update.lat, lon: update.lon });
|
||||
|
||||
this._db.emit("marker", padId, result);
|
||||
const result = await this._db.helpers._updatePadObject<Marker>("Marker", originalMarker.padId, originalMarker.id, update, noHistory);
|
||||
|
||||
this._db.emit("marker", originalMarker.padId, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ interface MetaModel extends Model<InferAttributes<MetaModel>, InferCreationAttri
|
|||
|
||||
export interface MetaProperties {
|
||||
dropdownKeysMigrated: "1";
|
||||
hasElevation: "1";
|
||||
hasElevation: "1" | "2";
|
||||
hasLegendOption: "1";
|
||||
hasBboxes: "1";
|
||||
untitledMigrationCompleted: "1";
|
||||
fieldsNullMigrationCompleted: "1";
|
||||
extraInfoNullMigrationCompleted: "1";
|
||||
}
|
||||
|
||||
export default class DatabaseMeta {
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class DatabaseMigrations {
|
|||
await this._spatialMigration();
|
||||
await this._untitledMigration();
|
||||
await this._fieldsNullMigration();
|
||||
await this._extraInfoNullMigration();
|
||||
}
|
||||
|
||||
|
||||
|
@ -489,4 +490,23 @@ export default class DatabaseMigrations {
|
|||
await this._db.meta.setMeta("fieldsNullMigrationCompleted", "1");
|
||||
}
|
||||
|
||||
|
||||
/** Convert "null" to null for extraInfo */
|
||||
async _extraInfoNullMigration(): Promise<void> {
|
||||
if(await this._db.meta.getMeta("extraInfoNullMigrationCompleted") == "1")
|
||||
return;
|
||||
|
||||
await this._db.lines.LineModel.update({
|
||||
extraInfo: null
|
||||
}, {
|
||||
where: {
|
||||
extraInfo: {
|
||||
[Op.in]: ["null", "{}"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await this._db.meta.setMeta("extraInfoNullMigrationCompleted", "1");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class DatabaseRoutes {
|
|||
|
||||
async afterConnect(): Promise<void> {
|
||||
// Delete all route points, clients will have to reconnect and recalculate their routes anyways
|
||||
this.RoutePointModel.truncate();
|
||||
await this.RoutePointModel.truncate();
|
||||
}
|
||||
|
||||
async getRoutePoints(routeId: string, bboxWithZoom?: BboxWithZoom & BboxWithExcept, getCompleteBasicRoute = false): Promise<TrackPoint[]> {
|
||||
|
|
|
@ -1,48 +1,44 @@
|
|||
import type { Point } from "facilmap-types";
|
||||
import config from "./config";
|
||||
|
||||
// const API_URL = "https://elevation.mapzen.com/height";
|
||||
// const LIMIT = 500;
|
||||
// const MIN_TIME_BETWEEN_REQUESTS = 600;
|
||||
|
||||
// const throttle = highland<() => void>();
|
||||
// throttle.ratelimit(1, MIN_TIME_BETWEEN_REQUESTS).each((func) => {
|
||||
// func();
|
||||
// });
|
||||
|
||||
export async function _getThrottledSlot(): Promise<void> {
|
||||
// return new Promise<void>((resolve) => {
|
||||
// throttle.write(resolve);
|
||||
// });
|
||||
}
|
||||
|
||||
export async function getElevationForPoint(point: Point): Promise<number | undefined> {
|
||||
const points = await getElevationForPoints([point]);
|
||||
export async function getElevationForPoint(point: Point, failOnError = false): Promise<number | undefined> {
|
||||
const points = await getElevationForPoints([point], failOnError);
|
||||
return points[0];
|
||||
}
|
||||
|
||||
export async function getElevationForPoints(points: Array<{ lat: string | number; lon: string | number }>): Promise<Array<number | undefined>> {
|
||||
return points.map(() => undefined);
|
||||
|
||||
/*if(points.length == 0)
|
||||
return Promise.resolve([ ]);
|
||||
|
||||
let ret = Promise.resolve([ ]);
|
||||
for(let i=0; i<points.length; i+=LIMIT) {
|
||||
ret = ret.then((heights) => {
|
||||
return elevation._getThrottledSlot().then(() => (heights));
|
||||
}).then((heights) => {
|
||||
let json = {
|
||||
encoded_polyline: polyline.encode(points.slice(i, i+LIMIT).map((point) => ([point.lat, point.lon])), 6),
|
||||
range: false
|
||||
};
|
||||
|
||||
return request.get({
|
||||
url: `${API_URL}?json=${encodeURI(JSON.stringify(json))}&api_key=${config.mapzenToken}`,
|
||||
json: true
|
||||
}).then((res) => (heights.concat(res.height)));
|
||||
});
|
||||
export async function getElevationForPoints(points: Point[], failOnError = false): Promise<Array<number | undefined>> {
|
||||
if(points.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${config.openElevationApiUrl}/api/v1/lookup`, {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
locations: points.map((point) => ({ latitude: point.lat, longitude: point.lon }))
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Looking up elevations failed with status ${res.status}.`);
|
||||
}
|
||||
const json: { results: Array<{ latitude: number; longitude: number; elevation: number }> } = await res.json();
|
||||
|
||||
return json.results.map((result: any) => {
|
||||
if (result.elevation !== 0) {
|
||||
return result.elevation;
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (failOnError) {
|
||||
throw err;
|
||||
} else {
|
||||
console.warn("Error lookup up elevation", err);
|
||||
return points.map(() => undefined);
|
||||
}
|
||||
}
|
||||
return ret;*/
|
||||
}
|
||||
|
||||
interface AscentDescent {
|
||||
|
|
|
@ -18,7 +18,7 @@ export function exportCsv(
|
|||
|
||||
const stringifier = stringify();
|
||||
stringifier.write(tabular.fields);
|
||||
tabular.objects.pipeTo(Writable.toWeb(stringifier));
|
||||
void tabular.objects.pipeTo(Writable.toWeb(stringifier));
|
||||
|
||||
return Readable.toWeb(stringifier);
|
||||
})());
|
||||
|
|
|
@ -153,7 +153,7 @@ export function exportGpx(database: Database, padId: PadId, useTracks: boolean,
|
|||
export function exportGpxZip(database: Database, padId: PadId, useTracks: boolean, filter?: string): ReadableStream<Uint8Array> {
|
||||
const encodeZipStream = getZipEncodeStream();
|
||||
|
||||
asyncIteratorToStream((async function*(): AsyncIterable<ZipEncodeStreamItem> {
|
||||
void asyncIteratorToStream((async function*(): AsyncIterable<ZipEncodeStreamItem> {
|
||||
const filterFunc = compileExpression(filter);
|
||||
|
||||
const [padData, types] = await Promise.all([
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { calculateBbox, isInBbox } from "../utils/geo.js";
|
||||
import type { Bbox, BboxWithZoom, CRU, Line, Point, Route, RouteInfo, RouteMode, TrackPoint } from "facilmap-types";
|
||||
import { decodeRouteMode, type DecodedRouteMode, calculateDistance } from "facilmap-utils";
|
||||
import { decodeRouteMode, type DecodedRouteMode, calculateDistance, round } from "facilmap-utils";
|
||||
import { calculateOSRMRoute } from "./osrm.js";
|
||||
import { calculateORSRoute, getMaximumDistanceBetweenRoutePoints } from "./ors.js";
|
||||
|
||||
|
@ -35,6 +35,11 @@ export async function calculateRoute(routePoints: Point[], encodedMode: RouteMod
|
|||
route = await calculateORSRoute(routePoints, decodedMode);
|
||||
}
|
||||
|
||||
route!.distance = round(route!.distance, 2);
|
||||
route!.time = route!.time != null ? Math.round(route!.time) : route!.time;
|
||||
route!.ascent = route!.ascent != null ? Math.round(route!.ascent) : route!.ascent;
|
||||
route!.descent = route!.descent != null ? Math.round(route!.descent) : route!.descent;
|
||||
|
||||
calculateZoomLevels(route!.trackPoints);
|
||||
|
||||
return {
|
||||
|
@ -54,9 +59,9 @@ export async function calculateRouteForLine(line: Pick<Line<CRU.CREATE_VALIDATED
|
|||
result.extraInfo = trackPointsFromRoute.extraInfo;
|
||||
result.trackPoints = trackPointsFromRoute.trackPoints;
|
||||
} else if(line.mode == "track" && line.trackPoints && line.trackPoints.length >= 2) {
|
||||
result.distance = calculateDistance(line.trackPoints);
|
||||
result.distance = round(calculateDistance(line.trackPoints), 2);
|
||||
result.time = undefined;
|
||||
result.extraInfo = {};
|
||||
result.extraInfo = undefined;
|
||||
|
||||
// TODO: ascent/descent?
|
||||
|
||||
|
@ -78,9 +83,9 @@ export async function calculateRouteForLine(line: Pick<Line<CRU.CREATE_VALIDATED
|
|||
|
||||
result.trackPoints = routeData.trackPoints;
|
||||
} else {
|
||||
result.distance = calculateDistance(line.routePoints);
|
||||
result.distance = round(calculateDistance(line.routePoints), 2);
|
||||
result.time = undefined;
|
||||
result.extraInfo = {};
|
||||
result.extraInfo = undefined;
|
||||
|
||||
result.trackPoints = [ ];
|
||||
for(let i=0; i<line.routePoints.length; i++) {
|
||||
|
|
|
@ -37,7 +37,6 @@ interface NominatimError {
|
|||
error: { code?: number; message: string } | string;
|
||||
}
|
||||
|
||||
const nameFinderUrl = "https://nominatim.openstreetmap.org";
|
||||
const limit = 25;
|
||||
const stateAbbr: Record<string, Record<string, string>> = {
|
||||
"us" : {
|
||||
|
@ -115,7 +114,7 @@ export async function find(query: string, loadUrls = false, loadElevation = fals
|
|||
|
||||
async function _findQuery(query: string, loadElevation = false): Promise<Array<SearchResult>> {
|
||||
const body: Array<NominatimResult> | NominatimError = await throttledFetch(
|
||||
nameFinderUrl + "/search?format=jsonv2&polygon_geojson=1&addressdetails=1&namedetails=1&limit=" + encodeURIComponent(limit) + "&extratags=1&q=" + encodeURIComponent(query),
|
||||
config.nominatimUrl + "/search?format=jsonv2&polygon_geojson=1&addressdetails=1&namedetails=1&limit=" + encodeURIComponent(limit) + "&extratags=1&q=" + encodeURIComponent(query),
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": config.userAgent
|
||||
|
@ -131,7 +130,7 @@ async function _findQuery(query: string, loadElevation = false): Promise<Array<S
|
|||
|
||||
const points = body.filter((res) => (res.lon && res.lat));
|
||||
if(loadElevation && points.length > 0) {
|
||||
const elevations = await getElevationForPoints(points);
|
||||
const elevations = await getElevationForPoints(points.map((point) => ({ lat: Number(point.lat), lon: Number(point.lon) })));
|
||||
elevations.forEach((elevation, i) => {
|
||||
points[i].elevation = elevation;
|
||||
});
|
||||
|
@ -142,7 +141,7 @@ async function _findQuery(query: string, loadElevation = false): Promise<Array<S
|
|||
|
||||
async function _findOsmObject(type: string, id: string, loadElevation = false): Promise<Array<SearchResult>> {
|
||||
const body: Array<NominatimResult> | NominatimError = await throttledFetch(
|
||||
`${nameFinderUrl}/lookup?format=jsonv2&addressdetails=1&polygon_geojson=1&extratags=1&namedetails=1&osm_ids=${encodeURI(type.toUpperCase())}${encodeURI(id)}`,
|
||||
`${config.nominatimUrl}/lookup?format=jsonv2&addressdetails=1&polygon_geojson=1&extratags=1&namedetails=1&osm_ids=${encodeURI(type.toUpperCase())}${encodeURI(id)}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": config.userAgent
|
||||
|
@ -158,7 +157,7 @@ async function _findOsmObject(type: string, id: string, loadElevation = false):
|
|||
|
||||
const points = body.filter((res) => (res.lon && res.lat));
|
||||
if(loadElevation && points.length > 0) {
|
||||
const elevations = await getElevationForPoints(points);
|
||||
const elevations = await getElevationForPoints(points.map((point) => ({ lat: Number(point.lat), lon: Number(point.lon) })));
|
||||
elevations.forEach((elevation, i) => {
|
||||
points[i].elevation = elevation;
|
||||
});
|
||||
|
@ -170,7 +169,7 @@ async function _findOsmObject(type: string, id: string, loadElevation = false):
|
|||
async function _findLonLat(lonlatWithZoom: PointWithZoom, loadElevation = false): Promise<Array<SearchResult>> {
|
||||
const [body, elevation] = await Promise.all([
|
||||
throttledFetch(
|
||||
`${nameFinderUrl}/reverse?format=jsonv2&addressdetails=1&polygon_geojson=0&extratags=1&namedetails=1&lat=${encodeURIComponent(lonlatWithZoom.lat)}&lon=${encodeURIComponent(lonlatWithZoom.lon)}&zoom=${encodeURIComponent(lonlatWithZoom.zoom != null ? (lonlatWithZoom.zoom >= 12 ? lonlatWithZoom.zoom+2 : lonlatWithZoom.zoom) : 17)}`,
|
||||
`${config.nominatimUrl}/reverse?format=jsonv2&addressdetails=1&polygon_geojson=0&extratags=1&namedetails=1&lat=${encodeURIComponent(lonlatWithZoom.lat)}&lon=${encodeURIComponent(lonlatWithZoom.lon)}&zoom=${encodeURIComponent(lonlatWithZoom.zoom != null ? (lonlatWithZoom.zoom >= 12 ? lonlatWithZoom.zoom+2 : lonlatWithZoom.zoom) : 17)}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": config.userAgent
|
||||
|
|
|
@ -80,7 +80,7 @@ export function flatMapStream<T, O>(stream: ReadableStream<T>, mapper: (it: T) =
|
|||
}
|
||||
}
|
||||
});
|
||||
stream.pipeTo(transform.writable);
|
||||
void stream.pipeTo(transform.writable);
|
||||
return transform.readable;
|
||||
}
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
|
||||
res.set("Content-type", "application/gpx+xml");
|
||||
res.attachment(`${getSafeFilename(normalizePadName(padData.name))}.gpx`);
|
||||
exportGpx(database, padData ? padData.id : req.params.padId, query.useTracks == "1", query.filter).pipeTo(Writable.toWeb(res));
|
||||
void exportGpx(database, padData ? padData.id : req.params.padId, query.useTracks == "1", query.filter).pipeTo(Writable.toWeb(res));
|
||||
});
|
||||
|
||||
app.get("/:padId/gpx/zip", async (req: Request<{ padId: string }>, res: Response<string>) => {
|
||||
|
@ -121,7 +121,7 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
|
||||
res.set("Content-type", "application/zip");
|
||||
res.attachment(padData.name.replace(/[\\/:*?"<>|]+/g, '_') + ".zip");
|
||||
exportGpxZip(database, padData ? padData.id : req.params.padId, query.useTracks == "1", query.filter).pipeTo(Writable.toWeb(res));
|
||||
void exportGpxZip(database, padData ? padData.id : req.params.padId, query.useTracks == "1", query.filter).pipeTo(Writable.toWeb(res));
|
||||
});
|
||||
|
||||
app.get("/:padId/table", async (req: Request<{ padId: string }>, res: Response<string>) => {
|
||||
|
@ -132,7 +132,7 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
|
||||
res.type("html");
|
||||
res.setHeader("Referrer-Policy", "origin");
|
||||
createTable(
|
||||
void createTable(
|
||||
database,
|
||||
req.params.padId,
|
||||
query.filter,
|
||||
|
@ -149,7 +149,7 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
|
||||
res.type("html");
|
||||
res.setHeader("Referrer-Policy", "origin");
|
||||
createSingleTable(
|
||||
void createSingleTable(
|
||||
database,
|
||||
req.params.padId,
|
||||
typeId,
|
||||
|
|
|
@ -13,6 +13,6 @@ export const markerValidator = cruValidator({
|
|||
colour: optionalCreate(colourValidator), // defaults to type.defaultColour
|
||||
size: optionalCreate(sizeValidator), // defaults to type.defaultSize
|
||||
data: optionalCreate(z.record(z.string())),
|
||||
ele: optionalCreate(z.number().or(z.null()), null)
|
||||
ele: optionalCreate(z.number().or(z.null()))
|
||||
});
|
||||
export type Marker<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof markerValidator>;
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"typescript": "^5.4.2",
|
||||
"vite": "^5.1.5",
|
||||
"vite-plugin-dts": "^3.7.3",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CRU, Field, FieldOption, Line, Marker, Type } from "facilmap-types";
|
||||
import { lineValidator, markerValidator, type CRU, type Field, type FieldOption, type Line, type Marker, type Type } from "facilmap-types";
|
||||
|
||||
export function isMarker<Mode extends CRU.READ | CRU.CREATE>(object: Marker<Mode> | Line<Mode>): object is Marker<Mode> {
|
||||
return "lat" in object && object.lat != null;
|
||||
|
@ -45,7 +45,7 @@ export function normalizeFieldValue(field: Field, value: string | undefined, ign
|
|||
}
|
||||
}
|
||||
|
||||
export function applyMarkerStyles(marker: Marker, type: Type): Omit<Marker<CRU.UPDATE_VALIDATED>, "id"> {
|
||||
export function applyMarkerStyles(marker: Marker<CRU.READ | CRU.CREATE_VALIDATED>, type: Type): Omit<Marker<CRU.UPDATE_VALIDATED>, "id"> {
|
||||
const update: Omit<Marker<CRU.UPDATE_VALIDATED>, "id"> = {};
|
||||
|
||||
if(type.colourFixed && marker.colour != type.defaultColour)
|
||||
|
@ -59,7 +59,7 @@ export function applyMarkerStyles(marker: Marker, type: Type): Omit<Marker<CRU.U
|
|||
|
||||
for(const field of type.fields) {
|
||||
if(field.controlColour || field.controlSize || field.controlSymbol || field.controlShape) {
|
||||
const option = getSelectedOption(field, marker.data[field.name]);
|
||||
const option = getSelectedOption(field, marker.data?.[field.name]);
|
||||
|
||||
if(option) {
|
||||
if(field.controlColour && marker.colour != (option.colour ?? type.defaultColour))
|
||||
|
@ -77,7 +77,28 @@ export function applyMarkerStyles(marker: Marker, type: Type): Omit<Marker<CRU.U
|
|||
return update;
|
||||
}
|
||||
|
||||
export function applyLineStyles(line: Line, type: Type): Omit<Marker<CRU.UPDATE_VALIDATED>, "id"> {
|
||||
export function resolveCreateMarker(marker: Marker<CRU.CREATE>, type: Type): Marker<CRU.CREATE_VALIDATED> {
|
||||
const parsed = markerValidator.create.parse(marker);
|
||||
const result: Marker<CRU.CREATE_VALIDATED> = {
|
||||
...parsed,
|
||||
colour: parsed.colour ?? type.defaultColour,
|
||||
size: parsed.size ?? type.defaultSize,
|
||||
symbol: parsed.symbol ?? type.defaultSymbol,
|
||||
shape: parsed.shape ?? type.defaultShape
|
||||
};
|
||||
Object.assign(result, applyMarkerStyles(result, type));
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveUpdateMarker(marker: Marker, update: Omit<Marker<CRU.UPDATE>, "id">, newType: Type): Marker<CRU.UPDATE_VALIDATED> {
|
||||
const resolvedUpdate = markerValidator.update.parse(update);
|
||||
return {
|
||||
...resolvedUpdate,
|
||||
...applyMarkerStyles({ ...marker, ...resolvedUpdate }, newType)
|
||||
};
|
||||
}
|
||||
|
||||
export function applyLineStyles(line: Line<CRU.READ | CRU.CREATE_VALIDATED>, type: Type): Omit<Line<CRU.UPDATE_VALIDATED>, "id"> {
|
||||
const update: Omit<Line<CRU.UPDATE_VALIDATED>, "id"> = {};
|
||||
|
||||
if(type.colourFixed && line.colour != type.defaultColour) {
|
||||
|
@ -95,7 +116,7 @@ export function applyLineStyles(line: Line, type: Type): Omit<Marker<CRU.UPDATE_
|
|||
|
||||
for(const field of type.fields) {
|
||||
if(field.controlColour || field.controlWidth || field.controlStroke) {
|
||||
const option = getSelectedOption(field, line.data[field.name]);
|
||||
const option = getSelectedOption(field, line.data?.[field.name]);
|
||||
|
||||
if(option) {
|
||||
if(field.controlColour && line.colour != (option.colour ?? type.defaultColour)) {
|
||||
|
@ -113,3 +134,24 @@ export function applyLineStyles(line: Line, type: Type): Omit<Marker<CRU.UPDATE_
|
|||
|
||||
return update;
|
||||
}
|
||||
|
||||
export function resolveCreateLine(line: Line<CRU.CREATE>, type: Type): Line<CRU.CREATE_VALIDATED> {
|
||||
const parsed = lineValidator.create.parse(line);
|
||||
const result: Line<CRU.CREATE_VALIDATED> = {
|
||||
...parsed,
|
||||
colour: line.colour ?? type.defaultColour,
|
||||
width: line.width ?? type.defaultWidth,
|
||||
stroke: line.stroke ?? type.defaultStroke,
|
||||
mode: line.mode ?? type.defaultMode
|
||||
};
|
||||
Object.assign(result, applyLineStyles(result, type));
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveUpdateLine(line: Line, update: Omit<Line<CRU.UPDATE>, "id">, newType: Type): Line<CRU.UPDATE_VALIDATED> {
|
||||
const resolvedUpdate = lineValidator.update.parse(update);
|
||||
return {
|
||||
...resolvedUpdate,
|
||||
...applyLineStyles({ ...line, ...resolvedUpdate }, newType)
|
||||
};
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vite";
|
||||
import dtsPlugin from "vite-plugin-dts";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({ loose: true }),
|
||||
dtsPlugin({ rollupTypes: true })
|
||||
],
|
||||
build: {
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -3736,7 +3736,7 @@ __metadata:
|
|||
leaflet-auto-graticule: ^2.0.0
|
||||
leaflet-draggable-lines: ^2.0.0
|
||||
leaflet-freie-tonne: ^2.0.1
|
||||
leaflet-highlightable-layers: ^2.1.0
|
||||
leaflet-highlightable-layers: ^3.0.0
|
||||
leaflet.markercluster: ^1.5.3
|
||||
lodash-es: ^4.17.21
|
||||
node-fetch: ^3.3.2
|
||||
|
@ -3860,6 +3860,7 @@ __metadata:
|
|||
typescript: ^5.4.2
|
||||
vite: ^5.1.5
|
||||
vite-plugin-dts: ^3.7.3
|
||||
vite-tsconfig-paths: ^4.3.2
|
||||
vitest: ^1.3.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
@ -5157,12 +5158,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"leaflet-highlightable-layers@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "leaflet-highlightable-layers@npm:2.1.0"
|
||||
"leaflet-highlightable-layers@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "leaflet-highlightable-layers@npm:3.0.0"
|
||||
peerDependencies:
|
||||
leaflet: x
|
||||
checksum: d96d3d927423990354717028f6f56c4de3d145e9d02b316ee7c9832ac2843967c16b884464ccd18f0e14e295b65f298e79dbce6cad616c248e55a9111120510e
|
||||
checksum: b0ffe1210f5f5cde618866980589f41b4083e6361624bece3bd8f1cce9e0498ad8554bea5791d73ab95fe36aeb23094f4aa372773c0c96036dcedc94ff865703
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7704,7 +7705,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsconfck@npm:^3.0.1":
|
||||
"tsconfck@npm:^3.0.1, tsconfck@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "tsconfck@npm:3.0.3"
|
||||
peerDependencies:
|
||||
|
@ -8091,6 +8092,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:^4.3.2":
|
||||
version: 4.3.2
|
||||
resolution: "vite-tsconfig-paths@npm:4.3.2"
|
||||
dependencies:
|
||||
debug: ^4.1.1
|
||||
globrex: ^0.1.2
|
||||
tsconfck: ^3.0.3
|
||||
peerDependencies:
|
||||
vite: "*"
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 7105ff641379f9f7055110f33067b236c8ee71b1390c0e6482412cdcc7a98c2e139c1c2a483d14fe9045d1977c14dc931e1ff302d6257ec919c70379db9d2419
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^5.0.0, vite@npm:^5.1.5":
|
||||
version: 5.1.5
|
||||
resolution: "vite@npm:5.1.5"
|
||||
|
|
Ładowanie…
Reference in New Issue