Porównaj commity

...

4 Commity

Autor SHA1 Wiadomość Data
Candid Dauth 9aa954c3c4 Update documentation to reflect new features 2024-03-29 01:44:57 +01:00
Candid Dauth 29ef631fc4 Remove id from update object types 2024-03-29 01:44:39 +01:00
Candid Dauth 326332eedf Enable high accuracy for locate control 2024-03-29 00:20:34 +01:00
Candid Dauth c43878c991 Fix colours for locate control 2024-03-29 00:19:17 +01:00
20 zmienionych plików z 93 dodań i 66 usunięć

Wyświetl plik

@ -440,7 +440,7 @@ export default class Client {
return marker;
}
async editMarker(data: Marker<CRU.UPDATE>): Promise<Marker> {
async editMarker(data: Marker<CRU.UPDATE> & { id: ID }): Promise<Marker> {
return await this._emit("editMarker", data);
}
@ -456,7 +456,7 @@ export default class Client {
return await this._emit("addLine", data);
}
async editLine(data: Line<CRU.UPDATE>): Promise<Line> {
async editLine(data: Line<CRU.UPDATE> & { id: ID }): Promise<Line> {
return await this._emit("editLine", data);
}
@ -542,7 +542,7 @@ export default class Client {
return await this._emit("addType", data);
}
async editType(data: Type<CRU.UPDATE>): Promise<Type> {
async editType(data: Type<CRU.UPDATE> & { id: ID }): Promise<Type> {
return await this._emit("editType", data);
}
@ -554,7 +554,7 @@ export default class Client {
return await this._emit("addView", data);
}
async editView(data: View<CRU.UPDATE>): Promise<View> {
async editView(data: View<CRU.UPDATE> & { id: ID }): Promise<View> {
return await this._emit("editView", data);
}

Wyświetl plik

@ -2,26 +2,39 @@
A map can be exported as a file, in order to use it in another application or to create a backup.
To export a map, in the [toolbox](../ui/#toolbox), click “Tools” and then one of the “Export” options. Note that when a [filter](../filter/) is active, only the objects that match the filter are exported.
To export a map, in the [toolbox](../ui/#toolbox), click “Tools” and then “Export”. This opens a dialog where you can configure in what format to export the map.
The exports are available under their own URL. In the context menu (right click) of the export links, you can copy the URL to use it elsewhere.
## Formats
## GeoJSON
### GPX
When exporting a map as GeoJSON, all markers and lines (or, if a [filter](../filter/) is active, those that match the filter) including their data fields, all [saved views](../views/), and all [types](../types/) that represent the exported markers/lines are exported. This means that if no filter is active, this is suitable to make a backup of the whole map (apart from the [map settings](../map-settings/) and the [edit history](../history/)). Such a file can also be [imported](../import/) again into a map to restore the backup.
GPX is a format that is understood by many geographic apps and navigation devices. Exporting a map as GPX will export all markers and lines on the map.
## GPX
When exporting to GPX, one of three “Route type” options needs to be selected:
* **Track points:** Lines that are calculated routes will be exported as GPX tracks. This means that the whole course of the route is saved in the file.
* **Track points, one file per line (ZIP file):** Like “Track points”, but rather than creating one GPX file containing all markers/lines of the map, a ZIP file will be generated that contains one GPX file with all markers and a folder with one GPX file per line. This is useful for importing the file into OsmAnd, which only supports one track per file.
* **Route points:** Lines that are calculated routes will be exported as GPX routes. This means that only the route destinations are saved in the file, and the app or device that opens the file is responsible for calculating the best route.
GPX is a format that is understood by many geographic apps and navigation devices. Exporting a map as GPX will export all markers and lines on the map (or, if a [filter](../filter/) is active, those that match the filter). There are two options:
* **Export as GPX (tracks):** Lines that are calculated routes will be exported as GPX tracks. This means that the whole course of the route is saved in the file.
* **Export as GPX (routes):** Lines that are calculated routes will be exported as GPX routes. This means that only the route destinations are saved in the file, and the app or device that opens the file is responsible for calculating the best route.
The marker/line description and any [custom fields](../types/) will be saved in the description of the GPX objects. The marker/line styles are not exported, with the exception of some basic style settings supported by OsmAnd.
The marker/line description and any [custom fields](../types/) will be saved in the description of the GPX objects.
### GeoJSON
## Table
When exporting a map as GeoJSON, all markers and lines including their data fields, all [saved views](../views/), and all [types](../types/) that represent the exported markers/lines are exported. This means that if no filter is active, this is suitable to make a backup of the whole map (apart from the [map settings](../map-settings/) and the [edit history](../history/)). Such a file can also be [imported](../import/) again into a map to restore the backup.
The table export is a static HTML export of all the markers and lines (or, if a [filter](../filter/) is active, those that match the filter) in table form. All the field values of the markers/lines are shown, along with some metadata (name, coordinates, distance, time).
### HTML
A separate table for each [type](../types/) is shown. Individual types can be hidden by clicking the down arrow next to their heading.
The HTML export will render a web page that contains a table for each [type](../types/) that lists each marker/line of that type along with all field values and some metadata (name, coordinates, distance, time). Types can be shown/hidden by clicking on the down arrow next to their heading, and the table can be sorted by an individual data attribute by clicking on its column header.
Table columns can be sorted by clicking on their header.
For HTML exports, there additional “Copy to clipboard” export method is available. This will copy the table for a single type into the clipboard. Such a table can be pasted directly into a spreadsheet application such as EtherCalc or LibreOffice Calc.
### CSV
CSV files can be opened by most spreadsheet applications. A CSV export will contain all the markers/lines of a single type, along with their field values and some metadata (name, coordinates, distance, time). Note that CSV only supports plain text, so any rich text formatting will be lost.
## Generate a link
By default, the export dialog will create a file and download or open it. By selecting “Generate link” as the export method, you can copy a URL to the exported file instead. This URL will always generate the file with the lastest map data according to the export settings that you have defined, so you can use it to link to the export from a website or a browser bookmark or to integrate the export with another tool that should periodically create copies of your map data.
## Apply a filter
If you have an active [filter](../filter/), the export dialog will show an additional “Apply filter” option. When this option is enabled, the exported file will only contain the map objects that match the filter.

Wyświetl plik

@ -20,6 +20,7 @@ Base layers set the main style of the map. Only one base layer can be shown at t
| Base layer | Source | Purpose |
|------------|--------|---------|
| Lima Labs | [Lima Labs](https://maps.lima-labs.com/) | Good for a general geographic overview. Has a focus on car infrastructure and political borders. Supports high resolution displays. |
| Mapnik | [OpenStreetMap](https://www.openstreetmap.org/) | Good for a general geographic overview. Has a focus on car infrastructure and political borders. |
| TopPlus | [German state](https://gdz.bkg.bund.de/index.php/default/wms-topplusopen-wms-topplus-open.html) | Good for a general geographic overview, with more details than Mapnik. Has a focus on car infrastructure, political borders and topography. Unfortunately all labels are in German. |
| Map1.eu | [Map1.eu](https://www.map1.eu/) | Has a focus on cities/towns, car infrastructure and political borders. Aims to have the same level of detail as a paper map. Only available for Europe. |

Wyświetl plik

@ -27,7 +27,7 @@ The zoom buttons allow you to zoom in and out of the map. Alternatively, you can
The search box allows you to [search for places](../search/) and to [calculate a route](../route/). On small screens, it also contains the [legend](../legend/) if enabled.
On top of the search box, you can click the different tabs to switch between different functions. By default, the search box contains two tabs, the search form and the route form. Different functions of the map may temporarily or permanently add additional tabs.
On top of the search box, you can click the different tabs to switch between different functions. By default, the search box contains three tabs, the search form, the route form and the POI tab. Different functions of the map may temporarily or permanently add additional tabs.
On big screens, you can drag the resize handle on the bottom right of the search box to resize it. Click the resize handle to bring it back to its original size.\
On small screens, the search box appears at the bottom of the screen. You can drag it into and out of view as it fits by dragging the tab bar on top of the search box.

Wyświetl plik

@ -126,7 +126,17 @@ function useLinesLayer(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<Lines
function useLocateControl(map: Ref<Map>): Ref<Raw<Control.Locate>> {
return useMapComponent(
map,
() => markRaw(control.locate({ flyTo: true, icon: "a", iconLoading: "a", markerStyle: { pane: "fm-raised-marker", zIndexOffset: 10000 } })),
() => (
markRaw(control.locate({
flyTo: true,
icon: "a",
iconLoading: "a",
markerStyle: { pane: "fm-raised-marker", zIndexOffset: 10000 },
locateOptions: {
enableHighAccuracy: true
}
}))
),
(locateControl, onCleanup) => {
locateControl.addTo(map.value);

Wyświetl plik

@ -1,12 +1,5 @@
<script setup lang="ts">
import { type Ref, computed, onMounted, ref } from "vue";
import "leaflet/dist/leaflet.css";
import "leaflet.locatecontrol";
import "leaflet.locatecontrol/dist/L.Control.Locate.css";
import "leaflet-graphicscale";
import "leaflet-graphicscale/src/Leaflet.GraphicScale.scss";
import "leaflet-mouse-position";
import "leaflet-mouse-position/src/L.Control.MousePosition.css";
import { useMapContext } from "./leaflet-map-components";
import vTooltip from "../../utils/tooltip";
import type { WritableMapContext } from "../facil-map-context-provider/map-context";
@ -142,11 +135,21 @@
}
}
.leaflet-control-locate.leaflet-control-locate a {
font-size: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
.leaflet-control-locate.leaflet-control-locate {
a {
font-size: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}
&.active a {
color: rgb(32, 116, 182);
}
&.following a {
color: rgb(252, 132, 40);
}
}
path.leaflet-interactive {

Wyświetl plik

@ -1,6 +1,6 @@
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 { SocketVersion, CRU, type Line, type LinePointsEvent, type FindOnMapLine, type ID } from "facilmap-types";
import type { LineWithTrackPoints } from "facilmap-client";
import { cloneDeep, omit } from "lodash-es";
@ -241,7 +241,7 @@ test("Edit line", async () => {
{ lat: 14, lon: 14 },
{ lat: 12, lon: 12 }
]
} satisfies Line<CRU.UPDATE>;
} satisfies Line<CRU.UPDATE> & { id: ID };
const line = await client1.editLine(newData);
const expectedLine = {

Wyświetl plik

@ -1,6 +1,6 @@
import { expect, test, vi } from "vitest";
import { createTemporaryPad, emit, getTemporaryPadData, openClient, openSocket, retry } from "./utils";
import { SocketVersion, CRU, type Marker, type FindOnMapMarker } from "facilmap-types";
import { SocketVersion, CRU, type Marker, type FindOnMapMarker, type ID } from "facilmap-types";
import { cloneDeep } from "lodash-es";
test("Create marker (using default values)", async () => {
@ -136,7 +136,7 @@ test("Edit marker", async () => {
const onMarker3 = vi.fn();
client3.on("marker", onMarker3);
const newData: Marker<CRU.UPDATE> = {
const newData: Marker<CRU.UPDATE> & { id: ID } = {
id: createdMarker.id,
lat: 10,
lon: 10,

Wyświetl plik

@ -1,6 +1,6 @@
import { expect, test, vi } from "vitest";
import { createTemporaryPad, openClient, retry } from "../utils";
import { CRU, type Type } from "facilmap-types";
import { CRU, type ID, type Type } from "facilmap-types";
import { cloneDeep } from "lodash-es";
test("Default types are added", async () => {
@ -321,7 +321,7 @@ test("Update type", async () => {
fields: [
{ name: "Test field", type: "input" }
]
} satisfies Type<CRU.UPDATE>;
} satisfies Type<CRU.UPDATE> & { id: ID };
const typeResult = await client1.editType(update);

Wyświetl plik

@ -1,6 +1,6 @@
import { expect, test, vi } from "vitest";
import { createTemporaryPad, openClient, retry } from "./utils";
import { type CRU, type View } from "facilmap-types";
import { type CRU, type ID, type View } from "facilmap-types";
import { cloneDeep } from "lodash-es";
test("Create view (default values)", async () => {
@ -129,7 +129,7 @@ test("Update view", async () => {
layers: ["grid"],
idx: 2,
filter: "name == 'Test'"
} satisfies View<CRU.UPDATE>;
} satisfies View<CRU.UPDATE> & { id: ID };
const view = await client1.editView(update);
const expectedView: View = {

Wyświetl plik

@ -223,13 +223,13 @@ export default class DatabaseLines {
return createdLine;
}
async updateLine(padId: PadId, lineId: ID, data: Omit<Line<CRU.UPDATE_VALIDATED>, "id">, noHistory?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
async updateLine(padId: PadId, lineId: ID, data: Line<CRU.UPDATE_VALIDATED>, 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> {
async _updateLine(originalLine: Line, data: Line<CRU.UPDATE_VALIDATED>, newType: Type, noHistory?: boolean, trackPointsFromRoute?: Route): Promise<Line> {
if (newType.type !== "line") {
throw new Error(`Cannot use ${newType.type} type for line.`);
}

Wyświetl plik

@ -110,13 +110,13 @@ export default class DatabaseMarkers {
return result;
}
async updateMarker(padId: PadId, markerId: ID, data: Omit<Marker<CRU.UPDATE_VALIDATED>, "id">, noHistory = false): Promise<Marker> {
async updateMarker(padId: PadId, markerId: ID, data: Marker<CRU.UPDATE_VALIDATED>, 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);
}
async _updateMarker(originalMarker: Marker, data: Omit<Marker<CRU.UPDATE_VALIDATED>, "id">, newType: Type, noHistory = false): Promise<Marker> {
async _updateMarker(originalMarker: Marker, data: Marker<CRU.UPDATE_VALIDATED>, newType: Type, noHistory = false): Promise<Marker> {
if (newType.type !== "marker") {
throw new Error(`Cannot use ${newType.type} type for marker.`);
}

Wyświetl plik

@ -125,7 +125,7 @@ export default class DatabaseTypes {
return createdType;
}
async updateType(padId: PadId, typeId: ID, data: Omit<Type<CRU.UPDATE_VALIDATED>, "id">): Promise<Type> {
async updateType(padId: PadId, typeId: ID, data: Type<CRU.UPDATE_VALIDATED>): Promise<Type> {
const rename: Record<string, { name?: string, values?: Record<string, string> }> = {};
for(const field of (data.fields || [])) {
if(field.oldName && field.oldName != field.name)

Wyświetl plik

@ -102,7 +102,7 @@ export default class DatabaseViews {
return newData;
}
async updateView(padId: PadId, viewId: ID, data: Omit<View<CRU.UPDATE_VALIDATED>, "id">): Promise<View> {
async updateView(padId: PadId, viewId: ID, data: View<CRU.UPDATE_VALIDATED>): Promise<View> {
if (data.idx != null) {
await this._freeViewIdx(padId, viewId, data.idx);
}

Wyświetl plik

@ -1,5 +1,5 @@
import { bboxValidator, colourValidator, idValidator, padIdValidator, pointValidator, routeModeValidator, strokeValidator, zoomLevelValidator } from "./base.js";
import { CRU, type CRUType, cruValidator, optionalCreate, onlyRead, exceptCreate, optionalUpdate, mapValues, exceptRead } from "./cru";
import { CRU, type CRUType, cruValidator, optionalCreate, onlyRead, optionalUpdate, mapValues, exceptRead } from "./cru";
import * as z from "zod";
export const extraInfoValidator = z.record(z.array(z.tuple([z.number(), z.number(), z.number()])));
@ -14,7 +14,7 @@ export const trackPointValidator = cruValidator({
export type TrackPoint<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof trackPointValidator>;
export const lineValidator = cruValidator({
id: exceptCreate(idValidator),
id: onlyRead(idValidator),
routePoints: optionalUpdate(z.array(pointValidator).min(2)),
typeId: optionalUpdate(idValidator),
name: optionalCreate(z.string().trim().max(100), ""),

Wyświetl plik

@ -1,9 +1,9 @@
import { colourValidator, idValidator, padIdValidator, pointValidator, shapeValidator, sizeValidator, symbolValidator } from "./base.js";
import { CRU, type CRUType, cruValidator, exceptCreate, onlyRead, optionalUpdate, mapValues, optionalCreate } from "./cru";
import { CRU, type CRUType, cruValidator, onlyRead, optionalUpdate, mapValues, optionalCreate } from "./cru";
import * as z from "zod";
export const markerValidator = cruValidator({
id: exceptCreate(idValidator),
id: onlyRead(idValidator),
padId: onlyRead(padIdValidator),
...mapValues(pointValidator.shape, optionalUpdate),
typeId: optionalUpdate(idValidator),

Wyświetl plik

@ -1,4 +1,4 @@
import { type Bbox, bboxWithZoomValidator, objectWithIdValidator, type ObjectWithId } from "../base.js";
import { type Bbox, bboxWithZoomValidator, objectWithIdValidator, type ObjectWithId, idValidator } from "../base.js";
import { type PadData, padDataValidator, Writable } from "../padData.js";
import { type Marker, markerValidator } from "../marker.js";
import { type Line, lineValidator, type TrackPoint } from "../line.js";
@ -23,11 +23,11 @@ export const requestDataValidatorsV2 = {
revertHistoryEntry: objectWithIdValidator,
getMarker: objectWithIdValidator,
addMarker: markerValidator.create,
editMarker: markerValidator.update,
editMarker: markerValidator.update.extend({ id: idValidator }),
deleteMarker: objectWithIdValidator,
getLineTemplate: lineTemplateRequestValidator,
addLine: lineValidator.create,
editLine: lineValidator.update,
editLine: lineValidator.update.extend({ id: idValidator }),
deleteLine: objectWithIdValidator,
exportLine: lineExportRequestValidator,
find: findQueryValidator,
@ -38,10 +38,10 @@ export const requestDataValidatorsV2 = {
lineToRoute: lineToRouteCreateValidator,
exportRoute: routeExportRequestValidator,
addType: typeValidator.create,
editType: typeValidator.update,
editType: typeValidator.update.extend({ id: idValidator }),
deleteType: objectWithIdValidator,
addView: viewValidator.create,
editView: viewValidator.update,
editView: viewValidator.update.extend({ id: idValidator }),
deleteView: objectWithIdValidator,
geoip: nullOrUndefinedValidator,
setPadId: z.string()

Wyświetl plik

@ -1,5 +1,5 @@
import { colourValidator, idValidator, padIdValidator, routeModeValidator, shapeValidator, sizeValidator, strokeValidator, symbolValidator, widthValidator } from "./base.js";
import { CRU, type CRUType, cruValidator, onlyUpdate, optionalCreate, exceptCreate, exceptUpdate, optionalUpdate, onlyRead } from "./cru";
import { CRU, type CRUType, cruValidator, onlyUpdate, optionalCreate, exceptUpdate, optionalUpdate, onlyRead } from "./cru";
import * as z from "zod";
export const objectTypeValidator = z.enum(["marker", "line"]);
@ -90,7 +90,7 @@ export const fieldsValidator = {
};
const rawTypeValidator = cruValidator({
id: exceptCreate(idValidator),
id: onlyRead(idValidator),
type: exceptUpdate(objectTypeValidator),
padId: onlyRead(padIdValidator),

Wyświetl plik

@ -1,9 +1,9 @@
import { bboxValidator, idValidator, layerValidator, padIdValidator } from "./base.js";
import { CRU, type CRUType, cruValidator, exceptCreate, onlyRead, optionalUpdate, mapValues, optionalCreate } from "./cru.js";
import { CRU, type CRUType, cruValidator, onlyRead, optionalUpdate, mapValues, optionalCreate } from "./cru.js";
import * as z from "zod";
export const viewValidator = cruValidator({
id: exceptCreate(idValidator),
id: onlyRead(idValidator),
padId: onlyRead(padIdValidator),
name: optionalUpdate(z.string().trim().min(1).max(100)),

Wyświetl plik

@ -46,8 +46,8 @@ export function normalizeFieldValue(field: Field, value: string | undefined, ign
}
}
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"> = {};
export function applyMarkerStyles(marker: Marker<CRU.READ | CRU.CREATE_VALIDATED>, type: Type): Marker<CRU.UPDATE_VALIDATED> {
const update: Marker<CRU.UPDATE_VALIDATED> = {};
if(type.colourFixed && marker.colour != type.defaultColour)
update.colour = type.defaultColour;
@ -91,16 +91,16 @@ export function resolveCreateMarker(marker: Marker<CRU.CREATE>, type: Type): Mar
return result;
}
export function resolveUpdateMarker(marker: Marker, update: Omit<Marker<CRU.UPDATE>, "id">, newType: Type): Omit<Marker<CRU.UPDATE_VALIDATED>, "id"> {
const resolvedUpdate = markerValidator.update.omit({ id: true }).parse(update);
export function resolveUpdateMarker(marker: Marker, update: Marker<CRU.UPDATE>, 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"> = {};
export function applyLineStyles(line: Line<CRU.READ | CRU.CREATE_VALIDATED>, type: Type): Line<CRU.UPDATE_VALIDATED> {
const update: Line<CRU.UPDATE_VALIDATED> = {};
if(type.colourFixed && line.colour != type.defaultColour) {
update.colour = type.defaultColour;
@ -149,8 +149,8 @@ export function resolveCreateLine(line: Line<CRU.CREATE>, type: Type): Line<CRU.
return result;
}
export function resolveUpdateLine(line: Line, update: Omit<Line<CRU.UPDATE>, "id">, newType: Type): Omit<Line<CRU.UPDATE_VALIDATED>, "id"> {
const resolvedUpdate = lineValidator.update.omit({ id: true }).parse(update);
export function resolveUpdateLine(line: Line, update: Line<CRU.UPDATE>, newType: Type): Line<CRU.UPDATE_VALIDATED> {
const resolvedUpdate = lineValidator.update.parse(update);
return {
...resolvedUpdate,
...applyLineStyles({ ...line, ...resolvedUpdate }, newType)