Porównaj commity

...

13 Commity

Autor SHA1 Wiadomość Data
Candid Dauth b7eaa44324 Fix failing marker tests 2024-03-17 04:52:39 +01:00
Candid Dauth bc4d97d998 Use Open Elevation instead of MapZen to retrieve point elevation (#250) 2024-03-16 06:54:42 +01:00
Candid Dauth db8d7d6080 Fix failing line test 2024-03-16 06:37:41 +01:00
Candid Dauth 3d3110607c Fix failing test 2024-03-16 01:02:32 +01:00
Candid Dauth 21e7aa70e3 Add more marker and line tests 2024-03-16 00:33:46 +01:00
Candid Dauth e7c377105f Add first line tests 2024-03-16 00:10:18 +01:00
Candid Dauth c97e76a6b4 Round route/line distance 2024-03-16 00:09:58 +01:00
Candid Dauth 1b5ad5838d Refactor marker/line style updating 2024-03-16 00:09:58 +01:00
Candid Dauth b42e54245d Add eslint warning for unawaited async functions 2024-03-16 00:09:58 +01:00
Candid Dauth 26753061a7 Properly store absent line extra info as null 2024-03-16 00:09:26 +01:00
Candid Dauth 5071e5b417 Fix diff on failing marker tests 2024-03-15 17:08:45 +01:00
Candid Dauth 16546e1585 Fix setting line points on line creation 2024-03-15 17:08:21 +01:00
Candid Dauth e3660ba870 Upgrade leaflet-highlightable-layers 2024-03-15 17:06:59 +01:00
50 zmienionych plików z 952 dodań i 242 usunięć

Wyświetl plik

@ -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"],

Wyświetl plik

@ -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");
});

Wyświetl plik

@ -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

Wyświetl plik

@ -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 FacilMaps 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

Wyświetl plik

@ -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
```

Wyświetl plik

@ -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> {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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); } }
]
});

Wyświetl plik

@ -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}}&#x202F;m</dd>
</template>
<template v-for="field in client.types[marker.typeId].fields" :key="field.name">

Wyświetl plik

@ -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 });

Wyświetl plik

@ -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();
});
}

Wyświetl plik

@ -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> {

Wyświetl plik

@ -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>

Wyświetl plik

@ -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();
});
}

Wyświetl plik

@ -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();
});
}

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
}
}
]

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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];
}
},

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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"
},

Wyświetl plik

@ -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 => {

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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())

Wyświetl plik

@ -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 }))
});

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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> {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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");
}
}

Wyświetl plik

@ -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[]> {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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);
})());

Wyświetl plik

@ -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([

Wyświetl plik

@ -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++) {

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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>;

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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)
};
}

Wyświetl plik

@ -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: {

Wyświetl plik

@ -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"