kopia lustrzana https://github.com/FacilMap/facilmap
Add option to export GPX files as ZIP, add Osmand line width and colour (#246)
rodzic
2999e4646c
commit
d3e3c513f6
|
@ -44,8 +44,14 @@
|
||||||
...Object.values(client.value.types).flatMap((type) => type.fields.map((field) => field.name))
|
...Object.values(client.value.types).flatMap((type) => type.fields.map((field) => field.name))
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
const routeTypeOptions = {
|
||||||
|
"tracks": "Track points",
|
||||||
|
"zip": "Track points, one file per line (ZIP file)",
|
||||||
|
"routes": "Route points"
|
||||||
|
};
|
||||||
|
|
||||||
const format = ref<keyof typeof formatOptions>("gpx");
|
const format = ref<keyof typeof formatOptions>("gpx");
|
||||||
const useTracks = ref<"1" | "0">("1");
|
const routeType = ref<keyof typeof routeTypeOptions>("tracks");
|
||||||
const filter = ref(true);
|
const filter = ref(true);
|
||||||
const hide = ref(new Set<string>());
|
const hide = ref(new Set<string>());
|
||||||
const typeId = ref<ID>();
|
const typeId = ref<ID>();
|
||||||
|
@ -69,7 +75,7 @@
|
||||||
const resolveTypeId = (typeId: ID | undefined) => typeId != null && client.value.types[typeId] ? typeId : undefined;
|
const resolveTypeId = (typeId: ID | undefined) => typeId != null && client.value.types[typeId] ? typeId : undefined;
|
||||||
const resolvedTypeId = computed(() => resolveTypeId(typeId.value));
|
const resolvedTypeId = computed(() => resolveTypeId(typeId.value));
|
||||||
|
|
||||||
const canSelectUseTracks = computed(() => format.value === "gpx");
|
const canSelectRouteType = computed(() => format.value === "gpx");
|
||||||
const canSelectType = computed(() => format.value === "csv" || (format.value === "table" && method.value === "copy"));
|
const canSelectType = computed(() => format.value === "csv" || (format.value === "table" && method.value === "copy"));
|
||||||
const mustSelectType = computed(() => canSelectType.value);
|
const mustSelectType = computed(() => canSelectType.value);
|
||||||
const canSelectHide = computed(() => ["table", "csv"].includes(format.value));
|
const canSelectHide = computed(() => ["table", "csv"].includes(format.value));
|
||||||
|
@ -83,8 +89,8 @@
|
||||||
|
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (canSelectUseTracks.value) {
|
if (canSelectRouteType.value) {
|
||||||
params.set("useTracks", useTracks.value);
|
params.set("useTracks", routeType.value === "routes" ? "0" : "1");
|
||||||
}
|
}
|
||||||
if (canSelectHide.value && hide.value.size > 0) {
|
if (canSelectHide.value && hide.value.size > 0) {
|
||||||
params.set("hide", [...hide.value].join(","));
|
params.set("hide", [...hide.value].join(","));
|
||||||
|
@ -130,6 +136,16 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "gpx": {
|
||||||
|
return (
|
||||||
|
context.baseUrl
|
||||||
|
+ client.value.padData!.id
|
||||||
|
+ `/${format.value}`
|
||||||
|
+ (routeType.value === "zip" ? `/zip` : "")
|
||||||
|
+ (paramsStr ? `?${paramsStr}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return (
|
return (
|
||||||
context.baseUrl
|
context.baseUrl
|
||||||
|
@ -232,14 +248,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="canSelectUseTracks" class="row mb-3">
|
<div v-if="canSelectRouteType" class="row mb-3">
|
||||||
<label class="col-sm-3 col-form-label" :for="`${id}-route-type-select`">
|
<label class="col-sm-3 col-form-label" :for="`${id}-route-type-select`">
|
||||||
Route type
|
Route type
|
||||||
<HelpPopover>
|
<HelpPopover>
|
||||||
<p>
|
<p>
|
||||||
<strong>Track points</strong> will export your lines exactly as they are on your map.
|
<strong>Track points</strong> will export your lines exactly as they are on your map.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Track points, one file per line (ZIP file)</strong> will create a ZIP file with one GPX file
|
||||||
|
for all markers and one GPX file for each line. This works better with apps such as Osmand that only
|
||||||
|
support one line style per file.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Route points</strong> will export only the from/via/to route points of your lines, and your
|
<strong>Route points</strong> will export only the from/via/to route points of your lines, and your
|
||||||
navigation software/device will have to calculate the route using its own map data and algorithm.
|
navigation software/device will have to calculate the route using its own map data and algorithm.
|
||||||
|
@ -247,9 +267,8 @@
|
||||||
</HelpPopover>
|
</HelpPopover>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="form-select" v-model="useTracks" :id="`${id}-route-type-select`">
|
<select class="form-select" v-model="routeType" :id="`${id}-route-type-select`">
|
||||||
<option value="1">Track points</option>
|
<option v-for="(label, value) in routeTypeOptions" :value="value" :key="value">{{label}}</option>
|
||||||
<option value="0">Route points</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { useToasts } from "./toasts/toasts.vue";
|
import { useToasts } from "./toasts/toasts.vue";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import vTooltip from "../../utils/tooltip";
|
import vTooltip from "../../utils/tooltip";
|
||||||
|
import { getSafeFilename } from "facilmap-utils";
|
||||||
|
|
||||||
const context = injectContextRequired();
|
const context = injectContextRequired();
|
||||||
const toasts = useToasts();
|
const toasts = useToasts();
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exported = await props.getExport(format);
|
const exported = await props.getExport(format);
|
||||||
saveAs(new Blob([exported], { type: "application/gpx+xml" }), `${props.filename}.gpx`);
|
saveAs(new Blob([exported], { type: "application/gpx+xml" }), `${getSafeFilename(props.filename)}.gpx`);
|
||||||
} catch(err: any) {
|
} catch(err: any) {
|
||||||
toasts.showErrorToast(`fm${context.id}-export-dropdown-error`, "Error exporting route", err);
|
toasts.showErrorToast(`fm${context.id}-export-dropdown-error`, "Error exporting route", err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -149,12 +149,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.fm-popover {
|
.fm-popover.fm-popover {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
> .popover-body {
|
> .popover-body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -66,6 +66,7 @@
|
||||||
"string-similarity": "^4.0.4",
|
"string-similarity": "^4.0.4",
|
||||||
"strip-bom-buf": "^4.0.0",
|
"strip-bom-buf": "^4.0.0",
|
||||||
"unzipper": "^0.10.14",
|
"unzipper": "^0.10.14",
|
||||||
|
"zip-stream": "^6.0.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default class DatabaseHelpers {
|
||||||
this._db = db;
|
this._db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateObjectStyles(objects: Marker | Line | AsyncGenerator<Marker | Line, void, void>): Promise<void> {
|
async _updateObjectStyles(objects: Marker | Line | AsyncIterable<Marker | Line>): Promise<void> {
|
||||||
const types: Record<ID, Type> = { };
|
const types: Record<ID, Type> = { };
|
||||||
for await (const object of Symbol.asyncIterator in objects ? objects : arrayToAsyncIterator([objects])) {
|
for await (const object of Symbol.asyncIterator in objects ? objects : arrayToAsyncIterator([objects])) {
|
||||||
const padId = object.padId;
|
const padId = object.padId;
|
||||||
|
@ -187,7 +187,7 @@ export default class DatabaseHelpers {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async* _getPadObjects<T>(type: string, padId: PadId, condition?: FindOptions): AsyncGenerator<T, void, void> {
|
async* _getPadObjects<T>(type: string, padId: PadId, condition?: FindOptions): AsyncIterable<T> {
|
||||||
const includeData = [ "Marker", "Line" ].includes(type);
|
const includeData = [ "Marker", "Line" ].includes(type);
|
||||||
|
|
||||||
if(includeData) {
|
if(includeData) {
|
||||||
|
@ -390,10 +390,23 @@ export default class DatabaseHelpers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _bulkCreateInBatches<T>(model: ModelCtor<Model>, data: Array<Record<string, unknown>>): Promise<Array<T>> {
|
async _bulkCreateInBatches<T>(model: ModelCtor<Model>, data: Iterable<Record<string, unknown>> | AsyncIterable<Record<string, unknown>>): Promise<Array<T>> {
|
||||||
const result: Array<any> = [];
|
const result: Array<any> = [];
|
||||||
for(let i=0; i<data.length; i+=ITEMS_PER_BATCH)
|
let slice: Array<Record<string, unknown>> = [];
|
||||||
result.push(...(await model.bulkCreate(data.slice(i, i+ITEMS_PER_BATCH))).map((it) => it.toJSON()));
|
const createSlice = async () => {
|
||||||
|
result.push(...(await model.bulkCreate(slice)).map((it) => it.toJSON()));
|
||||||
|
slice = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for await (const item of data) {
|
||||||
|
slice.push(item);
|
||||||
|
if (slice.length >= ITEMS_PER_BATCH) {
|
||||||
|
createSlice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slice.length > 0) {
|
||||||
|
createSlice();
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default class DatabaseHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getHistory(padId: PadId, types?: HistoryEntryType[]): AsyncGenerator<HistoryEntry, void, never> {
|
getHistory(padId: PadId, types?: HistoryEntryType[]): AsyncIterable<HistoryEntry> {
|
||||||
const query: FindOptions = { order: [[ "time", "DESC" ]] };
|
const query: FindOptions = { order: [[ "time", "DESC" ]] };
|
||||||
if(types)
|
if(types)
|
||||||
query.where = {type: types};
|
query.where = {type: types};
|
||||||
|
|
|
@ -179,22 +179,15 @@ export default class DatabaseLines {
|
||||||
this.LineModel.hasMany(this.LineDataModel, { foreignKey: "lineId" });
|
this.LineModel.hasMany(this.LineDataModel, { foreignKey: "lineId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
getPadLines(padId: PadId, fields?: Array<keyof Line>): AsyncGenerator<Line, void, void> {
|
getPadLines(padId: PadId, fields?: Array<keyof Line>): AsyncIterable<Line> {
|
||||||
const cond = fields ? { attributes: fields } : { };
|
const cond = fields ? { attributes: fields } : { };
|
||||||
return this._db.helpers._getPadObjects<Line>("Line", padId, cond);
|
return this._db.helpers._getPadObjects<Line>("Line", padId, cond);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPadLinesByType(padId: PadId, typeId: ID): AsyncGenerator<Line, void, void> {
|
getPadLinesByType(padId: PadId, typeId: ID): AsyncIterable<Line> {
|
||||||
return this._db.helpers._getPadObjects<Line>("Line", padId, { where: { typeId: typeId } });
|
return this._db.helpers._getPadObjects<Line>("Line", padId, { where: { typeId: typeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async* getPadLinesWithPoints(padId: PadId): AsyncGenerator<LineWithTrackPoints, void, void> {
|
|
||||||
for await (const line of this.getPadLines(padId)) {
|
|
||||||
const trackPoints = await this.getAllLinePoints(line.id);
|
|
||||||
yield { ...line, trackPoints };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLineTemplate(padId: PadId, data: { typeId: ID }): Promise<Line> {
|
async getLineTemplate(padId: PadId, data: { typeId: ID }): Promise<Line> {
|
||||||
const lineTemplate = {
|
const lineTemplate = {
|
||||||
...this.LineModel.build({ ...data, padId: padId } satisfies Partial<CreationAttributes<LineModel>> as any).toJSON(),
|
...this.LineModel.build({ ...data, padId: padId } satisfies Partial<CreationAttributes<LineModel>> as any).toJSON(),
|
||||||
|
@ -310,7 +303,7 @@ export default class DatabaseLines {
|
||||||
return oldLine;
|
return oldLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
async* getLinePointsForPad(padId: PadId, bboxWithZoom: BboxWithZoom & BboxWithExcept): AsyncGenerator<{ id: ID; trackPoints: TrackPoint[] }, void, void> {
|
async* getLinePointsForPad(padId: PadId, bboxWithZoom: BboxWithZoom & BboxWithExcept): AsyncIterable<{ id: ID; trackPoints: TrackPoint[] }> {
|
||||||
const lines = await this.LineModel.findAll({ attributes: ["id"], where: { padId } });
|
const lines = await this.LineModel.findAll({ attributes: ["id"], where: { padId } });
|
||||||
const chunks = chunk(lines.map((line) => line.id), 50000);
|
const chunks = chunk(lines.map((line) => line.id), 50000);
|
||||||
for (const lineIds of chunks) {
|
for (const lineIds of chunks) {
|
||||||
|
@ -336,12 +329,14 @@ export default class DatabaseLines {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllLinePoints(lineId: ID): Promise<TrackPoint[]> {
|
async* getAllLinePoints(lineId: ID): AsyncIterable<TrackPoint> {
|
||||||
const points = await this.LineModel.build({ id: lineId } satisfies Partial<CreationAttributes<LineModel>> as any).getLinePoints({
|
const points = await this.LineModel.build({ id: lineId } satisfies Partial<CreationAttributes<LineModel>> as any).getLinePoints({
|
||||||
attributes: [ "pos", "lat", "lon", "ele", "zoom", "idx" ],
|
attributes: [ "pos", "lat", "lon", "ele", "zoom", "idx" ],
|
||||||
order: [["idx", "ASC"]]
|
order: [["idx", "ASC"]]
|
||||||
});
|
});
|
||||||
return points.map((point) => omit(point.toJSON(), ["pos"]) as TrackPoint);
|
for (const point of points) {
|
||||||
|
yield omit(point.toJSON(), ["pos"]) as TrackPoint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,11 +76,11 @@ export default class DatabaseMarkers {
|
||||||
this.MarkerModel.hasMany(this.MarkerDataModel, { foreignKey: "markerId" });
|
this.MarkerModel.hasMany(this.MarkerDataModel, { foreignKey: "markerId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
getPadMarkers(padId: PadId, bbox?: BboxWithZoom & BboxWithExcept): AsyncGenerator<Marker, void, void> {
|
getPadMarkers(padId: PadId, bbox?: BboxWithZoom & BboxWithExcept): AsyncIterable<Marker> {
|
||||||
return this._db.helpers._getPadObjects<Marker>("Marker", padId, { where: this._db.helpers.makeBboxCondition(bbox) });
|
return this._db.helpers._getPadObjects<Marker>("Marker", padId, { where: this._db.helpers.makeBboxCondition(bbox) });
|
||||||
}
|
}
|
||||||
|
|
||||||
getPadMarkersByType(padId: PadId, typeId: ID): AsyncGenerator<Marker, void, void> {
|
getPadMarkersByType(padId: PadId, typeId: ID): AsyncIterable<Marker> {
|
||||||
return this._db.helpers._getPadObjects<Marker>("Marker", padId, { where: { padId: padId, typeId: typeId } });
|
return this._db.helpers._getPadObjects<Marker>("Marker", padId, { where: { padId: padId, typeId: typeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { type BboxWithExcept, createModel, getPosType, getVirtualLatType, getVir
|
||||||
import { calculateRouteForLine } from "../routing/routing.js";
|
import { calculateRouteForLine } from "../routing/routing.js";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import type { Point as GeoJsonPoint } from "geojson";
|
import type { Point as GeoJsonPoint } from "geojson";
|
||||||
|
import { asyncIteratorToArray } from "../utils/streams.js";
|
||||||
|
|
||||||
const updateTimes: Record<string, number> = {};
|
const updateTimes: Record<string, number> = {};
|
||||||
|
|
||||||
|
@ -155,24 +156,24 @@ export default class DatabaseRoutes {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const line = await this._db.lines.getLine(padId, lineId);
|
const line = await this._db.lines.getLine(padId, lineId);
|
||||||
const linePoints = await this._db.lines.getAllLinePoints(lineId);
|
const linePointsIt = this._db.lines.getAllLinePoints(lineId);
|
||||||
|
const linePoints = await asyncIteratorToArray((async function*() {
|
||||||
|
for await (const linePoint of linePointsIt) {
|
||||||
|
yield {
|
||||||
|
routeId,
|
||||||
|
lat: linePoint.lat,
|
||||||
|
lon: linePoint.lon,
|
||||||
|
ele: linePoint.ele,
|
||||||
|
zoom: linePoint.zoom,
|
||||||
|
idx: linePoint.idx
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
|
||||||
if(thisTime != updateTimes[routeId])
|
if(thisTime != updateTimes[routeId])
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const create = [];
|
await this._db.helpers._bulkCreateInBatches(this.RoutePointModel, linePoints);
|
||||||
for(const linePoint of linePoints) {
|
|
||||||
create.push({
|
|
||||||
routeId,
|
|
||||||
lat: linePoint.lat,
|
|
||||||
lon: linePoint.lon,
|
|
||||||
ele: linePoint.ele,
|
|
||||||
zoom: linePoint.zoom,
|
|
||||||
idx: linePoint.idx
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._db.helpers._bulkCreateInBatches(this.RoutePointModel, create);
|
|
||||||
|
|
||||||
if(thisTime != updateTimes[routeId])
|
if(thisTime != updateTimes[routeId])
|
||||||
return;
|
return;
|
||||||
|
@ -214,12 +215,14 @@ export default class DatabaseRoutes {
|
||||||
return data.map((d) => omit(d.toJSON(), ["pos"]) as TrackPoint);
|
return data.map((d) => omit(d.toJSON(), ["pos"]) as TrackPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllRoutePoints(routeId: string): Promise<TrackPoint[]> {
|
async* getAllRoutePoints(routeId: string): AsyncIterable<TrackPoint> {
|
||||||
const data = await this.RoutePointModel.findAll({
|
const points = await this.RoutePointModel.findAll({
|
||||||
where: {routeId},
|
where: { routeId },
|
||||||
attributes: [ "pos", "lat", "lon", "idx", "ele", "zoom"]
|
attributes: [ "pos", "lat", "lon", "idx", "ele", "zoom"]
|
||||||
});
|
});
|
||||||
return data.map((d) => omit(d.toJSON(), ["pos"]) as TrackPoint);
|
for (const point of points) {
|
||||||
|
yield omit(point.toJSON(), ["pos"]) as TrackPoint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ export default class DatabaseTypes {
|
||||||
PadModel.hasMany(this.TypeModel, { foreignKey: "padId" });
|
PadModel.hasMany(this.TypeModel, { foreignKey: "padId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypes(padId: PadId): AsyncGenerator<Type, void, void> {
|
getTypes(padId: PadId): AsyncIterable<Type> {
|
||||||
return this._db.helpers._getPadObjects<Type>("Type", padId);
|
return this._db.helpers._getPadObjects<Type>("Type", padId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default class DatabaseViews {
|
||||||
this._db.pads.PadModel.hasMany(this.ViewModel, { foreignKey: "padId" });
|
this._db.pads.PadModel.hasMany(this.ViewModel, { foreignKey: "padId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
getViews(padId: PadId): AsyncGenerator<View, void, void> {
|
getViews(padId: PadId): AsyncIterable<View> {
|
||||||
return this._db.helpers._getPadObjects<View>("View", padId);
|
return this._db.helpers._getPadObjects<View>("View", padId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { jsonStream, asyncIteratorToArray, streamPromiseToStream } from "../utils/streams.js";
|
import { asyncIteratorToArray, streamPromiseToStream, jsonStreamArray, mapAsyncIterator, jsonStreamRecord, type JsonStream, concatAsyncIterators, flatMapAsyncIterator } from "../utils/streams.js";
|
||||||
import { compileExpression } from "facilmap-utils";
|
import { compileExpression } from "facilmap-utils";
|
||||||
import type { Marker, MarkerFeature, LineFeature, PadId } from "facilmap-types";
|
import type { Marker, MarkerFeature, PadId, TrackPoint, Line } from "facilmap-types";
|
||||||
import Database from "../database/database.js";
|
import Database from "../database/database.js";
|
||||||
import { cloneDeep, keyBy, mapValues, omit } from "lodash-es";
|
import { cloneDeep, keyBy, mapValues, omit } from "lodash-es";
|
||||||
import type { LineWithTrackPoints } from "../database/line.js";
|
|
||||||
import type { ReadableStream } from "stream/web";
|
import type { ReadableStream } from "stream/web";
|
||||||
|
|
||||||
export function exportGeoJson(database: Database, padId: PadId, filter?: string): ReadableStream<string> {
|
export function exportGeoJson(database: Database, padId: PadId, filter?: string): ReadableStream<string> {
|
||||||
|
@ -17,49 +16,41 @@ export function exportGeoJson(database: Database, padId: PadId, filter?: string)
|
||||||
|
|
||||||
const types = keyBy(await asyncIteratorToArray(database.types.getTypes(padId)), "id");
|
const types = keyBy(await asyncIteratorToArray(database.types.getTypes(padId)), "id");
|
||||||
|
|
||||||
return jsonStream({
|
return jsonStreamRecord({
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
...(padData.defaultView ? { bbox: "%bbox%" } : { }),
|
...(padData.defaultView ? {
|
||||||
facilmap: {
|
bbox: [padData.defaultView.left, padData.defaultView.bottom, padData.defaultView.right, padData.defaultView.top]
|
||||||
name: "%name%",
|
} : { }),
|
||||||
searchEngines: "%searchEngines%",
|
facilmap: jsonStreamRecord({
|
||||||
description: "%description%",
|
name: padData.name,
|
||||||
clusterMarkers: "%clusterMarkers",
|
searchEngines: padData.searchEngines,
|
||||||
views: "%views%",
|
description: padData.description,
|
||||||
types: "%types%"
|
clusterMarkers: padData.clusterMarkers,
|
||||||
},
|
views: jsonStreamArray(mapAsyncIterator(database.views.getViews(padId), (view) => omit(view, ["id", "padId"])))
|
||||||
features: "%features%"
|
}),
|
||||||
}, {
|
|
||||||
bbox: padData.defaultView && [padData.defaultView.left, padData.defaultView.bottom, padData.defaultView.right, padData.defaultView.top],
|
|
||||||
name: padData.name,
|
|
||||||
searchEngines: padData.searchEngines,
|
|
||||||
description: padData.description,
|
|
||||||
clusterMarkers: padData.clusterMarkers,
|
|
||||||
views: async function*() {
|
|
||||||
for await (const view of database.views.getViews(padId)) {
|
|
||||||
yield omit(view, ["id", "padId"]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
types: mapValues(types, (type) => omit(type, ["id", "padId"])),
|
types: mapValues(types, (type) => omit(type, ["id", "padId"])),
|
||||||
features: async function*() {
|
features: jsonStreamArray(concatAsyncIterators(
|
||||||
for await (const marker of database.markers.getPadMarkers(padId)) {
|
flatMapAsyncIterator(database.markers.getPadMarkers(padId), (marker) => {
|
||||||
if (filterFunc(marker, types[marker.typeId])) {
|
if (filterFunc(marker, types[marker.typeId])) {
|
||||||
yield markerToGeoJson(marker);
|
return [markerToGeoJson(marker)];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
|
flatMapAsyncIterator(database.lines.getPadLines(padId), (line) => {
|
||||||
for await (const line of database.lines.getPadLinesWithPoints(padId)) {
|
|
||||||
if (filterFunc(line, types[line.typeId])) {
|
if (filterFunc(line, types[line.typeId])) {
|
||||||
yield lineToGeoJson(line);
|
return [lineToGeoJson(line, database.lines.getAllLinePoints(line.id))];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
))
|
||||||
});
|
});
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
function markerToGeoJson(marker: Marker): MarkerFeature {
|
function markerToGeoJson(marker: Marker): JsonStream {
|
||||||
return {
|
return jsonStreamRecord({
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
|
@ -74,16 +65,16 @@ function markerToGeoJson(marker: Marker): MarkerFeature {
|
||||||
data: cloneDeep(marker.data),
|
data: cloneDeep(marker.data),
|
||||||
typeId: marker.typeId
|
typeId: marker.typeId
|
||||||
}
|
}
|
||||||
};
|
} satisfies MarkerFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineToGeoJson(line: LineWithTrackPoints): LineFeature {
|
function lineToGeoJson(line: Line, trackPoints: AsyncIterable<TrackPoint>): ReadableStream<string> {
|
||||||
return {
|
return jsonStreamRecord({
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
geometry: {
|
geometry: jsonStreamRecord({
|
||||||
type: "LineString",
|
type: "LineString",
|
||||||
coordinates: line.trackPoints.map((trackPoint) => [trackPoint.lon, trackPoint.lat])
|
coordinates: jsonStreamArray(mapAsyncIterator(trackPoints, (trackPoint) => [trackPoint.lon, trackPoint.lat]))
|
||||||
},
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
name: line.name,
|
name: line.name,
|
||||||
mode: line.mode,
|
mode: line.mode,
|
||||||
|
@ -96,5 +87,5 @@ function lineToGeoJson(line: LineWithTrackPoints): LineFeature {
|
||||||
routePoints: line.routePoints,
|
routePoints: line.routePoints,
|
||||||
typeId: line.typeId
|
typeId: line.typeId
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FacilMap" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
|
|
||||||
<metadata>
|
|
||||||
<name><%=line.name || 'FacilMap route'%></name>
|
|
||||||
<time><%=time%></time>
|
|
||||||
</metadata>
|
|
||||||
<<%=useTracks ? 'trk' : 'rte'%>>
|
|
||||||
<name><%=line.name || 'FacilMap route'%></name>
|
|
||||||
<% if(desc) { -%>
|
|
||||||
<desc><%=desc%></desc>
|
|
||||||
<% } -%>
|
|
||||||
<% if(useTracks) { -%>
|
|
||||||
<trkseg>
|
|
||||||
<% for(let trackPoint of line.trackPoints) { -%>
|
|
||||||
<trkpt lat="<%=trackPoint.lat%>" lon="<%=trackPoint.lon%>"<% if(trackPoint.ele != null){ %> ele="<%=trackPoint.ele%>"<% } %> />
|
|
||||||
<% } -%>
|
|
||||||
</trkseg>
|
|
||||||
<% } else { -%>
|
|
||||||
<% for(let routePoint of line.routePoints) { -%>
|
|
||||||
<rtept lat="<%=routePoint.lat%>" lon="<%=routePoint.lon%>" />
|
|
||||||
<% } -%>
|
|
||||||
<% } -%>
|
|
||||||
</<%=useTracks ? 'trk' : 'rte'%>>
|
|
||||||
</gpx>
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { asyncIteratorToArray, asyncIteratorToStream } from "../utils/streams.js";
|
import { asyncIteratorToArray, asyncIteratorToStream, getZipEncodeStream, indentStream, stringToStream, type ZipEncodeStreamItem } from "../utils/streams.js";
|
||||||
import { compile } from "ejs";
|
|
||||||
import Database from "../database/database.js";
|
import Database from "../database/database.js";
|
||||||
import type { Field, PadId, Type } from "facilmap-types";
|
import type { Field, Line, Marker, PadId, TrackPoint, Type } from "facilmap-types";
|
||||||
import { compileExpression, normalizeLineName, normalizeMarkerName, normalizePadName, quoteHtml } from "facilmap-utils";
|
import { compileExpression, getSafeFilename, normalizeLineName, normalizeMarkerName, normalizePadName, quoteHtml } from "facilmap-utils";
|
||||||
import type { LineWithTrackPoints } from "../database/line.js";
|
import type { LineWithTrackPoints } from "../database/line.js";
|
||||||
import { keyBy } from "lodash-es";
|
import { keyBy } from "lodash-es";
|
||||||
import gpxLineEjs from "./gpx-line.ejs?raw";
|
|
||||||
import type { ReadableStream } from "stream/web";
|
import type { ReadableStream } from "stream/web";
|
||||||
|
|
||||||
const lineTemplate = compile(gpxLineEjs);
|
const gpxHeader = (
|
||||||
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||||
|
`<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FacilMap" version="1.1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">`
|
||||||
|
);
|
||||||
|
|
||||||
|
const gpxFooter = (
|
||||||
|
`</gpx>`
|
||||||
|
);
|
||||||
|
|
||||||
const markerShapeToOsmand: Record<string, string> = {
|
const markerShapeToOsmand: Record<string, string> = {
|
||||||
"drop": "circle",
|
"drop": "circle",
|
||||||
|
@ -34,6 +39,67 @@ function dataToText(fields: Field[], data: Record<string, string>) {
|
||||||
return text.join('\n\n');
|
return text.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMetadataGpx(data: { name: string }, extensions: Record<string, string> = {}): string {
|
||||||
|
return (
|
||||||
|
`<metadata>\n` +
|
||||||
|
Object.entries({
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
...data
|
||||||
|
}).map(([k, v]) => `\t<${quoteHtml(k)}>${quoteHtml(v)}</${quoteHtml(k)}>\n`).join("") +
|
||||||
|
`</metadata>` +
|
||||||
|
(Object.keys(extensions).length > 0 ? (
|
||||||
|
`\n` +
|
||||||
|
`<extensions>\n` +
|
||||||
|
Object.entries(extensions).map(([k, v]) => `\t<${quoteHtml(k)}>${quoteHtml(v)}</${quoteHtml(k)}>\n`).join("") +
|
||||||
|
`</extensions>`
|
||||||
|
) : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkerGpx(marker: Marker, type: Type): ReadableStream<string> {
|
||||||
|
const osmandBackground = markerShapeToOsmand[marker.shape || "drop"];
|
||||||
|
return stringToStream(
|
||||||
|
`<wpt lat="${quoteHtml(marker.lat)}" lon="${quoteHtml(marker.lon)}"${marker.ele != null ? ` ele="${quoteHtml(marker.ele)}"` : ""}>\n` +
|
||||||
|
`\t<name>${quoteHtml(normalizeMarkerName(marker.name))}</name>\n` +
|
||||||
|
`\t<desc>${quoteHtml(dataToText(type.fields, marker.data))}</desc>\n` +
|
||||||
|
`\t<extensions>\n` +
|
||||||
|
(osmandBackground ? `\t\t<osmand:background>${osmandBackground}</osmand:background>\n` : "") +
|
||||||
|
`\t\t<osmand:color>#${marker.colour}</osmand:color>\n` +
|
||||||
|
`\t</extensions>\n` +
|
||||||
|
`</wpt>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineRouteGpx(line: LineForExport, type: Type | undefined): ReadableStream<string> {
|
||||||
|
return stringToStream(
|
||||||
|
`<rte>\n` +
|
||||||
|
`\t<name>${quoteHtml(normalizeLineName(line.name))}</name>\n` +
|
||||||
|
(type ? `\t<desc>${quoteHtml(dataToText(type.fields, line.data ?? {}))}</desc>\n` : "") +
|
||||||
|
line.routePoints.map((routePoint) => (
|
||||||
|
`\t<rtept lat="${quoteHtml(routePoint.lat)}" lon="${quoteHtml(routePoint.lon)}" />\n`
|
||||||
|
)).join("") +
|
||||||
|
`</rte>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineTrackGpx(line: LineForExport, type: Type | undefined, trackPoints: AsyncIterable<TrackPoint>): ReadableStream<string> {
|
||||||
|
return asyncIteratorToStream((async function*() {
|
||||||
|
yield (
|
||||||
|
`<trk>\n` +
|
||||||
|
`\t<name>${quoteHtml(normalizeLineName(line.name))}</name>\n` +
|
||||||
|
(type ? `\t<desc>${quoteHtml(dataToText(type.fields, line.data ?? {}))}</desc>\n` : "") +
|
||||||
|
`\t<trkseg>\n`
|
||||||
|
);
|
||||||
|
for await (const trackPoint of trackPoints) {
|
||||||
|
yield `\t\t<trkpt lat="${quoteHtml(trackPoint.lat)}" lon="${quoteHtml(trackPoint.lon)}"${trackPoint.ele != null ? ` ele="${quoteHtml(trackPoint.ele)}"` : ""} />\n`;
|
||||||
|
}
|
||||||
|
yield (
|
||||||
|
`\t</trkseg>\n` +
|
||||||
|
`</trk>`
|
||||||
|
);
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
export function exportGpx(database: Database, padId: PadId, useTracks: boolean, filter?: string): ReadableStream<string> {
|
export function exportGpx(database: Database, padId: PadId, useTracks: boolean, filter?: string): ReadableStream<string> {
|
||||||
return asyncIteratorToStream((async function* () {
|
return asyncIteratorToStream((async function* () {
|
||||||
const filterFunc = compileExpression(filter);
|
const filterFunc = compileExpression(filter);
|
||||||
|
@ -47,69 +113,150 @@ export function exportGpx(database: Database, padId: PadId, useTracks: boolean,
|
||||||
throw new Error(`Pad ${padId} could not be found.`);
|
throw new Error(`Pad ${padId} could not be found.`);
|
||||||
|
|
||||||
yield (
|
yield (
|
||||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
`${gpxHeader}\n` +
|
||||||
`<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="FacilMap" version="1.1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">\n` +
|
`\t${getMetadataGpx({ name: normalizePadName(padData.name) }).replaceAll("\n", "\n\t")}\n`
|
||||||
`\t<metadata>\n` +
|
|
||||||
`\t\t<name>${quoteHtml(normalizePadName(padData.name))}</name>\n` +
|
|
||||||
`\t\t<time>${quoteHtml(new Date().toISOString())}</time>\n` +
|
|
||||||
`\t</metadata>\n`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const marker of database.markers.getPadMarkers(padId)) {
|
for await (const marker of database.markers.getPadMarkers(padId)) {
|
||||||
if (filterFunc(marker, types[marker.typeId])) {
|
if (filterFunc(marker, types[marker.typeId])) {
|
||||||
const osmandBackground = markerShapeToOsmand[marker.shape || "drop"];
|
for await (const chunk of indentStream(getMarkerGpx(marker, types[marker.typeId]), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
yield (
|
yield chunk;
|
||||||
`\t<wpt lat="${quoteHtml(marker.lat)}" lon="${quoteHtml(marker.lon)}"${marker.ele != null ? ` ele="${quoteHtml(marker.ele)}"` : ""}>\n` +
|
|
||||||
`\t\t<name>${quoteHtml(normalizeMarkerName(marker.name))}</name>\n` +
|
|
||||||
`\t\t<desc>${quoteHtml(dataToText(types[marker.typeId].fields, marker.data))}</desc>\n` +
|
|
||||||
`\t\t<extensions>\n` +
|
|
||||||
(osmandBackground ? `\t\t\t<osmand:background>${osmandBackground}</osmand:background>\n` : "") +
|
|
||||||
`\t\t\t<osmand:color>#${marker.colour}</osmand:color>\n` +
|
|
||||||
`\t\t</extensions>\n` +
|
|
||||||
`\t</wpt>\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const line of database.lines.getPadLinesWithPoints(padId)) {
|
|
||||||
if (filterFunc(line, types[line.typeId])) {
|
|
||||||
if (useTracks || line.mode == "track") {
|
|
||||||
yield (
|
|
||||||
`\t<trk>\n` +
|
|
||||||
`\t\t<name>${quoteHtml(normalizeLineName(line.name))}</name>\n` +
|
|
||||||
`\t\t<desc>${dataToText(types[line.typeId].fields, line.data)}</desc>\n` +
|
|
||||||
`\t\t<trkseg>\n` +
|
|
||||||
line.trackPoints.map((trackPoint) => (
|
|
||||||
`\t\t\t<trkpt lat="${quoteHtml(trackPoint.lat)}" lon="${quoteHtml(trackPoint.lon)}"${trackPoint.ele != null ? ` ele="${quoteHtml(trackPoint.ele)}"` : ""} />\n`
|
|
||||||
)).join("") +
|
|
||||||
`\t\t</trkseg>\n` +
|
|
||||||
`\t</trk>\n`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
yield (
|
|
||||||
`\t<rte>\n` +
|
|
||||||
`\t\t<name>${quoteHtml(normalizeLineName(line.name))}</name>\n` +
|
|
||||||
`\t\t<desc>${quoteHtml(dataToText(types[line.typeId].fields, line.data))}</desc>\n` +
|
|
||||||
line.routePoints.map((routePoint) => (
|
|
||||||
`\t\t<rtept lat="${quoteHtml(routePoint.lat)}" lon="${quoteHtml(routePoint.lon)}" />\n`
|
|
||||||
)).join("") +
|
|
||||||
`\t</rte>\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yield `</gpx>`;
|
for await (const line of database.lines.getPadLines(padId)) {
|
||||||
|
if (filterFunc(line, types[line.typeId])) {
|
||||||
|
if (useTracks || line.mode == "track") {
|
||||||
|
const trackPoints = database.lines.getAllLinePoints(line.id);
|
||||||
|
for await (const chunk of indentStream(getLineTrackGpx(line, types[line.typeId], trackPoints), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for await (const chunk of indentStream(getLineRouteGpx(line, types[line.typeId]), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield gpxFooter;
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
type LineForExport = Partial<Pick<LineWithTrackPoints, "name" | "data" | "mode" | "trackPoints" | "routePoints">>;
|
export function exportGpxZip(database: Database, padId: PadId, useTracks: boolean, filter?: string): ReadableStream<Uint8Array> {
|
||||||
|
const encodeZipStream = getZipEncodeStream();
|
||||||
|
|
||||||
export async function exportLineToGpx(line: LineForExport, type: Type | undefined, useTracks: boolean): Promise<string> {
|
asyncIteratorToStream((async function*(): AsyncIterable<ZipEncodeStreamItem> {
|
||||||
return lineTemplate({
|
const filterFunc = compileExpression(filter);
|
||||||
useTracks: (useTracks || line.mode == "track"),
|
|
||||||
time: new Date().toISOString(),
|
const [padData, types] = await Promise.all([
|
||||||
desc: type && dataToText(type.fields, line.data ?? {}),
|
database.pads.getPadData(padId),
|
||||||
line
|
asyncIteratorToArray(database.types.getTypes(padId)).then((types) => keyBy(types, 'id'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!padData) {
|
||||||
|
throw new Error(`Pad ${padId} could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
filename: "markers.gpx",
|
||||||
|
data: asyncIteratorToStream((async function*() {
|
||||||
|
yield (
|
||||||
|
`${gpxHeader}\n` +
|
||||||
|
`\t${getMetadataGpx({ name: normalizePadName(padData.name) }).replaceAll("\n", "\n\t")}\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const marker of database.markers.getPadMarkers(padId)) {
|
||||||
|
if (filterFunc(marker, types[marker.typeId])) {
|
||||||
|
for await (const chunk of indentStream(getMarkerGpx(marker, types[marker.typeId]), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield gpxFooter;
|
||||||
|
})())
|
||||||
|
};
|
||||||
|
|
||||||
|
yield {
|
||||||
|
filename: "lines/",
|
||||||
|
data: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const names = new Set<string>();
|
||||||
|
|
||||||
|
for await (const line of database.lines.getPadLines(padId)) {
|
||||||
|
if (filterFunc(line, types[line.typeId])) {
|
||||||
|
const lineName = normalizeLineName(line.name);
|
||||||
|
let name = lineName;
|
||||||
|
for (let i = 1; names.has(name); i++) {
|
||||||
|
name = `${lineName} (${i})`;
|
||||||
|
}
|
||||||
|
names.add(name);
|
||||||
|
|
||||||
|
const filename = `lines/${getSafeFilename(name)}.gpx`;
|
||||||
|
|
||||||
|
if (useTracks || line.mode == "track") {
|
||||||
|
const trackPoints = database.lines.getAllLinePoints(line.id);
|
||||||
|
yield {
|
||||||
|
filename,
|
||||||
|
data: exportLineToTrackGpx(line, types[line.typeId], trackPoints)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
yield {
|
||||||
|
filename,
|
||||||
|
data: exportLineToRouteGpx(line, types[line.typeId])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()).pipeTo(encodeZipStream.writable);
|
||||||
|
|
||||||
|
return encodeZipStream.readable;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineForExport = Pick<LineWithTrackPoints, "name" | "data" | "mode" | "routePoints"> & Partial<Pick<Line, "colour" | "width">>;
|
||||||
|
|
||||||
|
function getLineMetadataGpx(line: LineForExport): string {
|
||||||
|
return getMetadataGpx({
|
||||||
|
name: normalizeLineName(line.name)
|
||||||
|
}, {
|
||||||
|
...(line.colour ? {
|
||||||
|
color: `#${line.colour}`
|
||||||
|
} : {}),
|
||||||
|
...(line.width ? {
|
||||||
|
width: `${line.width}`
|
||||||
|
} : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exportLineToTrackGpx(line: LineForExport, type: Type | undefined, trackPoints: AsyncIterable<TrackPoint>): ReadableStream<string> {
|
||||||
|
return asyncIteratorToStream((async function*() {
|
||||||
|
yield (
|
||||||
|
`${gpxHeader}\n` +
|
||||||
|
`\t${getLineMetadataGpx(line).replaceAll("\n", "\n\t")}\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const chunk of indentStream(getLineTrackGpx(line, type, trackPoints), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield gpxFooter;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportLineToRouteGpx(line: LineForExport, type: Type | undefined): ReadableStream<string> {
|
||||||
|
return asyncIteratorToStream((async function*() {
|
||||||
|
yield (
|
||||||
|
`${gpxHeader}\n` +
|
||||||
|
`\t${getLineMetadataGpx(line).replaceAll("\n", "\n\t")}\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const chunk of indentStream(getLineRouteGpx(line, type), { indent: "\t", indentFirst: true, addNewline: true })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield gpxFooter;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Router, type RequestHandler } from "express";
|
||||||
import { static as expressStatic } from "express";
|
import { static as expressStatic } from "express";
|
||||||
import { normalizePadName, type InjectedConfig, quoteHtml } from "facilmap-utils";
|
import { normalizePadName, type InjectedConfig, quoteHtml } from "facilmap-utils";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import { asyncIteratorToArray, jsonStream, streamPromiseToStream, streamReplace } from "./utils/streams";
|
import { streamPromiseToStream, streamReplace } from "./utils/streams";
|
||||||
import { ReadableStream } from "stream/web";
|
import { ReadableStream } from "stream/web";
|
||||||
import { generateRandomId } from "./utils/utils";
|
import { generateRandomId } from "./utils/utils";
|
||||||
import type { TableParams } from "./export/table";
|
import type { TableParams } from "./export/table";
|
||||||
|
@ -143,10 +143,7 @@ export async function getStaticFrontendMiddleware(): Promise<RequestHandler> {
|
||||||
|
|
||||||
export async function getPwaManifest(): Promise<string> {
|
export async function getPwaManifest(): Promise<string> {
|
||||||
const template = await readFile(paths.pwaManifest).then((t) => t.toString());
|
const template = await readFile(paths.pwaManifest).then((t) => t.toString());
|
||||||
const chunks = await asyncIteratorToArray(jsonStream(JSON.parse(template), {
|
return template.replaceAll("%APP_NAME%", config.appName);
|
||||||
APP_NAME: config.appName
|
|
||||||
}));
|
|
||||||
return chunks.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOpensearchXml(baseUrl: string): Promise<string> {
|
export async function getOpensearchXml(baseUrl: string): Promise<string> {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
declare module "*?raw" {
|
declare module "*?raw" {
|
||||||
declare const content: string;
|
declare const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "zip-stream";
|
|
@ -1,8 +1,8 @@
|
||||||
import { promiseProps, type PromiseMap } from "../utils/utils.js";
|
import { promiseProps, type PromiseMap } from "../utils/utils.js";
|
||||||
import { asyncIteratorToArray } from "../utils/streams.js";
|
import { asyncIteratorToArray, streamToString } from "../utils/streams.js";
|
||||||
import { isInBbox } from "../utils/geo.js";
|
import { isInBbox } from "../utils/geo.js";
|
||||||
import { Socket, type Socket as SocketIO } from "socket.io";
|
import { Socket, type Socket as SocketIO } from "socket.io";
|
||||||
import { exportLineToGpx } from "../export/gpx.js";
|
import { exportLineToRouteGpx, exportLineToTrackGpx } from "../export/gpx.js";
|
||||||
import { find } from "../search.js";
|
import { find } from "../search.js";
|
||||||
import { geoipLookup } from "../geoip.js";
|
import { geoipLookup } from "../geoip.js";
|
||||||
import { cloneDeep, isEqual, omit } from "lodash-es";
|
import { cloneDeep, isEqual, omit } from "lodash-es";
|
||||||
|
@ -254,7 +254,7 @@ export class SocketConnectionV2 extends SocketConnection {
|
||||||
if (data.mode != "track") {
|
if (data.mode != "track") {
|
||||||
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
|
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
|
||||||
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
|
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
|
||||||
fromRoute = { ...route, trackPoints: await this.database.routes.getAllRoutePoints(route.id) };
|
fromRoute = { ...route, trackPoints: await asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ export class SocketConnectionV2 extends SocketConnection {
|
||||||
if (data.mode != "track") {
|
if (data.mode != "track") {
|
||||||
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
|
for (const route of [...(this.route ? [this.route] : []), ...Object.values(this.routes)]) {
|
||||||
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
|
if(isEqual(route.routePoints, data.routePoints) && data.mode == route.mode) {
|
||||||
fromRoute = { ...route, trackPoints: await this.database.routes.getAllRoutePoints(route.id) };
|
fromRoute = { ...route, trackPoints: await asyncIteratorToArray(this.database.routes.getAllRoutePoints(route.id)) };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,19 +300,16 @@ export class SocketConnectionV2 extends SocketConnection {
|
||||||
const lineP = this.database.lines.getLine(this.padId, data.id);
|
const lineP = this.database.lines.getLine(this.padId, data.id);
|
||||||
lineP.catch(() => null); // Avoid unhandled promise error (https://stackoverflow.com/a/59062117/242365)
|
lineP.catch(() => null); // Avoid unhandled promise error (https://stackoverflow.com/a/59062117/242365)
|
||||||
|
|
||||||
const [line, trackPoints, type] = await Promise.all([
|
const [line, type] = await Promise.all([
|
||||||
lineP,
|
lineP,
|
||||||
this.database.lines.getAllLinePoints(data.id),
|
|
||||||
lineP.then((line) => this.database.types.getType(this.padId as string, line.typeId))
|
lineP.then((line) => this.database.types.getType(this.padId as string, line.typeId))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const lineWithTrackPoints = { ...line, trackPoints };
|
|
||||||
|
|
||||||
switch(data.format) {
|
switch(data.format) {
|
||||||
case "gpx-trk":
|
case "gpx-trk":
|
||||||
return exportLineToGpx(lineWithTrackPoints, type, true);
|
return await streamToString(exportLineToTrackGpx(line, type, this.database.lines.getAllLinePoints(line.id)));
|
||||||
case "gpx-rte":
|
case "gpx-rte":
|
||||||
return exportLineToGpx(lineWithTrackPoints, type, false);
|
return await streamToString(exportLineToRouteGpx(line, type));
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown format.");
|
throw new Error("Unknown format.");
|
||||||
}
|
}
|
||||||
|
@ -536,15 +533,13 @@ export class SocketConnectionV2 extends SocketConnection {
|
||||||
throw new Error("Route not available.");
|
throw new Error("Route not available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackPoints = await this.database.routes.getAllRoutePoints(route.id);
|
const routeInfo = { ...route, name: "FacilMap route", data: {} };
|
||||||
|
|
||||||
const routeInfo = { ...this.route, trackPoints };
|
|
||||||
|
|
||||||
switch(data.format) {
|
switch(data.format) {
|
||||||
case "gpx-trk":
|
case "gpx-trk":
|
||||||
return await exportLineToGpx(routeInfo, undefined, true);
|
return await streamToString(exportLineToTrackGpx(routeInfo, undefined, this.database.routes.getAllRoutePoints(route.id)));
|
||||||
case "gpx-rte":
|
case "gpx-rte":
|
||||||
return await exportLineToGpx(routeInfo, undefined, false);
|
return await streamToString(exportLineToRouteGpx(routeInfo, undefined));
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown format.");
|
throw new Error("Unknown format.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { arrayToAsyncIterator, asyncIteratorToArray, asyncIteratorToStream, jsonStream, streamPromiseToStream, streamReplace } from "../streams.js";
|
import { arrayToAsyncIterator, asyncIteratorToArray, asyncIteratorToStream, jsonStreamArray, jsonStreamRecord, streamPromiseToStream, streamReplace } from "../streams.js";
|
||||||
import { ReadableStream } from "stream/web";
|
import { ReadableStream } from "stream/web";
|
||||||
|
|
||||||
describe("streamPromiseToStream", () => {
|
describe("streamPromiseToStream", () => {
|
||||||
|
@ -48,64 +48,60 @@ describe("streamPromiseToStream", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('jsonStream', async () => {
|
test('jsonStream', async () => {
|
||||||
const template = {
|
const stream = jsonStreamRecord({
|
||||||
test1: "%var1%",
|
test1: { test: 'object' },
|
||||||
test2: {
|
test2: jsonStreamRecord({
|
||||||
one: "one",
|
one: "one",
|
||||||
two: "%var2%",
|
two: jsonStreamArray([ { object: 'one' }, { object: 'two' } ]),
|
||||||
three: "three"
|
three: "three"
|
||||||
},
|
}),
|
||||||
test3: "%var3%",
|
test3: jsonStreamRecord(arrayToAsyncIterator(Object.entries({
|
||||||
test4: "%var4%",
|
one: "one",
|
||||||
test5: "%var5%",
|
two: jsonStreamArray(arrayToAsyncIterator([ { object: 'one' }, { object: 'two' } ])),
|
||||||
test6: "%var6%",
|
three: "three"
|
||||||
test7: "%var7%",
|
}))),
|
||||||
test8: "bla"
|
test4: jsonStreamArray([
|
||||||
};
|
"one",
|
||||||
|
jsonStreamRecord({ object1: "one", object2: "two" }),
|
||||||
const stream = jsonStream(template, {
|
"three"
|
||||||
var1: { test: 'object' },
|
]),
|
||||||
var2: arrayToAsyncIterator([ { object: 'one' }, { object: 'two' } ]),
|
test5: jsonStreamArray(arrayToAsyncIterator([
|
||||||
var3: () => arrayToAsyncIterator([ { object: 'one' }, { object: 'two' } ]),
|
"one",
|
||||||
var4: 'asdf',
|
jsonStreamRecord(arrayToAsyncIterator(Object.entries({ object1: "one", object2: "two" }))),
|
||||||
var5: () => 'bla',
|
"three"
|
||||||
var6: Promise.resolve('promise'),
|
])),
|
||||||
var7: () => Promise.resolve('async')
|
test6: Promise.resolve("promise"),
|
||||||
|
test7: "string"
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = (await asyncIteratorToArray(stream as any)).join("");
|
const result = (await asyncIteratorToArray(stream as any)).join("");
|
||||||
expect(result).toBe(
|
expect(result).toBe(JSON.stringify({
|
||||||
`{
|
test1: {
|
||||||
"test1": {
|
test: "object"
|
||||||
"test": "object"
|
|
||||||
},
|
|
||||||
"test2": {
|
|
||||||
"one": "one",
|
|
||||||
"two": [
|
|
||||||
{
|
|
||||||
"object": "one"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"object": "two"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"three": "three"
|
|
||||||
},
|
|
||||||
"test3": [
|
|
||||||
{
|
|
||||||
"object": "one"
|
|
||||||
},
|
},
|
||||||
{
|
test2: {
|
||||||
"object": "two"
|
one: "one",
|
||||||
}
|
two: [{ object: "one" }, { object: "two" }],
|
||||||
],
|
three: "three"
|
||||||
"test4": "asdf",
|
},
|
||||||
"test5": "bla",
|
test3: {
|
||||||
"test6": "promise",
|
one: "one",
|
||||||
"test7": "async",
|
two: [{ object: "one" }, { object: "two" }],
|
||||||
"test8": "bla"
|
three: "three"
|
||||||
}`
|
},
|
||||||
);
|
test4: [
|
||||||
|
"one",
|
||||||
|
{ object1: "one", object2: "two" },
|
||||||
|
"three"
|
||||||
|
],
|
||||||
|
test5: [
|
||||||
|
"one",
|
||||||
|
{ object1: "one", object2: "two" },
|
||||||
|
"three"
|
||||||
|
],
|
||||||
|
test6: "promise",
|
||||||
|
test7: "string"
|
||||||
|
}, undefined, "\t"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("streamReplace", async () => {
|
test("streamReplace", async () => {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { ReadableStream, TransformStream } from "stream/web";
|
import { Readable } from "stream";
|
||||||
|
import { type QueuingStrategy, ReadableStream, TransformStream } from "stream/web";
|
||||||
|
import Packer from "zip-stream";
|
||||||
|
|
||||||
export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promise<Array<T>> {
|
export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promise<Array<T>> {
|
||||||
const result: T[] = [];
|
const result: T[] = [];
|
||||||
|
@ -8,23 +10,48 @@ export async function asyncIteratorToArray<T>(iterator: AsyncIterable<T>): Promi
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* arrayToAsyncIterator<T>(array: T[]): AsyncGenerator<T, void, void> {
|
export async function* arrayToAsyncIterator<T>(array: T[]): AsyncIterable<T> {
|
||||||
for (const it of array) {
|
for (const it of array) {
|
||||||
yield it;
|
yield it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asyncIteratorToStream<T>(iterator: AsyncGenerator<T, void, void>): ReadableStream<T> {
|
export function asyncIteratorToStream<T>(iterator: AsyncIterable<T>, strategy?: QueuingStrategy<T>): ReadableStream<T> {
|
||||||
|
const it = iterator[Symbol.asyncIterator]();
|
||||||
return new ReadableStream<T>({
|
return new ReadableStream<T>({
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
const { value, done } = await iterator.next();
|
const { value, done } = await it.next();
|
||||||
if (done) {
|
if (done) {
|
||||||
controller.close();
|
controller.close();
|
||||||
} else {
|
} else {
|
||||||
controller.enqueue(value);
|
controller.enqueue(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
}, strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAsyncIterator<T, O>(iterator: AsyncIterable<T>, mapper: (it: T) => O): AsyncIterable<O> {
|
||||||
|
return flatMapAsyncIterator(iterator, (it) => [mapper(it)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterAsyncIterator<T>(iterator: AsyncIterable<T>, filter: (it: T) => boolean): AsyncIterable<T> {
|
||||||
|
return flatMapAsyncIterator(iterator, (it) => filter(it) ? [it] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* flatMapAsyncIterator<T, O>(iterator: AsyncIterable<T>, mapper: (it: T) => O[]): AsyncIterable<O> {
|
||||||
|
for await (const it of iterator) {
|
||||||
|
for (const o of mapper(it)) {
|
||||||
|
yield o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function* concatAsyncIterators<T>(...iterators: Array<AsyncIterable<T>>): AsyncIterable<T> {
|
||||||
|
for (const iterator of iterators) {
|
||||||
|
for await (const it of iterator) {
|
||||||
|
yield it;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function streamPromiseToStream<T>(streamPromise: Promise<ReadableStream<T>>): ReadableStream<T> {
|
export function streamPromiseToStream<T>(streamPromise: Promise<ReadableStream<T>>): ReadableStream<T> {
|
||||||
|
@ -57,40 +84,56 @@ export function flatMapStream<T, O>(stream: ReadableStream<T>, mapper: (it: T) =
|
||||||
return transform.readable;
|
return transform.readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jsonStream(template: any, data: Record<string, AsyncGenerator<any, any, void> | Promise<any> | any | (() => AsyncGenerator<any, any, void> | Promise<any> | any)>): ReadableStream<string> {
|
type Stringifiable = boolean | number | string | undefined | null | Array<Stringifiable> | /* Should be Record<any, Stringifiable> here, but that does not work */ object;
|
||||||
return asyncIteratorToStream((async function*() {
|
const jsonStreamSymbol = Symbol("jsonStream");
|
||||||
let lastIndent = '';
|
export type JsonStream = ReadableStream<string> & { [jsonStreamSymbol]: true };
|
||||||
|
const isJsonStream = (obj: unknown): obj is JsonStream => !!obj && typeof obj === "object" && jsonStreamSymbol in obj && !!obj[jsonStreamSymbol];
|
||||||
|
|
||||||
const parts = JSON.stringify(template, undefined, "\t").split(/"%([a-zA-Z0-9-_]+)%"/);
|
export function jsonStreamArray(iterator: Iterable<Stringifiable | JsonStream> | AsyncIterable<Stringifiable | JsonStream>): JsonStream {
|
||||||
for (let i = 0; i < parts.length; i++) {
|
return Object.assign(asyncIteratorToStream((async function*() {
|
||||||
const part = parts[i];
|
let first = true;
|
||||||
|
yield "[";
|
||||||
|
for await (const value of iterator) {
|
||||||
|
const prefix = `${first ? "" : ","}\n\t`;
|
||||||
|
first = false;
|
||||||
|
|
||||||
if (i % 2 == 0) {
|
if (isJsonStream(value)) {
|
||||||
const lastLineBreak = part.lastIndexOf('\n');
|
yield prefix;
|
||||||
if (lastLineBreak != -1)
|
for await (const chunk of streamReplace(value, { "\n": `\n\t` })) {
|
||||||
lastIndent = part.slice(lastLineBreak + 1).match(/^(\t*)/)![1];
|
yield chunk;
|
||||||
|
|
||||||
yield part;
|
|
||||||
} else {
|
|
||||||
const value = await (typeof data[part] === 'function' ? data[part]() : data[part]);
|
|
||||||
|
|
||||||
if (typeof value === 'object' && value && Symbol.asyncIterator in value) {
|
|
||||||
let first = true;
|
|
||||||
const indent = lastIndent + "\t";
|
|
||||||
yield '[\n';
|
|
||||||
for await (const obj of value) {
|
|
||||||
const prefix = first ? '' : ',\n';
|
|
||||||
first = false;
|
|
||||||
yield prefix + JSON.stringify(obj, undefined, "\t").replace(/^/gm, indent);
|
|
||||||
}
|
|
||||||
yield '\n' + lastIndent + ']';
|
|
||||||
} else {
|
|
||||||
const indent = lastIndent;
|
|
||||||
yield JSON.stringify(value, undefined, "\t").replace(/\n/g, '\n' + indent);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
yield `${prefix}${JSON.stringify(value, undefined, "\t").replaceAll("\n", "\n\t")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})());
|
yield `${first ? "" : "\n"}]`;
|
||||||
|
})()), {
|
||||||
|
[jsonStreamSymbol]: true as const
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonStreamRecord(iterator: Record<string | number, Stringifiable | Promise<Stringifiable> | JsonStream> | AsyncIterable<[key: string | number, value: Stringifiable | Promise<Stringifiable> | JsonStream]>): JsonStream {
|
||||||
|
return Object.assign(asyncIteratorToStream((async function*() {
|
||||||
|
let first = true;
|
||||||
|
yield "{";
|
||||||
|
const normalizedIterator = Symbol.asyncIterator in iterator ? iterator : Object.entries(iterator);
|
||||||
|
for await (const [key, value] of normalizedIterator) {
|
||||||
|
const prefix = `${first ? "" : ","}\n\t${JSON.stringify(key)}: `;
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
if (isJsonStream(value)) {
|
||||||
|
yield prefix;
|
||||||
|
for await (const chunk of streamReplace(value, { "\n": `\n\t` })) {
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yield `${prefix}${JSON.stringify(await value, undefined, "\t").replaceAll("\n", "\n\t")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield `${first ? "" : "\n"}}`;
|
||||||
|
})()), {
|
||||||
|
[jsonStreamSymbol]: true as const
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringToStream(string: string): ReadableStream<string> {
|
export function stringToStream(string: string): ReadableStream<string> {
|
||||||
|
@ -99,6 +142,10 @@ export function stringToStream(string: string): ReadableStream<string> {
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function streamToString(stream: ReadableStream<string>): Promise<string> {
|
||||||
|
return (await asyncIteratorToArray(stream)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
export function streamReplace(stream: ReadableStream<string> | string, replace: Record<string, ReadableStream<string> | string>): ReadableStream<string> {
|
export function streamReplace(stream: ReadableStream<string> | string, replace: Record<string, ReadableStream<string> | string>): ReadableStream<string> {
|
||||||
const normalizedStream = typeof stream === "string" ? stringToStream(stream) : stream;
|
const normalizedStream = typeof stream === "string" ? stringToStream(stream) : stream;
|
||||||
|
|
||||||
|
@ -159,3 +206,47 @@ export function streamReplace(stream: ReadableStream<string> | string, replace:
|
||||||
}
|
}
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ZipEncodeStreamItem = { filename: string, data: ReadableStream<string> | null };
|
||||||
|
|
||||||
|
export function getZipEncodeStream(): TransformStream<ZipEncodeStreamItem, Uint8Array> {
|
||||||
|
const archive = new Packer();
|
||||||
|
|
||||||
|
const writable = new WritableStream<ZipEncodeStreamItem>({
|
||||||
|
async write(chunk) {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
archive.entry(chunk.data && Readable.fromWeb(chunk.data), { name: chunk.filename }, (err: any) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
archive.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
readable: Readable.toWeb(archive),
|
||||||
|
writable
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function indentStream(stream: ReadableStream<string>, { indent, indentFirst, addNewline }: { indent: string, indentFirst: boolean; addNewline: boolean }): ReadableStream<string> {
|
||||||
|
return asyncIteratorToStream((async function*() {
|
||||||
|
let first = true;
|
||||||
|
for await (const chunk of streamReplace(stream, { "\n": `\n${indent}` })) {
|
||||||
|
if (chunk.length > 0) {
|
||||||
|
yield `${first && indentFirst ? indent : ""}${chunk}`;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (addNewline && !first) {
|
||||||
|
yield "\n";
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
}
|
|
@ -38,4 +38,4 @@ export async function fileExists(filename: string): Promise<boolean> {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,11 +5,11 @@ import { stringifiedIdValidator, type PadId } from "facilmap-types";
|
||||||
import { createSingleTable, createTable } from "./export/table.js";
|
import { createSingleTable, createTable } from "./export/table.js";
|
||||||
import Database from "./database/database";
|
import Database from "./database/database";
|
||||||
import { exportGeoJson } from "./export/geojson.js";
|
import { exportGeoJson } from "./export/geojson.js";
|
||||||
import { exportGpx } from "./export/gpx.js";
|
import { exportGpx, exportGpxZip } from "./export/gpx.js";
|
||||||
import domainMiddleware from "express-domain-middleware";
|
import domainMiddleware from "express-domain-middleware";
|
||||||
import { Readable, Writable } from "stream";
|
import { Readable, Writable } from "stream";
|
||||||
import { getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
|
import { getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
|
||||||
import { normalizePadName } from "facilmap-utils";
|
import { getSafeFilename, normalizePadName } from "facilmap-utils";
|
||||||
import { paths } from "facilmap-frontend/build.js";
|
import { paths } from "facilmap-frontend/build.js";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import { exportCsv } from "./export/csv.js";
|
import { exportCsv } from "./export/csv.js";
|
||||||
|
@ -104,10 +104,26 @@ export async function initWebserver(database: Database, port: number, host?: str
|
||||||
throw new Error(`Map with ID ${req.params.padId} could not be found.`);
|
throw new Error(`Map with ID ${req.params.padId} could not be found.`);
|
||||||
|
|
||||||
res.set("Content-type", "application/gpx+xml");
|
res.set("Content-type", "application/gpx+xml");
|
||||||
res.attachment(padData.name.replace(/[\\/:*?"<>|]+/g, '_') + ".gpx");
|
res.attachment(`${getSafeFilename(normalizePadName(padData.name))}.gpx`);
|
||||||
exportGpx(database, padData ? padData.id : req.params.padId, query.useTracks == "1", query.filter).pipeTo(Writable.toWeb(res));
|
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>) => {
|
||||||
|
const query = z.object({
|
||||||
|
useTracks: z.enum(["0", "1"]).default("0"),
|
||||||
|
filter: z.string().optional()
|
||||||
|
}).parse(req.query);
|
||||||
|
|
||||||
|
const padData = await database.pads.getPadDataByAnyId(req.params.padId);
|
||||||
|
|
||||||
|
if(!padData)
|
||||||
|
throw new Error(`Map with ID ${req.params.padId} could not be found.`);
|
||||||
|
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/:padId/table", async (req: Request<{ padId: string }>, res: Response<string>) => {
|
app.get("/:padId/table", async (req: Request<{ padId: string }>, res: Response<string>) => {
|
||||||
const query = z.object({
|
const query = z.object({
|
||||||
filter: z.string().optional(),
|
filter: z.string().optional(),
|
||||||
|
|
|
@ -3,12 +3,16 @@ import { cloneDeep, isEqual } from "lodash-es";
|
||||||
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
const LENGTH = 12;
|
const LENGTH = 12;
|
||||||
|
|
||||||
export function quoteJavaScript(str: any): string {
|
export function quoteHtml(str: string | number): string {
|
||||||
return "'" + `${str}`.replace(/['\\]/g, '\\$1').replace(/\n/g, "\\n") + "'";
|
return `${str}`
|
||||||
}
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
export function quoteHtml(str: any): string {
|
.replace(/>/g, ">")
|
||||||
return `${str}`.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/\n/g, " ")
|
||||||
|
.replace(/\r/g, " ")
|
||||||
|
.replace(/\t/g, "	");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function quoteRegExp(str: string): string {
|
export function quoteRegExp(str: string): string {
|
||||||
|
@ -159,3 +163,7 @@ export function mergeObject<T extends Record<keyof any, any>>(oldObject: T | und
|
||||||
targetObject[i] = cloneDeep(newObject[i]);
|
targetObject[i] = cloneDeep(newObject[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSafeFilename(fname: string): string {
|
||||||
|
return fname.replace(/[\\/:*?"<>|]+/g, '_');
|
||||||
|
}
|
140
yarn.lock
140
yarn.lock
|
@ -1686,6 +1686,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"abort-controller@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "abort-controller@npm:3.0.0"
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: ^5.0.0
|
||||||
|
checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.7":
|
"accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.7":
|
||||||
version: 1.3.8
|
version: 1.3.8
|
||||||
resolution: "accepts@npm:1.3.8"
|
resolution: "accepts@npm:1.3.8"
|
||||||
|
@ -1816,6 +1825,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"archiver-utils@npm:^5.0.0":
|
||||||
|
version: 5.0.1
|
||||||
|
resolution: "archiver-utils@npm:5.0.1"
|
||||||
|
dependencies:
|
||||||
|
glob: ^10.0.0
|
||||||
|
graceful-fs: ^4.2.0
|
||||||
|
lazystream: ^1.0.0
|
||||||
|
lodash: ^4.17.15
|
||||||
|
normalize-path: ^3.0.0
|
||||||
|
readable-stream: ^3.6.0
|
||||||
|
checksum: a6e907cea41486ff95fdbe1b267890df96d51e2712b6604cb9b0c5f7348aea475091f41e0e6fced3a893490deddfd8d0704f66c8aa470394cff3618bbec6d4cc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"argparse@npm:^2.0.1":
|
"argparse@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "argparse@npm:2.0.1"
|
resolution: "argparse@npm:2.0.1"
|
||||||
|
@ -1979,6 +2002,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"base64-js@npm:^1.3.1":
|
||||||
|
version: 1.5.1
|
||||||
|
resolution: "base64-js@npm:1.5.1"
|
||||||
|
checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"base64id@npm:2.0.0, base64id@npm:~2.0.0":
|
"base64id@npm:2.0.0, base64id@npm:~2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "base64id@npm:2.0.0"
|
resolution: "base64id@npm:2.0.0"
|
||||||
|
@ -2109,6 +2139,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"buffer@npm:^6.0.3":
|
||||||
|
version: 6.0.3
|
||||||
|
resolution: "buffer@npm:6.0.3"
|
||||||
|
dependencies:
|
||||||
|
base64-js: ^1.3.1
|
||||||
|
ieee754: ^1.2.1
|
||||||
|
checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"buffers@npm:~0.1.1":
|
"buffers@npm:~0.1.1":
|
||||||
version: 0.1.1
|
version: 0.1.1
|
||||||
resolution: "buffers@npm:0.1.1"
|
resolution: "buffers@npm:0.1.1"
|
||||||
|
@ -2353,6 +2393,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"compress-commons@npm:^6.0.0":
|
||||||
|
version: 6.0.1
|
||||||
|
resolution: "compress-commons@npm:6.0.1"
|
||||||
|
dependencies:
|
||||||
|
crc-32: ^1.2.0
|
||||||
|
crc32-stream: ^6.0.0
|
||||||
|
normalize-path: ^3.0.0
|
||||||
|
readable-stream: ^4.0.0
|
||||||
|
checksum: a2ecd7c536cb4e1f029863464202e584ef4ea1b1f6a22da526efaf744ba6b59d29e860a637c046118cc38559f0b202f1d4dfa5c445a8c0f4bbf54eabcbd74bbf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"compressible@npm:~2.0.16":
|
"compressible@npm:~2.0.16":
|
||||||
version: 2.0.18
|
version: 2.0.18
|
||||||
resolution: "compressible@npm:2.0.18"
|
resolution: "compressible@npm:2.0.18"
|
||||||
|
@ -2517,6 +2569,25 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"crc-32@npm:^1.2.0":
|
||||||
|
version: 1.2.2
|
||||||
|
resolution: "crc-32@npm:1.2.2"
|
||||||
|
bin:
|
||||||
|
crc32: bin/crc32.njs
|
||||||
|
checksum: ad2d0ad0cbd465b75dcaeeff0600f8195b686816ab5f3ba4c6e052a07f728c3e70df2e3ca9fd3d4484dc4ba70586e161ca5a2334ec8bf5a41bf022a6103ff243
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"crc32-stream@npm:^6.0.0":
|
||||||
|
version: 6.0.0
|
||||||
|
resolution: "crc32-stream@npm:6.0.0"
|
||||||
|
dependencies:
|
||||||
|
crc-32: ^1.2.0
|
||||||
|
readable-stream: ^4.0.0
|
||||||
|
checksum: e6edc2f81bc387daef6d18b2ac18c2ffcb01b554d3b5c7d8d29b177505aafffba574658fdd23922767e8dab1183d1962026c98c17e17fb272794c33293ef607c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
|
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
resolution: "cross-spawn@npm:7.0.3"
|
resolution: "cross-spawn@npm:7.0.3"
|
||||||
|
@ -3467,6 +3538,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"event-target-shim@npm:^5.0.0":
|
||||||
|
version: 5.0.1
|
||||||
|
resolution: "event-target-shim@npm:5.0.1"
|
||||||
|
checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"events@npm:^3.3.0":
|
||||||
|
version: 3.3.0
|
||||||
|
resolution: "events@npm:3.3.0"
|
||||||
|
checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"execa@npm:^8.0.1":
|
"execa@npm:^8.0.1":
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
resolution: "execa@npm:8.0.1"
|
resolution: "execa@npm:8.0.1"
|
||||||
|
@ -3734,6 +3819,7 @@ __metadata:
|
||||||
vite-plugin-dts: ^3.7.3
|
vite-plugin-dts: ^3.7.3
|
||||||
vite-tsconfig-paths: ^4.3.1
|
vite-tsconfig-paths: ^4.3.1
|
||||||
vitest: ^1.3.1
|
vitest: ^1.3.1
|
||||||
|
zip-stream: ^6.0.0
|
||||||
zod: ^3.22.4
|
zod: ^3.22.4
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
@ -4174,7 +4260,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
|
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
|
||||||
version: 10.3.10
|
version: 10.3.10
|
||||||
resolution: "glob@npm:10.3.10"
|
resolution: "glob@npm:10.3.10"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4264,7 +4350,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
|
||||||
version: 4.2.11
|
version: 4.2.11
|
||||||
resolution: "graceful-fs@npm:4.2.11"
|
resolution: "graceful-fs@npm:4.2.11"
|
||||||
checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7
|
checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7
|
||||||
|
@ -4465,7 +4551,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"ieee754@npm:^1.1.12":
|
"ieee754@npm:^1.1.12, ieee754@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "ieee754@npm:1.2.1"
|
resolution: "ieee754@npm:1.2.1"
|
||||||
checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e
|
checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e
|
||||||
|
@ -5017,6 +5103,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lazystream@npm:^1.0.0":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "lazystream@npm:1.0.1"
|
||||||
|
dependencies:
|
||||||
|
readable-stream: ^2.0.5
|
||||||
|
checksum: 822c54c6b87701a6491c70d4fabc4cafcf0f87d6b656af168ee7bb3c45de9128a801cb612e6eeeefc64d298a7524a698dd49b13b0121ae50c2ae305f0dcc5310
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"leaflet-auto-graticule@npm:^2.0.0":
|
"leaflet-auto-graticule@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "leaflet-auto-graticule@npm:2.0.0"
|
resolution: "leaflet-auto-graticule@npm:2.0.0"
|
||||||
|
@ -5199,7 +5294,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lodash@npm:^4.17.21, lodash@npm:~4.17.15":
|
"lodash@npm:^4.17.15, lodash@npm:^4.17.21, lodash@npm:~4.17.15":
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
resolution: "lodash@npm:4.17.21"
|
resolution: "lodash@npm:4.17.21"
|
||||||
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
|
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
|
||||||
|
@ -6414,6 +6509,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"process@npm:^0.11.10":
|
||||||
|
version: 0.11.10
|
||||||
|
resolution: "process@npm:0.11.10"
|
||||||
|
checksum: bfcce49814f7d172a6e6a14d5fa3ac92cc3d0c3b9feb1279774708a719e19acd673995226351a082a9ae99978254e320ccda4240ddc474ba31a76c79491ca7c3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"promise-retry@npm:^2.0.1":
|
"promise-retry@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "promise-retry@npm:2.0.1"
|
resolution: "promise-retry@npm:2.0.1"
|
||||||
|
@ -6523,7 +6625,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"readable-stream@npm:^2.0.2, readable-stream@npm:~2.3.6":
|
"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:~2.3.6":
|
||||||
version: 2.3.8
|
version: 2.3.8
|
||||||
resolution: "readable-stream@npm:2.3.8"
|
resolution: "readable-stream@npm:2.3.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6538,7 +6640,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"readable-stream@npm:^3.0.2":
|
"readable-stream@npm:^3.0.2, readable-stream@npm:^3.6.0":
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
resolution: "readable-stream@npm:3.6.2"
|
resolution: "readable-stream@npm:3.6.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6549,6 +6651,19 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"readable-stream@npm:^4.0.0":
|
||||||
|
version: 4.5.2
|
||||||
|
resolution: "readable-stream@npm:4.5.2"
|
||||||
|
dependencies:
|
||||||
|
abort-controller: ^3.0.0
|
||||||
|
buffer: ^6.0.3
|
||||||
|
events: ^3.3.0
|
||||||
|
process: ^0.11.10
|
||||||
|
string_decoder: ^1.3.0
|
||||||
|
checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"readdirp@npm:~3.6.0":
|
"readdirp@npm:~3.6.0":
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
resolution: "readdirp@npm:3.6.0"
|
resolution: "readdirp@npm:3.6.0"
|
||||||
|
@ -7303,7 +7418,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"string_decoder@npm:^1.1.1":
|
"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "string_decoder@npm:1.3.0"
|
resolution: "string_decoder@npm:1.3.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8427,6 +8542,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"zip-stream@npm:^6.0.0":
|
||||||
|
version: 6.0.0
|
||||||
|
resolution: "zip-stream@npm:6.0.0"
|
||||||
|
dependencies:
|
||||||
|
archiver-utils: ^5.0.0
|
||||||
|
compress-commons: ^6.0.0
|
||||||
|
readable-stream: ^4.0.0
|
||||||
|
checksum: 114565901579dc023b41b2d3d3ce2721ad734032295d50977dd58899cb6d1922abb6336ee388f6f90b74e0a384cb9c590f80cf9f1ac9ede865bd212ba96f6070
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"zod@npm:^3.22.4":
|
"zod@npm:^3.22.4":
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
resolution: "zod@npm:3.22.4"
|
resolution: "zod@npm:3.22.4"
|
||||||
|
|
Ładowanie…
Reference in New Issue