Porównaj commity

...

51 Commity

Autor SHA1 Wiadomość Data
Candid Dauth 1ebe449286 Add Russian to language picker 2024-04-07 22:40:17 +02:00
Candid Dauth 2949910ce3
Merge pull request #260 from weblate/weblate-facilmap-facilmap-frontend
Translations update from Hosted Weblate
2024-04-07 22:27:42 +02:00
Roman Deev 7f9e7378e5
Translated using Weblate (Russian)
Currently translated at 6.6% (24 of 359 strings)

Translation: FacilMap/FacilMap frontend
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-frontend/ru/
2024-04-07 22:25:46 +02:00
Roman Deev 9b51fb9f6d
Added translation using Weblate (Russian) 2024-04-07 22:25:45 +02:00
Candid Dauth f2a687e971 Add more translations 2024-04-07 22:25:22 +02:00
Candid Dauth 72210d9433 Make sure that route has query can be parsed again 2024-04-07 22:24:08 +02:00
Candid Dauth 14c3ab1a4a Add more translations 2024-04-07 13:49:21 +02:00
Candid Dauth 4cb2f52928 Fix route query parsing in other languages 2024-04-07 13:13:01 +02:00
Candid Dauth b9e4fffb0f Add more translations 2024-04-07 01:39:59 +02:00
Candid Dauth 7f103d4a95 Add more translations 2024-04-06 15:32:13 +02:00
Candid Dauth c6a89d111d Add more translations 2024-04-06 13:18:56 +02:00
Candid Dauth 1d82cfa7c3 Convert utils i18n file to weblate format 2024-04-06 01:38:21 +02:00
Candid Dauth 726f49d91c Add more translations 2024-04-06 01:38:06 +02:00
Candid Dauth 1b9d8fa162
Merge pull request #259 from weblate/weblate-facilmap-facilmap-frontend
Translations update from Hosted Weblate
2024-04-06 00:12:24 +02:00
Candid Dauth 3aa5d91ce7
Translated using Weblate (English)
Currently translated at 100.0% (248 of 248 strings)

Translation: FacilMap/FacilMap frontend
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-frontend/en/
2024-04-06 00:10:40 +02:00
Allan Nordhøy 44fcb27382
Translated using Weblate (Norwegian Bokmål)
Currently translated at 18.9% (47 of 248 strings)

Translation: FacilMap/FacilMap frontend
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-frontend/nb_NO/
2024-04-06 00:07:13 +02:00
Allan Nordhøy 42b181e597
Translated using Weblate (English)
Currently translated at 100.0% (248 of 248 strings)

Translation: FacilMap/FacilMap frontend
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-frontend/en/
2024-04-06 00:07:12 +02:00
Candid Dauth 667b7036bb Offer new Norwegian translation in the UI 2024-04-06 00:06:20 +02:00
Candid Dauth e5f806c21a
Merge pull request #258 from weblate/weblate-facilmap-facilmap-frontend
Translations update from Hosted Weblate
2024-04-05 23:18:07 +02:00
Allan Nordhøy ef563d357b
Added translation using Weblate (Norwegian Bokmål) 2024-04-05 18:32:18 +02:00
Allan Nordhøy c009cd4dcd
Translated using Weblate (Norwegian Bokmål)
Currently translated at 12.1% (4 of 33 strings)

Translation: FacilMap/FacilMap utils
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-utils/nb_NO/
2024-04-05 16:08:03 +02:00
Allan Nordhøy 4db6d44248
Added translation using Weblate (Norwegian Bokmål) 2024-04-05 16:08:03 +02:00
Candid Dauth cc4354877a Add note that translations may be incomplete 2024-04-05 16:07:48 +02:00
Candid Dauth 1de9134b2a Translate history-dialog 2024-04-05 13:48:26 +02:00
Candid Dauth 0e9398bc30 Change "fix" to "fest" in German 2024-04-05 13:47:53 +02:00
Candid Dauth bcdc4c1132 Translate edit-type-dialog to German 2024-04-05 13:28:14 +02:00
Candid Dauth b0426362f6 Avoid HMR error when file has errors 2024-04-05 01:12:48 +02:00
Candid Dauth d60455dd50 Internationalize edit-type-dialog 2024-04-05 01:10:54 +02:00
Candid Dauth 899ce296a9 Add heading to README 2024-04-05 00:14:34 +02:00
Candid Dauth df2dbef085 Document units query parameter for embedding 2024-04-05 00:14:02 +02:00
Candid Dauth 935502b640 Mention Weblate in the docs and README 2024-04-05 00:13:51 +02:00
Candid Dauth e84a5d5663 Adjust i18n file indentation to weblate format 2024-04-04 23:44:34 +02:00
Candid Dauth aea174eb5b
Merge pull request #256 from weblate/weblate-facilmap-facilmap-frontend
Translations update from Hosted Weblate
2024-04-04 23:40:18 +02:00
Anonymous c1a3de6015
Translated using Weblate (German)
Currently translated at 100.0% (112 of 112 strings)

Translation: FacilMap/FacilMap frontend
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-frontend/de/
2024-04-04 23:35:03 +02:00
Candid Dauth 3ceb01015c
Merge pull request #255 from weblate/weblate-facilmap-facilmap-frontend
Translations update from Hosted Weblate
2024-04-04 23:32:39 +02:00
Candid Dauth e51378496b
Translated using Weblate (German)
Currently translated at 100.0% (37 of 37 strings)

Translation: FacilMap/FacilMap server
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-server/de/
2024-04-04 23:30:13 +02:00
Candid Dauth 0a94392fea
Translated using Weblate (English)
Currently translated at 100.0% (37 of 37 strings)

Translation: FacilMap/FacilMap server
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-server/en/
2024-04-04 23:28:47 +02:00
Candid Dauth 8ec7e2b798
Translated using Weblate (German)
Currently translated at 100.0% (33 of 33 strings)

Translation: FacilMap/FacilMap utils
Translate-URL: https://hosted.weblate.org/projects/facilmap/facilmap-utils/de/
2024-04-04 23:15:08 +02:00
Candid Dauth c66a53386c Use JSON i18n files for compatibility with weblate 2024-04-04 22:42:25 +02:00
Candid Dauth 45e4171544 Translate toolbox 2024-04-04 21:52:05 +02:00
Candid Dauth 6ec952ca5a Mention language cookies in privacy docs 2024-04-04 21:39:31 +02:00
Candid Dauth 8384cdd316 Introduce units setting to show distances/elevations in US customary units 2024-04-04 21:10:26 +02:00
Candid Dauth 326258dbd1 Make translations in utils package reactive 2024-04-04 13:43:12 +02:00
Candid Dauth 0878fb3d4b Add user preferences dialog and persist language setting as cookie 2024-04-04 02:38:11 +02:00
Candid Dauth bbbfe83b77 Simplify client-provider toast logic and disable auto-hide for reconnecting toast 2024-04-03 23:49:33 +02:00
Candid Dauth 383864bd08 Do not override custom i18n config if server i18n module is imported later 2024-04-03 21:25:51 +02:00
Candid Dauth bfc1fb3028 Do not show "Unnamed map" in title for non-existing maps 2024-04-03 21:14:14 +02:00
Candid Dauth 99673bdbd7 Internationalize some frontend components 2024-04-03 21:06:47 +02:00
Candid Dauth 09ebec0535 gitignore temporary vite config files 2024-04-03 21:06:18 +02:00
Candid Dauth 010425b2cb Document i18n configuration 2024-04-03 21:05:53 +02:00
Candid Dauth fccb4eb2fa Internationalize server 2024-04-03 14:06:18 +02:00
149 zmienionych plików z 4057 dodań i 1370 usunięć

3
.gitignore vendored
Wyświetl plik

@ -13,4 +13,5 @@ config.env
!.yarn/sdks
!.yarn/versions
out
out.*
out.*
vite.config.ts.timestamp-*.mjs

Wyświetl plik

@ -1,3 +1,5 @@
# FacilMap
[FacilMap](https://facilmap.org/) is a privacy-friendly, open-source versatile online map that combines different services based on OpenStreetMap and makes it easy to find places, plan trips and add markers, lines and routes to custom maps with live collaboration. Features include:
* Choose between different map styles for roads, topography, cycling, hiking, public transportation, water navigation, …
@ -49,6 +51,16 @@ Quick start
More details can be found in the [Administrator guide](https://docs.facilmap.org/administrators/server.html#standalone) and the [Developer guide](https://docs.facilmap.org/developers/development/dev-setup.html).
Contribute
==========
* Raise bugs, feature requests and ideas [as issues](https://github.com/FacilMap/facilmap/issues)
* Help translate FacilMap into different languages on [Weblate](https://hosted.weblate.org/projects/facilmap/)
* Create a local [dev setup](https://docs.facilmap.org/developers/development/dev-setup.html) and open pull requests
* [Donate](https://docs.facilmap.org/users/contribute/)
* Spread the word!
Support FacilMap
================

Wyświetl plik

@ -33,6 +33,7 @@
},
"dependencies": {
"facilmap-types": "workspace:^",
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.4"
},
"devDependencies": {

Wyświetl plik

@ -1,5 +1,6 @@
import { io, type ManagerOptions, type Socket as SocketIO, type SocketOptions } from "socket.io-client";
import type { Bbox, BboxWithZoom, CRU, EventHandler, EventName, FindOnMapQuery, FindPadsQuery, FindPadsResult, FindQuery, GetPadQuery, HistoryEntry, ID, Line, LineExportRequest, LineTemplateRequest, LineToRouteCreate, SocketEvents, Marker, MultipleEvents, ObjectWithId, PadData, PadId, PagedResults, SocketRequest, SocketRequestName, SocketResponse, Route, RouteClear, RouteCreate, RouteExportRequest, RouteInfo, RouteRequest, SearchResult, SocketVersion, TrackPoint, Type, View, Writable, SocketClientToServerEvents, SocketServerToClientEvents, LineTemplate, LinePointsEvent } from "facilmap-types";
import { type Bbox, type BboxWithZoom, type CRU, type EventHandler, type EventName, type FindOnMapQuery, type FindPadsQuery, type FindPadsResult, type FindQuery, type GetPadQuery, type HistoryEntry, type ID, type Line, type LineExportRequest, type LineTemplateRequest, type LineToRouteCreate, type SocketEvents, type Marker, type MultipleEvents, type ObjectWithId, type PadData, type PadId, type PagedResults, type SocketRequest, type SocketRequestName, type SocketResponse, type Route, type RouteClear, type RouteCreate, type RouteExportRequest, type RouteInfo, type RouteRequest, type SearchResult, type SocketVersion, type TrackPoint, type Type, type View, type Writable, type SocketClientToServerEvents, type SocketServerToClientEvents, type LineTemplate, type LinePointsEvent, PadNotFoundError, type SetLanguageRequest } from "facilmap-types";
import { deserializeError, errorConstructors } from "serialize-error";
export interface ClientEvents extends SocketEvents<SocketVersion.V2> {
connect: [];
@ -65,6 +66,8 @@ interface ClientData {
routes: Record<string, RouteWithTrackPoints>;
}
errorConstructors.set("PadNotFoundError", PadNotFoundError as any);
export default class Client {
private socket: SocketIO<SocketServerToClientEvents<SocketVersion.V2>, SocketClientToServerEvents<SocketVersion.V2>>;
private state: ClientState;
@ -192,9 +195,9 @@ export default class Client {
this._simulateEvent("emit", eventName as any, data as any);
return await new Promise((resolve, reject) => {
this.socket.emit(eventName as any, data, (err: Error, data: SocketResponse<SocketVersion.V2, R>) => {
this.socket.emit(eventName as any, data, (err: any, data: SocketResponse<SocketVersion.V2, R>) => {
if(err) {
reject(err);
reject(deserializeError(err));
this._simulateEvent("emitReject", eventName as any, err);
} else {
const fixedData = this._fixResponseObject(eventName, data);
@ -352,6 +355,10 @@ export default class Client {
return await this._setPadId(padId);
}
async setLanguage(language: SetLanguageRequest): Promise<void> {
await this._emit("setLanguage", language);
}
async updateBbox(bbox: BboxWithZoom): Promise<MultipleEvents<SocketEvents<SocketVersion.V2>>> {
const isZoomChange = this.bbox && bbox.zoom !== this.bbox.zoom;

Wyświetl plik

@ -54,7 +54,8 @@ export default defineUserConfig({
"/users/files/",
"/users/locate/",
"/users/share/",
"/users/app/"
"/users/app/",
"/users/user-preferences/"
]
},
{
@ -80,7 +81,8 @@ export default defineUserConfig({
text: "Developer guide",
children: [
"/developers/",
"/developers/embed"
"/developers/embed",
"/developers/i18n"
]
},
{

Wyświetl plik

@ -86,6 +86,26 @@ When creating/updating/deleting an object, the data is propagated in multiple wa
Note that creating/updating/deleting an object will fail if the operation is not permitted. The above example will fail if the map was opened using its read-only ID.
### Internationalization
Most of the data returned by the client is user-generated and thus not internationalized. There are a few exceptions though, in particular error messages in case someting unexpected happens.
By default, the FacilMap backend detects the user language based on the `Accept-Language` HTTP header. The detected language can be overridden by setting a `lang` cookie or query parameter. In addition, a `units` cookie or query parameter can be set to `metric` or `us_customary`.
For the websocket, the `Accept-Language` header, cookies and query parameters are sent during the socket.io handshake. If you want to force the socket to use a sepcific language, you can pass query parameters through the third parameter of the client constructor:
```js
import Client from "facilmap-client";
const client = new Client("https://facilmap.org/", undefined, {
query: {
lang: "en",
units: "us_customary"
}
});
```
You can also update the internationalization settings for an existing socket connection at any point using [`setLanguage()`](./methods#setlanguage-settings).
### Deal with connection problems
```js

Wyświetl plik

@ -1,6 +1,6 @@
# Methods
## `constructor(server, padId)`
## `constructor(server, padId, socketOptions)`
Connects to the FacilMap server `server` and optionally opens the collaborative map with the ID `padId`. If the pad ID
is not set, it can be set later using [`setPadId(padId)`](#setpadid-padid) or using [`createPad(data)`](#createpad-data).
@ -11,6 +11,7 @@ If the connection to the server breaks down, a `disconnect` event will be emitte
* `server` (string): The URL of the FacilMap server, for example `https://facilmap.org/`.
* `padId` (string, optional): The ID of the collaborative map to open.
* `socketOptions` (object, optional): Any additional [Socket.io client options](https://socket.io/docs/v4/client-options/).
* **Events:** Causes a `connect` event to be fired when the connection is established. If `padId` is defined, causes events to be fired with the map settings, all views, all types and all lines (without line points) of the map. If the map with `padId` could not be opened, causes a [`serverError`](./events.md#servererror) event.
## `on(eventName, function)`
@ -40,11 +41,22 @@ Setting the padId causes the server to send several objects, such as the map set
* **Events:** Causes events to be fired with the map settings, all views, all types and all lines (without line points) of the map. If the map could not be opened, causes a [`serverError`](./events.md#servererror) event.
* **Availability:** Only available if no map is opened yet on this client instance.
## `setLanguage(settings)`
Updates the language settings for the current socket connection. Usually this only needs to be called if the user changes their internationalization settings and you want to apply the new settings live in the UI. See [Internationalization](./#internationalization) for the details and how to set the language settings when opening a client.
* `settings`: An object with the following properties:
* `lang` (optional): The language, for example `en` or `de`.
* `units` (optional): The units to use, either `metric` or `us_costomary`.
* **Returns:** A promise tat is resolved empty when the settings have been applied.
* **Events:** None.
* **Availability:** Always.
## `updateBbox(bbox)`
Updates the bbox. This will cause all markers, line points and route points within the bbox (except the ones that were already in the previous bbox, if there was one) to be received as individual events.
* __bbox__ ([Bbox](./types.md#bbox) with zoom): The bbox that objects should be received for.
* `bbox` ([Bbox](./types.md#bbox) with zoom): The bbox that objects should be received for.
* **Returns:** A promise that is resolved empty when all objects have been received.
* **Events:** Causes events to be fired with the markers, line points and route points within the bbox.
* **Availability:** Always.

Wyświetl plik

@ -107,7 +107,7 @@ their `idx` property.
## Type
* `id` (number): The ID of this type
* `name` (string): The name of this type
* `name` (string): The name of this type. Note that the if the name is "Marker" or "Line", the FacilMap UI will translate the name to other languages even though the underlying name is in English.
* `type` (string): `marker` or `line`
* `idx` (number): The sorting position of this type. When a list of types is shown to the user, it must be ordered by this value. If types were deleted or reordered, there may be gaps in the sequence of indexes, but no two types on the same map can ever have the same index. When setting this as part of a type creation/update, other types with a same/higher index will have their index increased to be moved down the list.
* `defaultColour`, `defaultSize`, `defaultSymbol`, `defaultShape`, `defaultWidth`, `defaultStroke`, `defaultMode` (string/number): Default values for the
@ -115,7 +115,7 @@ their `idx` property.
* `colourFixed`, `sizeFixed`, `symbolFixed`, `shapeFixed`, `widthFixed`, `strokeFixed`, `modeFixed` (boolean): Whether those values are fixed and
cannot be changed for an individual object
* `fields` ([object]): The form fields for this type. Each field has the following properties:
* `name` (string): The name of the field. This is at the same time the key in the `data` properties of markers and lines
* `name` (string): The name of the field. This is at the same time the key in the `data` properties of markers and lines. Note that the if the name is "Description", the FacilMap UI will translate the name to other languages even though the underlying name is in English.
* `oldName` (string): When renaming a field (using [`editType(data)`](./methods.md#edittype-data)), specify the former name here
* `type` (string): The type of field, one of `textarea`, `dropdown`, `checkbox`, `input`
* `controlColour`, `controlSize`, `controlSymbol`, `controlShape`, `controlWidth`, `controlStroke` (boolean): If this field is a dropdown, whether the different options set a specific property on the object

Wyświetl plik

@ -1,13 +1,12 @@
# Dev setup
1. Run `yarn install` to install the dependencies
2. Run `yarn build` to build the JS bundles
3. Copy `config.env.example` to `config.env` and adjust the settings
4. Run `yarn server` inside the `server` directory
2. Copy `config.env.example` to `config.env` and adjust the settings
3. Run `yarn dev-server` inside the `server` directory
For developing the frontend/client, FacilMap server can integrate Vite. This server will transpile frontend files on the fly can even apply changes to Vue components without having to reload the page. To run the dev server, run `yarn dev-server` instead of `yarn server` in the `server` directory. Note that changes in the `client`, `types` or `leaflet` directory still have to be built using `yarn build` in the respective directories for the dev-server to notice them.
This will start the FacilMap server with an integrated Vite dev server that takes care of transpiling the frontend on the fly and also integrating hot module reloading, which can apply Vue component changes without a page reload.
While developing the server, run `yarn ts-server`, which will start the server straight from the TypeScript files (which makes it obsolete to run `yarn build` every time before restarting the server).
While developing the server, you can also run `yarn server` instead, which will start the server straight from the TypeScript files (which makes it obsolete to run `yarn build` every time before restarting the server) but without transpiling the frontend each time, which makes restarts faster.
To enable debug output of various components, additionally prepend the command by `DEBUG=*`. See the documentation of
[debug](https://github.com/visionmedia/debug). To only enable the debug logging of SQL queries, use `DEBUG=sql`.

Wyświetl plik

@ -18,6 +18,8 @@ You can control the display of different components by using the following query
* `autofocus`: Autofocus the search field (default: `false`)
* `legend`: Show the legend if available (default: `true`)
* `interactive`: Enable [interactive mode](#interactive-mode) (default: `false`)
* `lang`: Use this display language (for example `en`) by default, instead of the language set by the user in the user preferences dialog or in their browser.
* `units`: Use this type of units (either `metric` or `us_customary`) by default, instead of what the user has configured in the user preferences dialog.
Example:

Wyświetl plik

@ -0,0 +1,93 @@
# I18n
FacilMap uses [i18next](https://www.i18next.com/) for internationalization throughout the frontend, the server and its libraries. It detects the desired user language like this:
* In the browser, [i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector) is used to detect the users language. It looks at the configured browser languages ([`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages)) and checks for which one a translation exists. The configured language can be overridden by setting a `lang` cookie or appending a `?lang=` query parameter to the URL.
* On the server, when a request is handled through HTTP (including the WebSocket), [i18next-http-middleware](https://www.npmjs.com/package/i18next-http-middleware) is used to detect the users language. It looks at the configured browser languages ([`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)) and checks for which one a translation exists. The configured language can be overridden by setting a `lang` cookie or by appending a `?lang=` query parameter to the URL. The server stores the selected language in the [Node.js domain](https://nodejs.org/api/domain.html) that is created for each incoming request, causing all functions triggered (sync or async) from the request to use the language setting of the request.
* On the sever, when a function is called outside of an incoming HTTP request, messages are not internationalized and output in English.
In addition, certain values can be shown in metric units or in US customary units. By default, metric units are used. This can be changed by sending a `units` query parameter or cookie with the value `us_customary`.
Translations are managed on [Weblate](https://hosted.weblate.org/projects/facilmap/), changes there automatically trigger a pull request to the FacilMap repository.
## Use FacilMap in an app not using i18next
When you import any of the FacilMap modules into a JavaScript app that does not use i18next, they will automatically detect the user language and internationalize their output accordingly as described above.
The main instance of i18next is [initialized](https://www.i18next.com/overview/api#init) by FacilMap as soon as the first message is internationalized. This that before calling any of the functions exported by FacilMap, you can still change the i18next configuration.
### Change the language detector
To change the detected user language, you have two options. As mentioned above, both options need to be executed before calling any FacilMap functions that generate internationalized messages.
Option 1 is to set a custom [language detector](https://www.i18next.com/overview/plugins-and-utils#language-detector) using the `setLanguageDetector()` function exported by `facilmap-utils`. This language detector is applied to the `i18next` main instance when it is initialized instead of the default language detector used by FacilMap.
```typescript
import { setLanguageDetector } from "facilmap-utils";
setLanguageDetector(myLanguageDetector);
```
Option 2 is to set a custom [i18next instance](https://www.i18next.com/overview/api#instance-creation) that will be used by FacilMap. In this example, we are disabling the language detector and use a custom i18next instance that always uses German:
```typescript
import { setLanguageDetector, setI18nGetter } from "facilmap-utils";
import { createInstance } from "i18next";
setLanguageDetector(undefined);
setI18nGetter(() => createInstance({ lng: "de" }));
```
### Use the backend language detector
If you are including some of FacilMaps server modules in your own Node.js express server and want messages to be internationalized according to the user language as described above, you need to add FacilMaps `i18nMiddleware` to your express server:
```typescript
import express from "express";
import domainMiddleware from "express-domain-middleware";
import { i18nMiddleware } from "facilmap-server";
const app = express();
app.use(domainMiddleware);
app.use(i18nMiddleware);
```
As seen in the example, `i18nMiddleware` requires [express-domain-middleware](https://www.npmjs.com/package/express-domain-middleware) to be initialized before it.
## Use FacilMap in an app already using i18next
When you use FacilMaps modules in an app that is already using i18next, you may want FacilMap to reuse your existing i18n configuration (such as language detection), or you may want it to use its own configuration independently from yours.
When a FacilMap function internationalizes a message for the first time, it initializes i18next in the following way:
1. It retrieves the i18next instance set through `setI18nGetter()` (exported by `facilmap-utils`). If no instance was set, it defaults to the i18next main instance (`import i18next from "i18next"`).
2. If the instance is not initialized yet (`!i18next.isInitializing && !i18next.isInitialized`), it initializes it with its default configuration (language detector as described above).
3. It adds its resources to the instance under namespaces prefixed by `facilmap-`.
### Reuse your i18next instance for FacilMap
You can make FacilMap reuse your existing i18next instance and configuration in the following way:
* Make sure your i18next instance is initialized before you call any FacilMap functions that need to internationalize messages.
* Call `setI18nGetter()` with a callback that returns your i18next instance (can be skipped if you are using the main instance).
```typescript
import { setLanguageDetector, setI18nGetter } from "facilmap-utils";
import { createInstance } from "i18next";
const i18next = createInstance();
await i18next.init({
lng: "en"
});
setI18nGetter(() => i18next);
```
### Use a separate i18next instance for FacilMap
If you want to make FacilMap use a separate i18next instance, for example because you want to keep your and FacilMaps language detection independent, use the following example:
```typescript
import { setI18nGetter } from "facilmap-utils";
import { createInstance } from "i18next";
setLanguageDetector(undefined);
setI18nGetter(() => createInstance());
```
Because the instance returned by `createInstance()` is not initialized, FacilMap will initialize it using its default configuration.

Wyświetl plik

@ -12,6 +12,7 @@ FacilMap is a privacy-friendly, open-source versatile online map that combines d
* [Show your location on the map](./locate/)
* [Share a link](./share/) to a particular view of the map.
* Add FacilMap as an [app](./app/) to your device.
* Change the language settings in the [user preferences](./user-preferences/).
* FacilMap is [privacy-friendly](./privacy/) and does not track you.
In addition, FacilMap allows you to create collaborative maps, where you and others can add markers, draw lines, save routes and import GPX/KML/GeoJSON files, which will be saved under a custom URL.

Wyświetl plik

@ -2,4 +2,6 @@
If you have a question that is not answered by this documentation, feel free to to open a [discussion on GitHub](https://github.com/FacilMap/facilmap/discussions) or join the [Matrix chat room](https://matrix.to/#/#facilmap:rankenste.in).
If you have found an error or have a suggestion how FacilMap or this documentation could be improved, please raise an [issue on GitHub](https://github.com/FacilMap/facilmap/issues).
If you have found an error or have a suggestion how FacilMap or this documentation could be improved, please raise an [issue on GitHub](https://github.com/FacilMap/facilmap/issues).
If you have found a translation mistake or would like to add a missing translation, you can contribute directly on [Weblate](https://hosted.weblate.org/projects/facilmap/).

Wyświetl plik

@ -6,18 +6,19 @@ FacilMap is a combination of services provided by FacilMap itself and third-part
## FacilMap itself
The following data is *sent* to the FacilMap server but is *not persisted* there:
The following data is *processed* but *not persisted* by the FacilMap server:
* Your **IP address**: When you open FacilMap, a connection to the server is made, which inevitably reveils your IP address to the server. FacilMap uses your IP address to guess your location using the [MaxMind GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) database (to decide the initial map view), but it uses a local copy of that database rather than sending your IP address to MaxMind.
* Your **map position**: When you have a [route](../route/) or a [collaborative map](../collaborative/) open, the current map position is sent to the server every time you move/zoom the map. (In response, the server will send any map objects in your current map view.)
* Your **search term**: When you [search](../search/) for a place, open a [map point](../click-marker/) or calculate a [route](../route/), the search term is sent to the FacilMap server and relayed to third-party services from there.
* Your **language settings**: If you have set custom language and/or unit user preferences, these are sent to the server on each request. Otherwise, the language preferences that your browser sends with each request are used.
The following data is sent to the FacilMap server and *persisted* there:
The following data is *processed* and *persisted* by the FacilMap server:
* When you calculate a [route](../route/), the details about this route are stored on the server until you close the route (or your browser window) again.
* Any [markers](../markers/), [lines](../lines), [types](../types), [views](../views) that you add and any [map settings](../map-settings/) that you make on a [collaborative map](../collaborative/) is stored on the server. Deleting such objects will still keep a copy in the [change history](../history/). The only way to delete the data is to [delete the collaborative map](../map-settings/#delete-the-map).
The following data is persisted in your browser:
* If you change the [zoom settings](../search/#zoom-settings), these are persisted in the local storage of your browser.
* If you add [bookmarks](../collaborative/#bookmark-a-map), these are persisted in the local storage of your browser.
* If you change the language or unit user preferences, these are persisted as a cookie in your browser.
## Layers
@ -30,7 +31,7 @@ FacilMap does not have any control over what these providers do with your data,
## Search/route
When you make a [search](../search/), calculate a [route](../route/) or open a [map point](../click-marker/), the search is relayed through the FacilMap server to the third-party provider. This means that only the search terms itself are sent to the provider, but your IP address or any cookies are not reveiled to the third party. The following providers are used:
When you make a [search](../search/), calculate a [route](../route/) or open a [map point](../click-marker/), a third-party provider is used, so the search terms, your IP address and any cookies set by the provider may be transmitted. The following providers are used:
* [Nominatim](https://nominatim.openstreetmap.org/) to resolve search terms or locations
* [Mapbox](https://www.mapbox.com/) for routes with a simple [route mode](../route/#route-modes)
* [OpenRouteService](https://openrouteservice.org/) for routes with an advanced [route mode](../route/#route-modes)

Wyświetl plik

@ -0,0 +1,13 @@
# User preferences
Clicking “Tools” and then “User preferences” in the [toolbox](../ui/#toolbox) will open the user preferences dialog. What you set here will be saved as cookies in your browser, so it will affect how FacilMap works for you on any map that you open, but it will not affect what any other user will see.
## Language and units
FacilMap tries to display its user interface in one of the languages that you have configured your browser to display websites in. If no translation is available for any of your languages, English is used.
By default, FacilMap uses international standard units (metric) to display distances and elevations.
If these defaults do not match your preferences, you can configure them in the user preferences dialog.
FacilMaps translations are collectively created on [Weblate](https://hosted.weblate.org/projects/facilmap/). If the translation for your preferred language is incomplete, incorrect or doesnt exist yet, your contribution on Weblate would be greatly appreciated!

Wyświetl plik

@ -54,6 +54,7 @@
"hammerjs": "^2.0.8",
"i18next": "^23.10.1",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"leaflet-draggable-lines": "^2.0.0",
"leaflet-graphicscale": "^0.0.4",
@ -74,7 +75,8 @@
"vite-plugin-css-injected-by-js": "^3.4.0",
"vite-plugin-dts": "^3.7.3",
"vue": "^3.4.21",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/bootstrap": "^5.2.10",
@ -82,6 +84,7 @@
"@types/file-saver": "^2.0.7",
"@types/hammerjs": "^2.0.45",
"@types/jquery": "^3.5.29",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.8",
"@types/leaflet-mouse-position": "^1.2.4",
"@types/leaflet.locatecontrol": "^0.74.4",

Wyświetl plik

@ -0,0 +1,536 @@
{
"about-dialog": {
"header": "Über FacilMap {{version}}",
"license-text": "{{facilmap}} is unter der {{license}} verfügbar.",
"license-text-facilmap": "FacilMap",
"license-text-license": "GNU Affero General Public License, Version 3",
"issues-text": "Bitte melden Sie Fehler und Verbesserungsvorschläge auf {{tracker}}.",
"issues-text-tracker": "GitHub",
"help-text": "Wenn Sie Fragen haben, schauen Sie sich die {{documentation}} an, schreiben Sie ins {{discussions}} oder fragen im {{chat}}.",
"help-text-documentation": "Dokumentation",
"help-text-discussions": "Forum",
"help-text-chat": "Matrix-Chat",
"privacy-information": "Informationen zum Datenschutz",
"map-data": "Kartendaten",
"map-data-search": "Suche",
"map-data-pois": "POIs",
"map-data-directions": "Routenberechnung",
"map-data-geoip": "GeoIP",
"map-data-geoip-description": "Dieses Produkt enthält GeoLite2-Daten von Maxmind, verfügbar unter {{maxmind}}.",
"attribution-osm-contributors": "OSM-Mitwirkende",
"programs-libraries": "Programme/Bibliotheken",
"icons": "Symbole"
},
"add-to-map-dropdown": {
"fallback-label": "Zur Karte hinzufügen",
"add-error": "Fehler beim Hinzufügen der Objekte",
"add-marker-items": "Marker als {{typeName}}",
"add-line-items": "Linien/Polygone als {{typeName}}"
},
"alert": {
"fallback-ok-label": "OK"
},
"click-marker-tab": {
"look-up-error": "Fehler beim Laden der Geoinformationen"
},
"client-provider": {
"loading-map-header": "Karte wird geladen",
"loading-map": "Karte wird geladen…",
"connecting-header": "Verbindung wird hergestellt",
"connecting": "Verbindung mit dem Server wird hergestellt…",
"map-deleted-header": "Karte gelöscht",
"map-deleted": "Die Karte wurde gelöscht.",
"close-map": "Karte schließen",
"connection-error": "Bei der Verbindung zum Server ist ein Fehler aufgetreten",
"open-map-error": "Beim Öffnen der Karte ist ein Fehler aufgetreten",
"disconnected-header": "Verbindung unterbrochen",
"disconnected": "Die Verbindung zum Server ist verloren gegangen. Verbindung wird wiederhergestellt…"
},
"colour-picker": {
"format-error": "Muss im hexadezimalen Format mit 6 Stellen definiert werden, zum Beispiel 0000ff."
},
"coordinates": {
"copied-title": "Koordinaten kopiert",
"copied-message": "Die Koordinaten wurden in die Zwischenablage kopiert.",
"copy-to-clipboard": "In die Zwischenablage kopieren",
"elevation": "Höhe über NN"
},
"copy-to-clipboard-input": {
"copied-fallback-title": "Link kopiert",
"copied-fallback-message": "Der Link wurde in die Zwischenablage kopiert.",
"copy": "Kopieren",
"qr-code-alt": "QR-Code",
"qr-code-tooltip": "QR-Code anzeigen"
},
"custom-import-dialog": {
"existing-type": "Existierender Objekttyp „{{name}}“",
"import-type": "Objekttyp „{{name}}“ importieren",
"no-import": "Nicht importieren",
"import-error": "Fehler beim Importieren",
"dialog-title": "Benutzerdefinierter Import",
"ok-label": "Importieren",
"type": "Objekttyp",
"map-to": "Importieren als…",
"markers": "Marker des Typs {{typeName}} ({{count}})",
"lines_one": "Linie des Typs {{typeName}} ({{count}})",
"lines_other": "Linien des Typs {{typeName}} ({{count}})",
"untyped-markers": "Marker ohne Typ ({{count}})",
"untyped-lines_one": "Linie/Polygon ohne Typ ({{count}})",
"untyped-lines_other": "Linien/Polygone ohne Typ ({{count}})"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Anwenden",
"introduction": "Hier können Sie eine Filterformel festlegen, die definiert, welche Marker/Linien basierend auf ihren Attributen angezeigt/versteckt werden sollen. Die Filterformel beeinflusst nur Ihre persönliche Ansicht der Karte, sie kann jedoch als Teil einer gespeicherten Ansicht oder eines geteilten Links wiederverwendet werden.",
"syntax-header": "Syntax",
"variable": "Variable",
"operator": "Operator",
"description": "Beschreibung",
"example": "Beispiel",
"name-description": "Name des Markers oder der Linie",
"type-description": "Art des Objekts: {{marker}} (Marker) / {{line}} (Linie)",
"typeId-description": "Typ des Objekts: {{items}})",
"typeId-description-item": "{{typeId}} ({{typeName}})",
"typeId-description-separator": " / ",
"data-description-1": "Feldwerte (Beispiel: {{example1}} oder {{example2}}).",
"data-description-2": "Für Checkbox-Felder ist der Wert {{uncheckedValue}} (nicht selektiert) oder {{checkedValue}} (selektiert).",
"lon-lat-description": "Koordinaten des Markers",
"colour-description": "Farbe des Markers oder der Linie",
"size-description": "Größe des Markers",
"symbol-description": "Symbol des Markers",
"shape-description": "Form des Markers",
"ele-description": "Höhe über NN des Markers",
"mode-description": "Routenmodus der Linie (z.B. {{straight}} (Luftlinie) / {{car}} (Auto) / {{bicycle}} (Fahrrad) / {{pedestrian}} (zu Fuß) / {{track}} (importierter GPX-Track))",
"width-description": "Dicke der Linie",
"stroke-description": "Kontur der Linie ({{solid}} (durchgezogen) / {{dashed}} (gestrichelt) / {{dotted}} (gepunktet))",
"distance-description": "Länge der Linie in Kilometern",
"time-description": "Reisedauer der Linie in Sekunden",
"ascent-descent-description": "Aufstieg/Abstieg der Linie",
"routePoints-description": "Koordinaten der Wegpunkte der Linie",
"number-description": "Zahl",
"text-description": "Zeichenkette",
"mathematical-description": "Mathematische Operationen ({{modulo}}: modulo, {{power}}: Potenz)",
"logical-description": "Logische Operationen",
"ternary-description": "Wenn/dann/sonst-Operator",
"comparison-description": "Vergleich ({{notEqual}}: ungleich) (Groß-/Kleinschreibung relevant)",
"list-description": "Listen-Operator ({{in}}: Wert kommt in der Liste vor, {{notIn}}: Wert kommt nicht in der Liste vor) (Groß-/Kleinschreibung relevant)",
"regexp-description": "Regulärer Ausdruck (Groß-/Kleinschreibung relevant)",
"lower-description": "Zu Kleinbuchstaben konvertieren",
"round-description": "Runden ({{round}}: kaufmännisch runden, {{ceil}}: aufrunden, {{floor}}: abrunden)",
"functions-description": "Mathematische Funktionen ({{abs}}: Betrag, {{log}}: Natürlicher Logarithmus, {{sqrt}}: Quadratwurzel)",
"min-max-description": "Kleinster/größter Wert"
},
"edit-type-dialog": {
"delete-field-title": "Feld löschen",
"delete-field-message": "Wollen Sie das Feld „{{fieldName}}“ wirklich löschen?",
"delete-field-button": "Löschen",
"create-type-error": "Fehler beim Erstellen des Objekttyps",
"save-type-error": "Fehler beim Speichern des Objekttyps",
"field-update-error": "Fehler beim Speichern des Feldes",
"field-disappeared-error": "Das Feld existiert nicht mehr.",
"unique-field-name-error": "Mehrere Felder können nicht den gleichen Namen haben.",
"title": "Objekttyp bearbeiten",
"name": "Name",
"type": "Art",
"type-marker": "Marker",
"type-line": "Linie",
"styles-introduction": "Diese Attribute werden angewendet, wenn ein neues Objekt dieses Typs erstellt wird. Wenn „fest“ aktiviert ist, werden die Attribute für alle Objekte des Typs angewendet und können auch für ein individuelles Objekt nicht mehr geändert werden. Für eine noch komplexere Attributkontrolle können Dropdown- und Checkboxfelder unten konfiguriert werden die Attribute basierend auf ihren ausgewählten Werten zu setzen.",
"default-colour": "Standard-Farbe",
"fixed": "fest",
"default-size": "Standard-Größe",
"default-icon": "Standard-Symbol",
"default-shape": "Standard-Form",
"default-width": "Standard-Dicke",
"default-stroke": "Standard-Kontur",
"default-route-mode": "Standard-Routenmodus",
"legend": "Legende",
"show-in-legend": "In der Legende anzeigen",
"show-in-legend-description": "In der Legende wird ein Eintrag für diesen Objekttyp im Stil seiner festen Attribute angezeigt. Dropdown- und Checkboxfelder mit Attributkontrolle erzeugen weitere Einträge in der Legende für ihre Optionen.",
"fields": "Felder",
"field-name": "Name",
"field-type": "Art",
"field-default-value": "Standardwert",
"field-delete": "Löschen",
"field-type-input": "Textfeld einzeilig",
"field-type-textarea": "Textfeld mehrzeilig",
"field-type-dropdown": "Dropdown",
"field-type-checkbox": "Checkbox",
"field-edit": "Bearbeiten",
"field-reorder": "Umordnen",
"field-add": "Erstellen"
},
"edit-type-dropdown-dialog": {
"delete-option-title": "Option löschen",
"delete-option-message": "Wollen Sie die Option „{{value}}“ wirklich löschen?",
"delete-option-button": "Löschen",
"unique-value-error": "Mehrere Optionen können nicht den selben Wert haben.",
"no-options-error": "Felder mit Attributkontrolle müssen mindestens eine Option haben.",
"title-checkbox": "Checkbox-Feld bearbeiten",
"title-dropdown": "Dropdown-Feld bearbeiten",
"ok-button": "OK",
"control": "Attributkontrolle",
"control-interpolation-marker": "des Markers",
"control-interpolation-line": "der Linie",
"control-colour": "Farbe {{type}} kontrollieren",
"control-size": "Größe {{type}} kontrollieren",
"control-icon": "Symbol {{type}} kontrollieren",
"control-shape": "Form {{type}} kontrollieren",
"control-width": "Dicke {{type}} kontrollieren",
"control-stroke": "Kontur {{type}} kontrollieren",
"option": "Option",
"label": "Bezeichnung (für Legende)",
"colour": "Farbe",
"size": "Größe",
"icon": "Symbol",
"shape": "Form",
"width": "Dicke",
"stroke": "Kontur",
"option-remove": "Löschen",
"option-reorder": "Umordnen",
"option-add": "Erstellen"
},
"history-dialog": {
"loading-error": "Fehler beim Laden der Versionsgeschichte",
"revert-error": "Fehler beim Rückgängigmachen der Änderung",
"title": "Versionsgeschichte",
"introduction": "Hier können Sie die letzten 50 Änderungen der Karte inspizieren und rückgängig machen.",
"date": "Zeit",
"action": "Änderung",
"restore": "Wiederherstellen",
"diff-field": "Attribut",
"diff-before": "Vorher",
"diff-after": "Nachher"
},
"history-utils": {
"description-create-marker": "Marker {{id}} {{quotedName}} erstellt",
"description-create-line": "Linie {{id}} {{quotedName}} erstellt",
"description-create-view": "Ansicht {{id}} {{quotedName}} erstellt",
"description-create-type": "Objekttyp {{id}} {{quotedName}} erstellt",
"description-update-map": "Karteneinstellungen geändert",
"description-update-marker": "Marker {{id}} {{quotedName}} geändert",
"description-update-line": "Linie {{id}} {{quotedName}} geändert",
"description-update-view": "Ansicht {{id}} {{quotedName}} geändert",
"description-update-type": "Objekttyp {{id}} {{quotedName}} geändert",
"description-delete-marker": "Marker {{id}} {{quotedName}} gelöscht",
"description-delete-line": "Linie {{id}} {{quotedName}} gelöscht",
"description-delete-view": "Ansicht {{id}} {{quotedName}} gelöscht",
"description-delete-type": "Objekttyp {{id}} {{quotedName}} gelöscht",
"description-interpolation-quotedName": "„{{name}}“",
"description-interpolation-quotedName-renamed": "{{quotedBefore}} (neu: {{quotedAfter}})",
"revert-create": "Löschen",
"revert-create-marker-title": "Marker löschen",
"revert-create-line-title": "Linie löschen",
"revert-create-view-title": "Ansicht löschen",
"revert-create-type-title": "Typ löschen",
"revert-create-marker-message-named": "Wollen Sie den Marker {{quotedName}} wirklich löschen?",
"revert-create-marker-message-unnamed": "Wollen Sie diesen Marker wirklich löschen?",
"revert-create-line-message-named": "Wollen Sie die Linie {{quotedName}} wirklich löschen?",
"revert-create-line-message-unnamed": "Wollen Sie diese Linie wirklich löschen?",
"revert-create-view-message-named": "Wollen Sie die Ansicht {{quotedName}} wirklich löschen?",
"revert-create-view-message-unnamed": "Wollen Sie diese Ansicht wirklich löschen?",
"revert-create-type-message-named": "Wollen Sie den Objekttyp {{quotedName}} wirklich löschen?",
"revert-create-type-message-unnamed": "Wollen Sie diesen Objekttyp wirklich löschen?",
"revert-create-button": "Löschen",
"revert-update": "Wiederherstellen",
"revert-update-map-title": "Karteneinstellungen wiederherstellen",
"revert-update-marker-title": "Marker wiederherstellen",
"revert-update-line-title": "Linie wiederherstellen",
"revert-update-view-title": "Ansicht wiederherstellen",
"revert-update-type-title": "Objekttyp wiederherstellen",
"revert-update-map-message": "Wollen Sie die alte Version der Karteneinstellungen wirklich wiederherstellen?",
"revert-update-marker-message-named": "Wollen Sie die alte Version des Markers {{quotedName}} wirklich wiederherstellen?",
"revert-update-marker-message-unnamed": "Wollen Sie die alte Version dieses Markers wirklich wiederherstellen?",
"revert-update-line-message-named": "Wollen Sie die alte Version der Linie {{quotedName}} wirklich wiederherstellen?",
"revert-update-line-message-unnamed": "Wollen Sie die alte Version dieser Linie wirklich wiederherstellen?",
"revert-update-view-message-named": "Wollen Sie die alte Version der Ansicht {{quotedName}} wirklich wiederherstellen?",
"revert-update-view-message-unnamed": "Wollen Sie die alte Version dieser Ansicht wirklich wiederherstellen?",
"revert-update-type-message-named": "Wollen Sie die alte Version des Objekttyps {{quotedName}} wirklich wiederherstellen?",
"revert-update-type-message-unnamed": "Wollen Sie die alte Version dieses Objekttyps wirklich wiederherstellen?",
"revert-update-button": "Wiederherstellen",
"revert-delete": "Wiederherstellen",
"revert-delete-marker-title": "Marker wiederherstellen",
"revert-delete-line-title": "Linie wiederherstellen",
"revert-delete-view-title": "Ansicht wiederherstellen",
"revert-delete-type-title": "Objekttype wiederherstellen",
"revert-delete-marker-message-named": "Wollen Sie die alte Version des Markers {{quotedName}} wirklich wiederherstellen?",
"revert-delete-marker-message-unnamed": "Wollen Sie die alte Version dieses Markers wirklich wiederherstellen?",
"revert-delete-line-message-named": "Wollen Sie die alte Version der Linie {{quotedName}} wirklich wiederherstellen?",
"revert-delete-line-message-unnamed": "Wollen Sie die alte Version dieser Linie wirklich wiederherstellen?",
"revert-delete-view-message-named": "Wollen Sie die alte Version der Ansicht {{quotedName}} wirklich wiederherstellen?",
"revert-delete-view-message-unnamed": "Wollen Sie die alte Version dieser Ansicht wirklich wiederherstellen?",
"revert-delete-type-message-named": "Wollen Sie die alte Version des Objekttyps {{quotedName}} wirklich wiederherstellen?",
"revert-delete-type-message-unnamed": "Wollen Sie die alte Version dieses Objekttyps wirklich wiederherstellen?",
"revert-delete-button": "Wiederherstellen"
},
"leaflet-map": {
"open-full-size": "{{appName}} als ganze Seite öffnen",
"loading": "Wird geladen…"
},
"leaflet-map-components": {
"pois-too-many-results": "Nicht alle POIs konnten geladen werden, weil zu viele gefunden wurden. Zoom Sie weiter hinein, um alle POIs anzuzeigen.",
"pois-zoom-in": "Zoomen Sie hinein, um die POIs zu laden.",
"pois-error": "Fehler beim Laden der POIs: {{message}}"
},
"legend": {
"tab-label": "Legende"
},
"legend-content": {
"click-explanation": "Klicken Sie, um Objekte dieses Typs zu verstecken/anzuzeigen."
},
"line-info": {
"delete-line-title": "Linie löschen",
"delete-line-message": "Wollen Sie die Linie „{{name}}“ wirklich löschen?",
"delete-line-ok": "Löschen",
"delete-line-error": "Fehler beim Löschen der Linie",
"save-line-error": "Fehler beim Speichern der Linie",
"move-line-title": "Linie bewegen",
"move-line-message": "Benutzen Sie das Routenformular oder ziehen Sie die Linie, um den Routenverlauf zu ändern. Klicken Sie auf „Speichern“, um den neuen Verlauf abzuspeichern.",
"move-line-finish": "Speichern",
"move-line-cancel": "Abbrechen",
"hide-elevation-plot": "Höhenprofil verstecken",
"show-elevation-plot": "Höhenprofil anzeigen",
"distance": "Länge",
"ascent-descent": "Aufstieg/Abstieg",
"zoom-to-object-label": "Zur Linie zoomen",
"edit-data": "Bearbeiten",
"edit-waypoints": "Bewegen",
"delete": "Löschen"
},
"marker-info": {
"delete-marker-title": "Marker löschen",
"delete-marker-message": "Wollen Sie den Marker „{{name}}“ wirklich löschen?",
"delete-marker-ok": "Löschen",
"delete-marker-error": "Fehler beim Löschen des Markers",
"coordinates": "Koordinaten",
"zoom-to-object-label": "Zum Marker zoomen",
"edit-data": "Bearbeiten",
"move": "Bewegen",
"delete": "Löschen"
},
"multiple-info": {
"delete-objects-title_one": "{{count}} Objekt löschen",
"delete-objects-title_other": "{{count}} Objekte löschen",
"delete-objects-message_one": "Wollen Sie wirklich {{count}} Objekt löschen?",
"delete-objects-message_other": "Wollen Sie wirklich {{count}} Objekte löschen?",
"delete-objects-ok": "Löschen",
"delete-objects-error": "Fehler beim Löschen der Objekte",
"zoom-to-object": "Zum Objekt zoomen",
"zoom": "Zoomen",
"show-details": "Details anzeigen",
"details": "Details",
"zoom-to-object-label": "Zur Auswahl zoomen",
"delete": "Löschen"
},
"modal-dialog": {
"close": "Schließen",
"cancel": "Abbrechen",
"save": "Speichern"
},
"overpass-form": {
"filter": "Filtern…",
"custom-explanation-1": "Geben Sie hier eine {{statement}} ein. Die Einstellungen und das {{out}}-Ausgabeformat werden im Hintergrund automatisch gesetzt. Für Wege und Relationen werden statt Linien oder Polygonen Marker in der geometrischen Mitte angezeigt.",
"custom-explanation-1-interpolation-statement": "Overpass-Query-Anweisung",
"custom-explanation-1-interpolation-statement-url": "https://wiki.openstreetmap.org/wiki/DE:Overpass_API/Overpass_QL#Query-Anweisung",
"custom-explanation-2": "Beispielanweisungen sind {{parking}}, um Kfz-Parkplätze anzuzeigen und {{atm}} für Geldautomaten.",
"custom-query": "Benutzerdefinierte Anweisung"
},
"overpass-form-tab": {
"pois": "POIs"
},
"overpass-info-tab": {
"tab-label_one": "{{count}} POI",
"tab-label_other": "{{count}} POIs"
},
"overpass-info": {
"coordinates": "Koordinaten",
"zoom-to-object-label": "Zum POI zoomen"
},
"overpass-multiple-info": {
"zoom-to-object": "Zum POI zoomen",
"zoom": "Zoomen",
"show-details": "Details anzeigen",
"details": "Details",
"zoom-to-object-label": "Zur Auswahl zoomen"
},
"pad-id-edit": {
"unique-id-error": "Die selbe ID kann nicht für unterschiedliche Zugangsrechte verwendet werden."
},
"pad-settings-dialog": {
"create-map-error": "Fehler beim Erstellen der Karte",
"save-map-error": "Fehler beim Speichern der Karte",
"delete-map-title": "Karte löschen",
"delete-map-message": "Wollen Sie die Karte „{{name}}“ wirklich löschen? Gelöschte Karten können nicht wiederhergestellt werden!",
"delete-map-ok": "Löschen",
"delete-map-error": "Fehler beim Löschen der Karte",
"title-create": "Neue Karte erstellen",
"title-edit": "Karteneigenschaften",
"create-button": "Erstellen",
"admin-link-label": "Admin-Link",
"admin-link-description": "Wenn die Karte über diesen Link geöffnet wird, können alle Aspekte der Karte bearbeitet werden, inklusive der Einstellungen, Objekttypen und Ansichten.",
"write-link-label": "Schreib-Link",
"write-link-description": "Wenn die Karte über diesen Link geöffnet wird, können Marker und Linien erstellt, bearbeitet und gelöscht werden, die Karteneinstellungen, Objekttypen und Ansichten können jedoch nicht verändert werden.",
"read-link-label": "Lese-Link",
"read-link-description": "Wenn die Karte über diesen Link geöffnet wird, können Marker, Linien und Ansichten gesehen aber nichts verändert werden.",
"map-name": "Name der Karte",
"search-engines": "Suchmaschinen",
"search-engines-label": "Suchmaschinenzugriff erlauben",
"search-engines-description": "Wenn dies aktiviert ist, dürfen Suchmaschinen wie Google den Lese-Link der Karte in ihren Suchergebnissen auffindbar machen.",
"map-description": "Beschreibung der Karte",
"map-description-description": "Diese Beschreibung wird in Suchmaschinen für diese Karte angezeigt.",
"cluster-markers": "Marker bündeln",
"cluster-markers-label": "Marker bündeln",
"cluster-markers-description": "Wenn dies aktiviert ist, werden Marker, die nah beieinander sind, in niedrigen Zoomstufen durch einen Gruppenplatzhalter ersetzt. Das verbessert die Performance auf Karten mit vielen Markern.",
"legend-text": "Text in der Legende",
"legend-text-description": "Dieser Text wird über und unter der Legende angezeigt. Der Text kann mit {{markdown}} formatiert werden.",
"legend-text-description-interpolation-markdown": "Markdown",
"delete-map": "Karte löschen",
"delete-map-button": "Karte löschen",
"delete-description": "Um die Karte zu löschen, tippen Sie {{code}} in das Feld und klicken Sie „Karte löschen“.",
"delete-code": "LÖSCHEN"
},
"user-preferences-dialog": {
"title": "Benutzereinstellungen",
"introduction": "Diese Einstellungen werden als Cookies auf Ihrem Computer gespeichert und werden unabhängig von der geöffneten Karte angewendet.",
"language": "Sprache",
"language-description": "Manche Übersetzungen sind möglicherweise noch unvollständig. Die Übersetzungen werden gemeinschaftlich erstellt, Sie können sich gerne auf {{weblate}} daran beteiligen.",
"language-description-interpolation-weblate": "Weblate",
"units": "Einheiten",
"units-metric": "Metrisch",
"units-us": "US customary (Meilen und Füße)"
},
"route-form": {
"route-description-outer": "Route von {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
"route-description-inner-joiner": " nach ",
"find-destination-error": "Fehler bei der Suche nach dem Wegpunkt „{{query}}“",
"some-destinations-not-found": "Manche Wegpunkte konnten nicht gefunden werden.",
"route-calculation-error": "Error calculating route",
"reorder-alt": "Umordnen",
"from-placeholder": "Von",
"to-placeholder": "Nach",
"via-placeholder": "Über",
"zoom-alt": "Zoomen",
"remove-destination-tooltip": "Diesen Wegpunkt entfernen",
"remove-destination-alt": "Entfernen",
"add-destination-tooltip": "Weiteren Wegpunkt hinzufügen",
"add-destination-alt": "Hinzufügen",
"submit": "Los!",
"clear-route-tooltip": "Route ausblenden",
"clear-route-alt": "Ausblenden",
"distance": "Länge",
"ascent-descent": "Aufstieg/Abstieg",
"zoom-to-object-label": "Zur Route zoomen",
"export-filename": "FacilMap-Route"
},
"route-form-tab": {
"tab-label": "Route"
},
"route-mode": {
"car-alt": "Auto",
"bicycle-alt": "Fahrrad",
"pedestrian-alt": "Zu Fuß",
"straight-alt": "Luftlinie",
"car-title": "Mit dem Auto",
"bicycle-title": "Mit dem Fahrrad",
"pedestrian-title": "Zu Fuß",
"straight-title": "Luftlinie",
"car": "Auto",
"hgv": "LKW",
"bicycle": "Fahrrad",
"road-bike": "Rennrad",
"mountain-bike": "Mountainbike",
"electric-bike": "Pedelec",
"walking": "Zu Fuß",
"hiking": "Wandern",
"wheelchair": "Rollstuhl",
"straight": "Luftlinie",
"fastest": "Schnellste Route",
"shortest": "Kürzteste Route",
"avoid-highways": "Autobahnen vermeiden",
"avoid-toll-roads": "Mautstraßen vermeiden",
"avoid-ferries": "Fähren vermeiden",
"avoid-fords": "Furten vermeiden",
"avoid-steps": "Treppen vermeiden",
"custom-alt": "Benutzerdefiniert",
"load-details": "Details laden (Höhenprofil, Straßenqualität, …)"
},
"search-box": {
"close-alt": "Schließen",
"resize-tooltip": "Größe durch Ziehen verändern, durch Klick zurücksetzen"
},
"search-form": {
"search-description": "Suche nach {{query}}",
"search-error": "Fehler bei der Suche",
"search-alt": "Suchen",
"clear-alt": "Verstecken",
"auto-zoom": "Automatisch zu den Ergebnissen zoomen",
"zoom-to-all": "Zur Übersicht aller Ergebnisse zoomen"
},
"search-form-tab": {
"tab-label": "Suche"
},
"search-results": {
"no-results": "Es wurden keine Ergebnisse gefunden.",
"zoom-to-result-tooltip": "Zum Suchergebnis zoomen",
"zoom-to-result-alt": "Zoomen",
"show-details-tooltip": "Details anzeigen",
"show-details-alt": "Details",
"select-all": "Alle auswählen",
"add-to-map-label": "Zur Karte hinzufügen",
"custom-type-mapping": "Benutzerdefiniert…"
},
"toasts": {
"unexpected-error": "Unerwarteter Fehler",
"close-label": "Schließen",
"spinner-label": "Lädt…"
},
"toolbox-add-dropdown": {
"label": "Erstellen",
"manage-types": "Objekttypen verwalten"
},
"toolbox-collab-maps-dropdown": {
"label": "Kollaborative Karten",
"bookmark": "Karte „{{padName}}“ als Favoriten hinzufügen",
"manage-bookmarks": "Favoriten verwalten",
"create-map": "Neue Karte erstellen",
"open-map": "Existierende Karte öffnen",
"open-other-map": "Andere Karte öffnen",
"close-map": "Karte „{{padName}}“ schließen"
},
"toolbox-help-dropdown": {
"label": "Hilfe",
"documentation": "Benutzerhandbuch",
"matrix-chat": "Matrix-Chat",
"bugtracker": "Fehler melden",
"forum": "Frage stellen",
"about": "Über {{appName}}"
},
"toolbox-map-style-dropdown": {
"label": "Kartenstil",
"openstreetmap": "OpenStreetMap",
"google-maps": "Google Maps",
"google-maps-satellite": "Google Maps (Satellit)",
"bing-maps": "Bing Maps"
},
"toolbox-tools-dropdown": {
"label": "Werkzeuge",
"share": "Teilen",
"open-file": "Datei öffnen",
"export": "Exportieren",
"filter": "Filter",
"settings": "Eigenschaften",
"history": "Versionsgeschichte",
"user-preferences": "Benutzereinstellungen"
},
"toolbox-views-dropdown": {
"label": "Ansichten",
"save-current-view": "Ansicht speichern",
"manage-views": "Ansichten verwalten"
},
"validated-field": {
"validation-error": "Fehler bei der Validierung des Formularfelds"
},
"zoom-to-object-button": {
"fallback-label": "Zum Objekt zoomen"
}
}

Wyświetl plik

@ -1,32 +0,0 @@
const messagesDe = {
"about-dialog": {
"header": `Über FacilMap {{version}}`,
"license-text": `{{facilmap}} is unter der {{license}} verfügbar.`,
"license-text-facilmap": `FacilMap`,
"license-text-license": `GNU Affero General Public License, Version 3`,
"issues-text": `Bitte melden Sie Fehler und Verbesserungsvorschläge auf {{tracker}}.`,
"issues-text-tracker": `GitHub`,
"help-text": `Wenn Sie Fragen haben, schauen Sie sich die {{documentation}} an, schreiben Sie ins {{discussions}} oder fragen im {{chat}}.`,
"help-text-documentation": `Dokumentation`,
"help-text-discussions": `Forum`,
"help-text-chat": `Matrix-Chat`,
"privacy-information": `Informationen zum Datenschutz`,
"map-data": `Kartendaten`,
"map-data-search": `Suche`,
"map-data-pois": `POIs`,
"map-data-directions": `Routenberechnung`,
"map-data-geoip": `GeoIP`,
"map-data-geoip-description": `Dieses Produkt enthält GeoLine2-Daten von Maxmind, verfügbar unter {{maxmind}}.`,
"attribution-osm-contributors": `OSM-Mitwirkende`,
"programs-libraries": `Programme/Bibliotheken`,
"icons": `Symbole`
},
"modal-dialog": {
"close": "Schließen",
"cancel": "Abbrechen",
"save": "Speichern"
}
};
export default messagesDe;

Wyświetl plik

@ -0,0 +1,539 @@
{
"about-dialog": {
"header": "About FacilMap {{version}}",
"license-text": "{{facilmap}} is available under the {{license}}.",
"license-text-facilmap": "FacilMap",
"license-text-license": "GNU Affero General Public License, Version 3",
"issues-text": "If something does not work or you have a suggestion for improvement, please report on the {{tracker}}.",
"issues-text-tracker": "issue tracker",
"help-text": "If you have a question, please have a look at the {{documentation}}, raise a question in the {{discussions}} or ask in the {{chat}}.",
"help-text-documentation": "documentation",
"help-text-discussions": "discussion forum",
"help-text-chat": "Matrix chat",
"privacy-information": "Privacy information",
"map-data": "Map data",
"map-data-search": "Search",
"map-data-pois": "POIs",
"map-data-directions": "Directions",
"map-data-geoip": "GeoIP",
"map-data-geoip-description": "This product includes GeoLite2 data created by MaxMind, available from {{maxmind}}.",
"attribution-osm-contributors": "OSM Contributors",
"programs-libraries": "Programs/libraries",
"icons": "Icons"
},
"add-to-map-dropdown": {
"fallback-label": "Add to map",
"add-error": "Error adding to map",
"add-marker-items": "Marker items as {{typeName}}",
"add-line-items": "Line/polygon items as {{typeName}}"
},
"alert": {
"fallback-ok-label": "OK"
},
"click-marker-tab": {
"look-up-error": "Error looking up point"
},
"client-provider": {
"loading-map-header": "Loading",
"loading-map": "Loading map…",
"connecting-header": "Connecting",
"connecting": "Connecting to server…",
"map-deleted-header": "Map deleted",
"map-deleted": "This map has been deleted.",
"close-map": "Close map",
"connection-error": "Error connecting to server",
"open-map-error": "Error opening map",
"disconnected-header": "Disconnected",
"disconnected": "The connection to the server was lost. Trying to reconnect…"
},
"colour-picker": {
"format-error": "Needs to be in 6-digit hex format, for example 0000ff."
},
"coordinates": {
"copied-title": "Coordinates copied",
"copied-message": "The coordinates were copied to the clipboard.",
"copy-to-clipboard": "Copy to clipboard",
"elevation": "Elevation"
},
"copy-to-clipboard-input": {
"copied-fallback-title": "Link copied",
"copied-fallback-message": "The link was copied to the clipboard.",
"copy": "Copy",
"qr-code-alt": "QR code",
"qr-code-tooltip": "Show QR code"
},
"custom-import-dialog": {
"existing-type": "Existing type “{{name}}”",
"import-type": "Import type “{{name}}”",
"no-import": "Do not import",
"import-error": "Error importing to map",
"dialog-title": "Custom Import",
"ok-label": "Import",
"type": "Type",
"map-to": "Map to…",
"markers_one": "Marker of type {{typeName}} ({{count}})",
"markers_other": "Markers of type {{typeName}} ({{count}})",
"lines_one": "Line of type {{typeName}} ({{count}})",
"lines_other": "Lines of type {{typeName}} ({{count}})",
"untyped-markers_one": "Untyped marker ({{count}})",
"untyped-markers_other": "Untyped markers ({{count}})",
"untyped-lines_one": "Untyped line/polygon ({{count}})",
"untyped-lines_other": "Untyped lines/polygons ({{count}})"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Apply",
"introduction": "Here you can set an advanced expression to show/hide certain markers/lines based on their attributes. The filter expression only applies to your view of the map, but it can be persisted as part of a saved view or a shared link.",
"syntax-header": "Syntax",
"variable": "Variable",
"operator": "Operator",
"description": "Description",
"example": "Example",
"name-description": "Marker/Line name",
"type-description": "{{marker}} / {{line}}",
"typeId-description": "{{items}})",
"typeId-description-item": "{{typeId}} ({{typeName}})",
"typeId-description-separator": " / ",
"data-description-1": "Field values (example: {{example1}} or {{example2}}).",
"data-description-2": "For checkbox fields, the value is {{uncheckedValue}} (unchecked) or {{checkedValue}} (checked).",
"lon-lat-description": "Marker coordinates",
"colour-description": "Marker/line colour",
"size-description": "Marker size",
"symbol-description": "Marker icon",
"shape-description": "Marker shape",
"ele-description": "Marker elevation",
"mode-description": "Line routing mode ({{straight}} / {{car}} / {{bicycle}} / {{pedestrian}} / {{track}})",
"width-description": "Line width",
"stroke-description": "Line stroke ({{solid}} (solid) / {{dashed}} / {{dotted}})",
"distance-description": "Line distance in kilometers",
"time-description": "Line routing time in seconds",
"ascent-descent-description": "Total climb/drop of line",
"routePoints-description": "Line point coordinates",
"number-description": "Numerical value",
"text-description": "Text value",
"mathematical-description": "Mathematical operations ({{modulo}}: modulo, {{power}}: power)",
"logical-description": "Logical operators",
"ternary-description": "if/then/else operator",
"comparison-description": "Comparison ({{notEqual}}: not equal) (case sensitive)",
"list-description": "List operator (case sensitive)",
"regexp-description": "Regular expression match (case sensitive)",
"lower-description": "Convert to lower case",
"round-description": "Round ({{ceil}}: up, {{floor}}: down)",
"functions-description": "Mathematical functions",
"min-max-description": "Smallest/highest value"
},
"edit-type-dialog": {
"delete-field-title": "Delete field",
"delete-field-message": "Do you really want to delete the field “{{fieldName}}”?",
"delete-field-button": "Delete",
"create-type-error": "Error creating type",
"save-type-error": "Error saving type",
"field-update-error": "Error updating field",
"field-disappeared-error": "The field cannot be found on the type anymore.",
"unique-field-name-error": "Multiple fields cannot have the same name.",
"title": "Edit Type",
"name": "Name",
"type": "Type",
"type-marker": "Marker",
"type-line": "Line",
"styles-introduction": "These styles are applied when a new object of this type is created. If “Fixed” is enabled, the style is applied to all objects of this type and cannot be changed for an individual object anymore. For more complex style control, dropdown or checkbox fields can be configured below to change the style based on their selected value.",
"default-colour": "Default colour",
"fixed": "Fixed",
"default-size": "Default size",
"default-icon": "Default icon",
"default-shape": "Default shape",
"default-width": "Default width",
"default-stroke": "Default stroke",
"default-route-mode": "Default routing mode",
"legend": "Legend",
"show-in-legend": "Show in legend",
"show-in-legend-description": "An item for this type will be shown in the legend. Any fixed style attributes are applied to it. Dropdown or checkbox fields that control the style generate additional legend items.",
"fields": "Fields",
"field-name": "Name",
"field-type": "Type",
"field-default-value": "Default value",
"field-delete": "Delete",
"field-type-input": "Text field",
"field-type-textarea": "Text area",
"field-type-dropdown": "Dropdown",
"field-type-checkbox": "Checkbox",
"field-edit": "Edit",
"field-reorder": "Reorder",
"field-add": "Add"
},
"edit-type-dropdown-dialog": {
"delete-option-title": "Delete option",
"delete-option-message": "Do you really want to delete the option “{{value}}”?",
"delete-option-button": "Delete",
"unique-value-error": "Multiple options cannot have the same label.",
"no-options-error": "Controlling fields need to have at least one option.",
"title-checkbox": "Edit Checkbox",
"title-dropdown": "Edit Dropdown",
"ok-button": "OK",
"control": "Control",
"control-interpolation-marker": "marker",
"control-interpolation-line": "line",
"control-colour": "Control {{type}} colour",
"control-size": "Control {{type}} size",
"control-icon": "Control {{type}} icon",
"control-shape": "Control {{type}} shape",
"control-width": "Control {{type}} width",
"control-stroke": "Control {{type}} stroke",
"option": "Option",
"label": "Label (for legend)",
"colour": "Colour",
"size": "Size",
"icon": "Icon",
"shape": "Shape",
"width": "Width",
"stroke": "Stroke",
"option-remove": "Remove",
"option-reorder": "Reorder",
"option-add": "Add"
},
"history-dialog": {
"loading-error": "Error loading history",
"revert-error": "Error reverting history entry",
"title": "History",
"introduction": "Here you can inspect and revert the last 50 changes to the map.",
"date": "Date",
"action": "Action",
"restore": "Restore",
"diff-field": "Field",
"diff-before": "Before",
"diff-after": "After"
},
"history-utils": {
"description-create-marker": "Created marker {{id}} {{quotedName}}",
"description-create-line": "Created line {{id}} {{quotedName}}",
"description-create-view": "Created view {{id}} {{quotedName}}",
"description-create-type": "Created type {{id}} {{quotedName}}",
"description-update-map": "Changed map settings",
"description-update-marker": "Changed marker {{id}} {{quotedName}}",
"description-update-line": "Changed line {{id}} {{quotedName}}",
"description-update-view": "Changed view {{id}} {{quotedName}}",
"description-update-type": "Changed type {{id}} {{quotedName}}",
"description-delete-marker": "Deleted marker {{id}} {{quotedName}}",
"description-delete-line": "Deleted line {{id}} {{quotedName}}",
"description-delete-view": "Deleted view {{id}} {{quotedName}}",
"description-delete-type": "Deleted type {{id}} {{quotedName}}",
"description-interpolation-quotedName": "“{{name}}”",
"description-interpolation-quotedName-renamed": "{{quotedBefore}} (new name: {{quotedAfter}})",
"revert-create": "Revert (delete)",
"revert-create-marker-title": "Delete marker",
"revert-create-line-title": "Delete line",
"revert-create-view-title": "Delete view",
"revert-create-type-title": "Delete type",
"revert-create-marker-message-named": "Do you really want to delete the marker {{quotedName}}?",
"revert-create-marker-message-unnamed": "Do you really want to delete this marker?",
"revert-create-line-message-named": "Do you really want to delete the line {{quotedName}}?",
"revert-create-line-message-unnamed": "Do you really want to delete this line?",
"revert-create-view-message-named": "Do you really want to delete the view {{quotedName}}?",
"revert-create-view-message-unnamed": "Do you really want to delete this view?",
"revert-create-type-message-named": "Do you really want to delete the type {{quotedName}}?",
"revert-create-type-message-unnamed": "Do you really want to delete this type?",
"revert-create-button": "Delete",
"revert-update": "Revert",
"revert-update-map-title": "Revert map settings",
"revert-update-marker-title": "Revert marker",
"revert-update-line-title": "Revert line",
"revert-update-view-title": "Revert view",
"revert-update-type-title": "Revert type",
"revert-update-map-message": "Do you really want to restore the old version of the map settings?",
"revert-update-marker-message-named": "Do you really want to restore the old version of the marker {{quotedName}}?",
"revert-update-marker-message-unnamed": "Do you really want to restore the old version of this marker?",
"revert-update-line-message-named": "Do you really want to restore the old version of the line {{quotedName}}?",
"revert-update-line-message-unnamed": "Do you really want to restore the old version of this line?",
"revert-update-view-message-named": "Do you really want to restore the old version of the view {{quotedName}}?",
"revert-update-view-message-unnamed": "Do you really want to restore the old version of this view?",
"revert-update-type-message-named": "Do you really want to restore the old version of the type {{quotedName}}?",
"revert-update-type-message-unnamed": "Do you really want to restore the old version of this type?",
"revert-update-button": "Revert",
"revert-delete": "Restore",
"revert-delete-marker-title": "Restore marker",
"revert-delete-line-title": "Restore line",
"revert-delete-view-title": "Restore view",
"revert-delete-type-title": "Restore type",
"revert-delete-marker-message-named": "Do you really want to restore the marker {{quotedName}}?",
"revert-delete-marker-message-unnamed": "Do you really want to restore this marker?",
"revert-delete-line-message-named": "Do you really want to restore the line {{quotedName}}?",
"revert-delete-line-message-unnamed": "Do you really want to restore this line?",
"revert-delete-view-message-named": "Do you really want to restore the view {{quotedName}}?",
"revert-delete-view-message-unnamed": "Do you really want to restore this view?",
"revert-delete-type-message-named": "Do you really want to restore the type {{quotedName}}?",
"revert-delete-type-message-unnamed": "Do you really want to restore this type?",
"revert-delete-button": "Restore"
},
"leaflet-map": {
"open-full-size": "Open {{appName}} in full size",
"loading": "Loading…"
},
"leaflet-map-components": {
"pois-too-many-results": "Not all POIs are shown because there are too many results. Zoom in to show all results.",
"pois-zoom-in": "Zoom in to show POIs.",
"pois-error": "Error loading POIs: {{message}}"
},
"legend": {
"tab-label": "Legend"
},
"legend-content": {
"click-explanation": "Click to show/hide objects of this type."
},
"line-info": {
"delete-line-title": "Delete line",
"delete-line-message": "Do you really want to delete the line “{{name}}”?",
"delete-line-ok": "Delete",
"delete-line-error": "Error deleting line",
"save-line-error": "Error saving line",
"move-line-title": "Edit waypoints",
"move-line-message": "Use the routing form or drag the line around to change it. Click “Finish” to save the changes.",
"move-line-finish": "Finish",
"move-line-cancel": "Cancel",
"hide-elevation-plot": "Hide elevation plot",
"show-elevation-plot": "Show elevation plot",
"distance": "Distance",
"ascent-descent": "Climb/drop",
"zoom-to-object-label": "Zoom to line",
"edit-data": "Edit data",
"edit-waypoints": "Edit waypoints",
"delete": "Delete"
},
"marker-info": {
"delete-marker-title": "Delete marker",
"delete-marker-message": "Do you really want to delete the marker “{{name}}”?",
"delete-marker-ok": "Delete",
"delete-marker-error": "Error deleting marker",
"coordinates": "Coordinates",
"zoom-to-object-label": "Zoom to marker",
"edit-data": "Edit data",
"move": "Move",
"delete": "Delete"
},
"multiple-info": {
"delete-objects-title_one": "Delete {{count}} object",
"delete-objects-title_other": "Delete {{count}} objects",
"delete-objects-message_one": "Do you really want to remove {{count}} object?",
"delete-objects-message_other": "Do you really want to remove {{count}} objects?",
"delete-objects-ok": "Delete",
"delete-objects-error": "Error deleting objects",
"zoom-to-object": "Zoom to object",
"zoom": "Zoom",
"show-details": "Show details",
"details": "Details",
"zoom-to-object-label": "Zoom to selection",
"delete": "Delete"
},
"modal-dialog": {
"close": "Close",
"cancel": "Cancel",
"save": "Save"
},
"overpass-form": {
"filter": "Filter…",
"custom-explanation-1": "Enter an {{statement}} here. Settings and an {{out}} statement are added automatically in the background. For ways and relations, a marker will be shown at the geometric centre, no lines or polygons are drawn.",
"custom-explanation-1-interpolation-statement": "Overpass query statement",
"custom-explanation-1-interpolation-statement-url": "https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#The_Query_Statement",
"custom-explanation-2": "Example queries are {{parking}} to get parking places or {{atm}} for ATMs.",
"custom-query": "Custom query"
},
"overpass-form-tab": {
"pois": "POIs"
},
"overpass-info-tab": {
"tab-label_one": "{{count}} POI",
"tab-label_other": "{{count}} POIs"
},
"overpass-info": {
"coordinates": "Coordinates",
"zoom-to-object-label": "Zoom to POI"
},
"overpass-multiple-info": {
"zoom-to-object": "Zoom to object",
"zoom": "Zoom",
"show-details": "Show details",
"details": "Details",
"zoom-to-object-label": "Zoom to selection"
},
"pad-id-edit": {
"unique-id-error": "The same link cannot be used for different access levels."
},
"pad-settings-dialog": {
"create-map-error": "Error creating map",
"save-map-error": "Error saving map settings",
"delete-map-title": "Delete map",
"delete-map-message": "Are you sure you want to delete the map “{{name}}”? Deleted maps cannot be restored!",
"delete-map-ok": "Delete map",
"delete-map-error": "Error deleting map",
"title-create": "Create collaborative map",
"title-edit": "Map settings",
"create-button": "Create",
"admin-link-label": "Admin link",
"admin-link-description": "When opening the map through this link, all parts of the map can be edited, including the map settings, object types and views.",
"write-link-label": "Editable link",
"write-link-description": "When opening the map through this link, markers and lines can be added, changed and deleted, but the map settings, object types and views cannot be modified.",
"read-link-label": "Read-only link",
"read-link-description": "When opening the map through this link, markers, lines and views can be seen, but nothing can be changed.",
"map-name": "Map name",
"search-engines": "Search engines",
"search-engines-label": "Accessible for search engines",
"search-engines-description": "If this is enabled, search engines like Google will be allowed to add the read-only version of this map.",
"map-description": "Short description",
"map-description-description": "This description will be shown under the result in search engines.",
"cluster-markers": "Cluster markers",
"cluster-markers-label": "Cluster markers",
"cluster-markers-description": "If enabled, when there are many markers in one area, they will be replaced by a placeholder at low zoom levels. This improves performance on maps with many markers.",
"legend-text": "Legend text",
"legend-text-description": "Text that will be shown above and below the legend. Can be formatted with {{markdown}}.",
"legend-text-description-interpolation-markdown": "Markdown",
"delete-map": "Delete map",
"delete-map-button": "Delete map",
"delete-description": "To delete this map, type {{code}} into the field and click the “Delete map” button.",
"delete-code": "DELETE"
},
"user-preferences-dialog": {
"title": "User preferences",
"introduction": "These settings are stored on your computer as a cookie and are applied independently of the opened map.",
"language": "Language",
"language-description": "Some translations may not be complete yet. Translations are created by the community, feel free to contribute on {{weblate}}.",
"language-description-interpolation-weblate": "Weblate",
"units": "Units",
"units-metric": "Metric",
"units-us": "US customary (miles, feet)"
},
"route-form": {
"route-description-outer": "Route from {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
"route-description-inner-joiner": " to ",
"find-destination-error": "Error finding destination “{{query}}”",
"some-destinations-not-found": "Some destinations could not be found.",
"route-calculation-error": "Error calculating route",
"reorder-alt": "Reorder",
"from-placeholder": "From",
"to-placeholder": "To",
"via-placeholder": "Via",
"zoom-alt": "Zoom",
"remove-destination-tooltip": "Remove this destination",
"remove-destination-alt": "Remove",
"add-destination-tooltip": "Add another destination",
"add-destination-alt": "Add",
"submit": "Go!",
"clear-route-tooltip": "Clear route",
"clear-route-alt": "Clear",
"distance": "Distance",
"ascent-descent": "Climb/drop",
"zoom-to-object-label": "Zoom to route",
"export-filename": "FacilMap route"
},
"route-form-tab": {
"tab-label": "Route"
},
"route-mode": {
"car-alt": "Car",
"bicycle-alt": "Bicycle",
"pedestrian-alt": "Foot",
"straight-alt": "Straight",
"car-title": "Go by car",
"bicycle-title": "Go by bicycle",
"pedestrian-title": "Go on foot",
"straight-title": "Go in a straight line",
"car": "Car",
"hgv": "HGV",
"bicycle": "Bicycle",
"road-bike": "Road bike",
"mountain-bike": "Mountain bike",
"electric-bike": "Electric bike",
"walking": "Walking",
"hiking": "Hiking",
"wheelchair": "Wheelchair",
"straight": "Straight line",
"fastest": "Fastest",
"shortest": "Shortest",
"avoid-highways": "Avoid highways",
"avoid-toll-roads": "Avoid toll roads",
"avoid-ferries": "Avoid ferries",
"avoid-fords": "Avoid fords",
"avoid-steps": "Avoid steps",
"custom-alt": "Custom",
"load-details": "Load route details (elevation, road types, …)"
},
"search-box": {
"close-alt": "Close",
"resize-tooltip": "Drag to resize, click to reset"
},
"search-form": {
"search-description": "Search for {{query}}",
"search-error": "Search error",
"search-alt": "Search",
"clear-alt": "Clear",
"auto-zoom": "Auto-zoom to results",
"zoom-to-all": "Zoom to all results"
},
"search-form-tab": {
"tab-label": "Search"
},
"search-results": {
"no-results": "No results have been found.",
"zoom-to-result-tooltip": "Zoom to result",
"zoom-to-result-alt": "Zoom",
"show-details-tooltip": "Show details",
"show-details-alt": "Details",
"select-all": "Select all",
"add-to-map-label_one": "Add selected item to map",
"add-to-map-label_other": "Add selected items to map",
"custom-type-mapping": "Custom type mapping…"
},
"toasts": {
"unexpected-error": "Unexpected error",
"close-label": "Close",
"spinner-label": "Loading…"
},
"toolbox-add-dropdown": {
"label": "Add",
"manage-types": "Manage types"
},
"toolbox-collab-maps-dropdown": {
"label": "Collaborative maps",
"bookmark": "Bookmark map “{{padName}}”",
"manage-bookmarks": "Manage bookmarks",
"create-map": "Create a new map",
"open-map": "Open an existing map",
"open-other-map": "Open another map",
"close-map": "Close map “{{padName}}”"
},
"toolbox-help-dropdown": {
"label": "Help",
"documentation": "Documentation",
"matrix-chat": "Matrix chat room",
"bugtracker": "Report a problem",
"forum": "Ask a question",
"about": "About {{appName}}"
},
"toolbox-map-style-dropdown": {
"label": "Map style",
"openstreetmap": "OpenStreetMap",
"google-maps": "Google Maps",
"google-maps-satellite": "Google Maps (Satellite)",
"bing-maps": "Bing Maps"
},
"toolbox-tools-dropdown": {
"label": "Tools",
"share": "Share",
"open-file": "Open file",
"export": "Export",
"filter": "Filter",
"settings": "Settings",
"history": "History",
"user-preferences": "User preferences"
},
"toolbox-views-dropdown": {
"label": "Views",
"save-current-view": "Save current view",
"manage-views": "Manage views"
},
"validated-field": {
"validation-error": "Error while validating form field"
},
"zoom-to-object-button": {
"fallback-label": "Zoom to object"
}
}

Wyświetl plik

@ -1,32 +0,0 @@
const messagesEn = {
"about-dialog": {
"header": `About FacilMap {{version}}`,
"license-text": `{{facilmap}} is available under the {{license}}.`,
"license-text-facilmap": `FacilMap`,
"license-text-license": `GNU Affero General Public License, Version 3`,
"issues-text": `If something does not work or you have a suggestion for improvement, please report on the {{tracker}}.`,
"issues-text-tracker": `issue tracker`,
"help-text": `If you have a question, please have a look at the {{documentation}}, raise a question in the {{discussions}} or ask in the {{chat}}.`,
"help-text-documentation": `documentation`,
"help-text-discussions": `discussion forum`,
"help-text-chat": `Matrix chat`,
"privacy-information": `Privacy information`,
"map-data": `Map data`,
"map-data-search": `Search`,
"map-data-pois": `POIs`,
"map-data-directions": `Directions`,
"map-data-geoip": `GeoIP`,
"map-data-geoip-description": `This product includes GeoLite2 data created by MaxMind, available from {{maxmind}}.`,
"attribution-osm-contributors": `OSM Contributors`,
"programs-libraries": `Programs/libraries`,
"icons": `Icons`
},
"modal-dialog": {
"close": "Close",
"cancel": "Cancel",
"save": "Save"
}
};
export default messagesEn;

Wyświetl plik

@ -0,0 +1,62 @@
{
"about-dialog": {
"header": "Om FaciiMap {{version}}",
"license-text-facilmap": "FaciiMap",
"help-text-documentation": "dokumentasjon",
"help-text-discussions": "diskusjonsforum",
"help-text-chat": "Matrix-sludring",
"privacy-information": "Personvernsinfo",
"map-data": "Kartdata",
"map-data-search": "Søk",
"map-data-pois": "Interessepunkter",
"map-data-geoip": "GeoIP",
"attribution-osm-contributors": "OSM-bidragsytere",
"programs-libraries": "Programmer/bibliotek",
"icons": "Ikoner"
},
"client-provider": {
"close-map": "Lukk kart",
"disconnected-header": "Frakoblet",
"map-deleted-header": "Kart slettet"
},
"edit-filter-dialog": {
"description": "Beskrivelse",
"example": "Eksempel",
"typeId-description": "{{items}}",
"apply": "Bruk",
"typeId-description-separator": " / ",
"size-description": "Pekerstørrelse",
"symbol-description": "Pekerikon",
"shape-description": "Pekerform",
"width-description": "Linjebredde"
},
"edit-type-dialog": {
"default-size": "Forvalgt størrelse",
"default-icon": "Forvalkt ikon",
"default-shape": "Forvalgt form",
"default-width": "Forvalgt bredde",
"field-name": "Navn",
"field-default-value": "Forvalgt verdi",
"field-delete": "Slett",
"field-type-input": "Tekstfelt",
"field-type-textarea": "Tekstområde",
"field-type-checkbox": "Avkryssningsboks",
"field-edit": "Rediger",
"field-reorder": "Endre rekkefølge",
"field-add": "Legg til"
},
"edit-type-dropdown-dialog": {
"delete-option-title": "Slett alternativ",
"delete-option-message": "Slett alternativet «{{value}}»?",
"delete-option-button": "Slett"
},
"history-dialog": {
"title": "Historikk",
"date": "Dato",
"action": "Handling",
"restore": "Gjenopprett",
"diff-field": "Felt",
"diff-before": "Før",
"diff-after": "Etter"
}
}

Wyświetl plik

@ -0,0 +1,36 @@
{
"client-provider": {
"connection-error": "Ошибка при подключении к серверу",
"open-map-error": "Ошибка при открытии карты",
"connecting-header": "Подключение",
"map-deleted-header": "Карта удалена",
"close-map": "Закрыть карту",
"disconnected": "Соединение с сервером потеряно. Повторное подключение…",
"loading-map-header": "Загрузка",
"loading-map": "Загрузка карты…",
"connecting": "Подключение к серверу…"
},
"edit-filter-dialog": {
"description": "Описание",
"title": "Фильтр",
"syntax-header": "Синтаксис",
"example": "Пример"
},
"about-dialog": {
"header": "О FacilMap {{version}}",
"license-text": "{{facilmap}} доступен под лицензией {{license}}.",
"license-text-facilmap": "FacilMap",
"license-text-license": "GNU Affero General Public License, Version 3",
"map-data-search": "Поиск",
"attribution-osm-contributors": "Участники OSM",
"help-text-documentation": "документация",
"help-text-chat": "Matrix-чат",
"icons": "Иконки"
},
"route-form-tab": {
"tab-label": "Навигация"
},
"zoom-to-object-button": {
"fallback-label": "Приблизить объект"
}
}

Wyświetl plik

@ -108,6 +108,7 @@
<li><a href="https://github.com/cure53/DOMPurify" target="_blank">DOMPurify</a></li>
<li><a href="https://expressjs.com/" target="_blank">Express</a></li>
<li><a href="https://vuepress.vuejs.org/" target="_blank">Vuepress</a></li>
<li><a href="https://www.i18next.com/" target="_blank">I18next</a></li>
</ul>
<h4>{{t('about-dialog.icons')}}</h4>
<ul>

Wyświetl plik

@ -10,8 +10,10 @@
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import type { WritableClickMarkerTabContext } from "./facil-map-context-provider/click-marker-tab-context";
import { useToasts } from "./ui/toasts/toasts.vue";
import { useI18n } from "../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
const context = injectContextRequired();
const mapContext = requireMapContext(context);
@ -69,7 +71,7 @@
tab.isLoading = false;
})().catch((err) => {
toasts.showErrorToast(`find-error-${tab.id}`, "Error looking up point", err);
toasts.showErrorToast(`find-error-${tab.id}`, i18n.t("click-marker-tab.look-up-error"), err);
});
(async () => {

Wyświetl plik

@ -1,22 +1,24 @@
<script lang="ts">
import { onBeforeUnmount, reactive, ref, toRaw, watch } from "vue";
import { computed, onBeforeUnmount, reactive, ref, toRaw, watch } from "vue";
import Client from "facilmap-client";
import type { PadData, PadId } from "facilmap-types";
import { PadNotFoundError, type PadData, type PadId } from "facilmap-types";
import PadSettingsDialog from "./pad-settings-dialog/pad-settings-dialog.vue";
import storage from "../utils/storage";
import { useToasts } from "./ui/toasts/toasts.vue";
import { type ToastAction } from "./ui/toasts/toasts.vue";
import Toast from "./ui/toasts/toast.vue";
import type { ClientContext } from "./facil-map-context-provider/client-context";
import { injectContextRequired } from "./facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../utils/i18n";
import { getCurrentLanguage, getCurrentUnits } from "facilmap-utils";
function isPadNotFoundError(serverError: Client["serverError"]): boolean {
return !!serverError?.message?.includes("does not exist");
return !!serverError && serverError instanceof PadNotFoundError;
}
</script>
<script setup lang="ts">
const context = injectContextRequired();
const toasts = useToasts();
const i18n = useI18n();
const client = ref<ClientContext>();
const connectingClient = ref<ClientContext>();
@ -42,14 +44,6 @@
if (existingClient && existingClient.server == props.serverUrl && existingClient.padId == props.padId)
return;
toasts.hideToast(`fm${context.id}-client-connecting`);
toasts.hideToast(`fm${context.id}-client-error`);
toasts.hideToast(`fm${context.id}-client-deleted`);
if (props.padId)
toasts.showToast(`fm${context.id}-client-connecting`, "Loading", "Loading map…", { spinner: true, noCloseButton: true });
else
toasts.showToast(`fm${context.id}-client-connecting`, "Connecting", "Connecting to server…", { spinner: true, noCloseButton: true });
class CustomClient extends Client implements ClientContext {
_makeReactive<O extends object>(obj: O) {
return reactive(obj) as O;
@ -64,7 +58,12 @@
}
}
const newClient = new CustomClient(props.serverUrl, props.padId);
const newClient = new CustomClient(props.serverUrl, props.padId, {
query: {
lang: getCurrentLanguage(),
units: getCurrentUnits()
}
});
connectingClient.value = newClient;
let lastPadId: PadId | undefined = undefined;
@ -86,25 +85,6 @@
lastPadData = newClient.padData;
});
newClient.on("deletePad", () => {
toasts.showToast(`fm${context.id}-client-deleted`, "Map deleted", "This map has been deleted.", {
noCloseButton: true,
variant: "danger",
actions: context.settings.interactive ? [
{
label: "Close map",
href: context.baseUrl,
onClick: (e) => {
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
e.preventDefault();
openPad(undefined);
}
}
}
] : []
});
})
await new Promise<void>((resolve) => {
newClient.once(props.padId ? "padData" : "connect", () => { resolve(); });
newClient.on("serverError", () => { resolve(); });
@ -116,39 +96,6 @@
return;
}
// Bootstrap-Vue uses animation frames to show the connecting toast. If the map is loading in a background tab, the toast might not be shown
// yet when we are trying to hide it, so the hide operation is skipped and once the loading toast is shown, it stays forever.
// We need to wait for two animation frames to make sure that the toast is shown.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
toasts.hideToast(`fm${context.id}-client-connecting`);
});
});
if (newClient.serverError && !newClient.isCreatePad) {
if (newClient.disconnected || !props.padId) {
toasts.showErrorToast(`fm${context.id}-client-error`, "Error connecting to server", newClient.serverError, {
noCloseButton: !!props.padId
});
} else {
toasts.showErrorToast(`fm${context.id}-client-error`, "Error opening map", newClient.serverError, {
noCloseButton: true,
actions: context.settings.interactive ? [
{
label: "Close map",
href: context.baseUrl,
onClick: (e) => {
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
e.preventDefault();
newClient.openPad(undefined);
}
}
}
] : []
});
}
}
connectingClient.value = undefined;
client.value?.disconnect();
client.value = newClient;
@ -165,19 +112,70 @@
client.value.openPad(undefined);
}
}
const closeMapAction = computed<ToastAction>(() => ({
label: i18n.t("client-provider.close-map"),
href: context.baseUrl,
onClick: (e) => {
if (!e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey) {
e.preventDefault();
openPad(undefined);
}
}
}));
</script>
<template>
<Toast
v-if="client && client.disconnected && !client.serverError"
:id="`fm${context.id}-client-disconnected`"
variant="danger"
title="Disconnected"
message="The connection to the server was lost. Trying to reconnect…"
auto-hide
no-close-button visible
spinner
/>
<template v-if="connectingClient">
<Toast
:id="`fm${context.id}-client-connecting`"
:title="props.padId ? i18n.t('client-provider.loading-map-header') : i18n.t('client-provider.connecting-header')"
:message="props.padId ? i18n.t('client-provider.loading-map') : i18n.t('client-provider.connecting')"
spinner
noCloseButton
/>
</template>
<template v-else-if="client">
<template v-if="client.serverError && !client.isCreatePad">
<template v-if="client.disconnected || !props.padId">
<Toast
:id="`fm${context.id}-client-error`"
:title="i18n.t('client-provider.connection-error')"
:message="client.serverError"
:noCloseButton="!!props.padId"
/>
</template>
<template v-else>
<Toast
:id="`fm${context.id}-client-error`"
:title="i18n.t('client-provider.open-map-error')"
:message="client.serverError"
noCloseButton
:actions="context.settings.interactive ? [closeMapAction] : []"
/>
</template>
</template>
<template v-else-if="client.disconnected">
<Toast
:id="`fm${context.id}-client-disconnected`"
variant="danger"
:title="i18n.t('client-provider.disconnected-header')"
:message="i18n.t('client-provider.disconnected')"
no-close-button visible
spinner
/>
</template>
<template v-else-if="client.deleted">
<Toast
:id="`fm${context.id}-client-deleted`"
variant="danger"
:title="i18n.t('client-provider.map-deleted-header')"
:message="i18n.t('client-provider.map-deleted')"
noCloseButton
:actions="context.settings.interactive ? [closeMapAction] : []"
/>
</template>
</template>
<PadSettingsDialog
v-if="client?.isCreatePad"

Wyświetl plik

@ -4,10 +4,12 @@
import { computed, ref } from "vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "./ui/validated-form/validated-field.vue";
import { T, useI18n } from "../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const client = requireClientContext(context);
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -34,15 +36,15 @@
<template>
<ModalDialog
title="Filter"
:title="i18n.t('edit-filter-dialog.title')"
class="fm-edit-filter"
:isModified="isModified"
@submit="save"
:okLabel="isModified ? 'Apply' : undefined"
:okLabel="isModified ? i18n.t('edit-filter-dialog.apply') : undefined"
ref="modalRef"
@hidden="emit('hidden')"
>
<p>Here you can set an advanced expression to show/hide certain markers/lines based on their attributes. The filter expression only applies to your view of the map, but it can be persisted as part of a saved view or a shared link.</p>
<p>{{i18n.t("edit-filter-dialog.introduction")}}</p>
<ValidatedField
:value="filter"
@ -68,34 +70,55 @@
<hr />
<div class="fm-edit-filter-syntax">
<h3>Syntax</h3>
<h3>{{i18n.t("edit-filter-dialog.syntax-header")}}</h3>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
<th>Example</th>
<th>{{i18n.t("edit-filter-dialog.variable")}}</th>
<th>{{i18n.t("edit-filter-dialog.description")}}</th>
<th>{{i18n.t("edit-filter-dialog.example")}}</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td>Marker/Line name</td>
<td><code>name == "Berlin"</code></td>
<td>{{i18n.t("edit-filter-dialog.name-description")}}</td>
<td><code>name == &quot;Berlin&quot;</code></td>
</tr>
<tr>
<td><code>type</code></td>
<td><code>marker</code> / <code>line</code></td>
<td><code>type == "marker"</code></td>
<td>
<T k="edit-filter-dialog.type-description">
<template #marker>
<code>marker</code>
</template>
<template #line>
<code>line</code>
</template>
</T>
</td>
<td><code>type == &quot;marker&quot;</code></td>
</tr>
<tr>
<td><code>typeId</code></td>
<td>
<span v-for="(type, idx) in types" :key="type.id">
<span v-if="idx != 0"> / </span> <code>{{type.id}}</code> <span class="text-break">({{type.name}})</span>
</span>
<T k="edit-filter-dialog.typeId-description">
<template #items>
<span v-for="(type, idx) in types" :key="type.id">
<span v-if="idx != 0">{{i18n.t("edit-filter-dialog.typeId-description-separator")}}</span>
<T k="edit-filter-dialog.typeId-description-item">
<template #typeId>
<code>{{type.id}}</code>
</template>
<template #typeName>
<span class="text-break">{{type.name}}</span>
</template>
</T>
</span>
</template>
</T>
</td>
<td><code>typeId == {{types[0]?.id || 1}}</code></td>
</tr>
@ -103,165 +126,279 @@
<tr>
<td><code>data.&lt;field&gt;</code> / <code>prop(data, &lt;field&gt;)</code></td>
<td>
Field values (example: <code>data.Description</code> or <code>prop(data, &quot;Description&quot;)</code>).<br />
For checkbox fields, the value is <code>0</code> (unchecked) or <code>1</code> (checked).
<T k="edit-filter-dialog.data-description-1">
<template #example1>
<code>data.Description</code>
</template>
<template #example2>
<code>prop(data, &quot;Description&quot;)</code>)
</template>
</T>
<br />
<T k="edit-filter-dialog.data-description-2">
<template #uncheckedValue>
<code>0</code>
</template>
<template #checkedValue>
<code>1</code>
</template>
</T>
</td>
<td><code>lower(data.Description) ~= &quot;camp&quot;</code></td>
</tr>
<tr>
<td><code>lat</code>, <code>lon</code></td>
<td>Marker coordinates</td>
<td>{{i18n.t("edit-filter-dialog.lon-lat-description")}}</td>
<td><code>lat &lt; 50</code></td>
</tr>
<tr>
<td><code>colour</code></td>
<td>Marker/line colour</td>
<td>{{i18n.t("edit-filter-dialog.colour-description")}}</td>
<td><code>colour == &quot;ff0000&quot;</code></td>
</tr>
<tr>
<td><code>size</code></td>
<td>Marker size</td>
<td>{{i18n.t("edit-filter-dialog.size-description")}}</td>
<td><code>size &gt; 30</code></td>
</tr>
<tr>
<td><code>symbol</code></td>
<td>Marker icon</td>
<td>{{i18n.t("edit-filter-dialog.symbol-description")}}</td>
<td><code>symbol == &quot;accommodation_camping&quot;</code></td>
</tr>
<tr>
<td><code>shape</code></td>
<td>Marker shape</td>
<td>{{i18n.t("edit-filter-dialog.shape-description")}}</td>
<td><code>shape == &quot;circle&quot;</code></td>
</tr>
<tr>
<td><code>ele</code></td>
<td>Marker elevation</td>
<td>{{i18n.t("edit-filter-dialog.ele-description")}}</td>
<td><code>ele &gt; 500</code></td>
</tr>
<tr>
<td><code>mode</code></td>
<td>Line routing mode (<code>&quot;&quot;</code> / <code>&quot;car&quot;</code> / <code>&quot;bicycle&quot;</code> / <code>&quot;pedestrian&quot;</code> / <code>&quot;track&quot;</code>)</td>
<td>
<T k="edit-filter-dialog.mode-description">
<template #straight>
<code>&quot;&quot;</code>
</template>
<template #car>
<code>&quot;car&quot;</code>
</template>
<template #bicycle>
<code>&quot;bicycle&quot;</code>
</template>
<template #pedestrian>
<code>&quot;pedestrian&quot;</code>
</template>
<template #track>
<code>&quot;track&quot;</code>
</template>
</T>
</td>
<td><code>mode in (&quot;bicycle&quot;, &quot;pedestrian&quot;)</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>Line width</td>
<td>{{i18n.t("edit-filter-dialog.width-description")}}</td>
<td><code>width &gt; 10</code></td>
</tr>
<tr>
<td><code>stroke</code></td>
<td>Line stroke (<code>`&quot;&quot;`</code> (solid) / <code>`&quot;dashed&quot;`</code> / <code>&quot;dotted&quot;</code>)</td>
<td>
<T k="edit-filter-dialog.stroke-description">
<template #solid>
<code>&quot;&quot;</code>
</template>
<template #dashed>
<code>&quot;dashed&quot;</code>
</template>
<template #dotted>
<code>&quot;dotted&quot;</code>
</template>
</T>
</td>
<td><code>shape == &quot;dotted&quot;</code></td>
</tr>
<tr>
<td><code>distance</code></td>
<td>Line distance in kilometers</td>
<td>{{i18n.t("edit-filter-dialog.distance-description")}}</td>
<td><code>distance &lt; 50</code></td>
</tr>
<tr>
<td><code>time</code></td>
<td>Line routing time in seconds</td>
<td>{{i18n.t("edit-filter-dialog.time-description")}}</td>
<td><code>time &gt; 3600</code></td>
</tr>
<tr>
<td><code>ascent</code>, <code>descent</code></td>
<td>Total ascent/descent of line</td>
<td>{{i18n.t("edit-filter-dialog.ascent-descent-description")}}</td>
<td><code>ascent &gt; 1000</code></td>
</tr>
<tr>
<td><code>routePoints</code></td>
<td>Line point coordinates</td>
<td>{{i18n.t("edit-filter-dialog.routePoints-description")}}</td>
<td><code>routePoints.0.lon &gt; 60 and routePoints.2.lat &lt; 50</code></td>
</tr>
<tr>
<th>Operator</th>
<th>Description</th>
<th>Example</th>
<th>{{i18n.t("edit-filter-dialog.operator")}}</th>
<th>{{i18n.t("edit-filter-dialog.description")}}</th>
<th>{{i18n.t("edit-filter-dialog.example")}}</th>
</tr>
<tr>
<td><code>number</code></td>
<td>Numerical value</td>
<td>{{i18n.t("edit-filter-dialog.number-description")}}</td>
<td><code>distance &lt; 1.5</code></td>
</tr>
<tr>
<td><code>"text"</code></td>
<td>Text value</td>
<td>{{i18n.t("edit-filter-dialog.text-description")}}</td>
<td><code>name == &quot;Athens&quot;</code></td>
</tr>
<tr>
<td><code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code>, <code>^</code></td>
<td>Mathematical operations (<code>%</code>: modulo, <code>^</code>: power)</td>
<td>
<T k="edit-filter-dialog.mathematical-description">
<template #plus>
<code>+</code>
</template>
<template #minus>
<code>-</code>
</template>
<template #times>
<code>*</code>
</template>
<template #divided>
<code>/</code>
</template>
<template #modulo>
<code>%</code>
</template>
<template #power>
<code>^</code>
</template>
</T>
</td>
<td><code>distance / time &gt; 30</code></td>
</tr>
<tr>
<td><code>and</code>, <code>or</code>, <code>not</code>, <code>()</code></td>
<td>Logical operators</td>
<td>{{i18n.t("edit-filter-dialog.logical-description")}}</td>
<td><code>not (size&gt;10) or (type==&quot;line&quot; and length&lt;=10)</code></td>
</tr>
<tr>
<td><code>? :</code></td>
<td>if/then/else operator</td>
<td>{{i18n.t("edit-filter-dialog.ternary-description")}}</td>
<td><code>(type==&quot;marker&quot; ? size : width) &gt; 10</code></td>
</tr>
<tr>
<td><code>==</code>, <code>!=</code>, <code>&lt;</code>, <code>&lt;=</code>, <code>&gt;</code>, <code>&gt;=</code></td>
<td>Comparison (<code>!=</code>: not equal) (case sensitive)</td>
<td>
<T k="edit-filter-dialog.comparison-description">
<template #notEqual>
<code>!=</code>
</template>
</T>
</td>
<td><code>type != &quot;marker&quot;</code></td>
</tr>
<tr>
<td><code>in</code>, <code>not in</code></td>
<td>List operator (case sensitive)</td>
<td>
<T k="edit-filter-dialog.list-description">
<template #in>
<code>in</code>
</template>
<template #notIn>
<code>not in</code>
</template>
</T>
</td>
<td><code>typeId not in (1,2)</code></td>
</tr>
<tr>
<td><code>~=</code></td>
<td>Regular expression match (case sensitive)</td>
<td>{{i18n.t("edit-filter-dialog.regexp-description")}}</td>
<td><code>name ~= &quot;^[Cc]amp$&quot;</code></td>
</tr>
<tr>
<td><code>lower()</code></td>
<td>Convert to lower case</td>
<td>{{i18n.t("edit-filter-dialog.lower-description")}}</td>
<td><code>lower(name) ~= &quot;untitled&quot;</code></td>
</tr>
<tr>
<td><code>ceil()</code>, <code>floor()</code>, <code>round()</code></td>
<td>Round (<code>ceil</code>: up, <code>floor</code>: down)</td>
<td>
<T k="edit-filter-dialog.round-description">
<template #round>
<code>round</code>
</template>
<template #ceil>
<code>ceil</code>
</template>
<template #floor>
<code>floor</code>
</template>
</T>
</td>
<td><code>floor(distance/100) == 5</code></td>
</tr>
<tr>
<td><code>abs()</code>, <code>log()</code>, <code>sqrt()</code></td>
<td>Mathematical functions</td>
<td>
<T k="edit-filter-dialog.functions-description">
<template #abs>
<code>abs</code>
</template>
<template #log>
<code>log</code>
</template>
<template #sqrt>
<code>sqrt</code>
</template>
</T>
</td>
<td><code>abs(lat) &lt; 30</code></td>
</tr>
<tr>
<td><code>min()</code>, <code>max()</code></td>
<td>Smallest/highest value</td>
<td>
<T k="edit-filter-dialog.min-max-description">
<template #min>
<code>min</code>
</template>
<template #max>
<code>max</code>
</template>
</T>
</td>
<td><code>min(routePoints.0.lat,routePoints.1.lat) &lt; 50</code></td>
</tr>
</tbody>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { lineValidator, type ID } from "facilmap-types";
import { canControl, getOrderedTypes, mergeObject } from "facilmap-utils";
import { canControl, formatFieldName, formatTypeName, getOrderedTypes, mergeObject } from "facilmap-utils";
import { getUniqueId, getZodValidator, validateRequired } from "../utils/utils";
import { cloneDeep, isEqual, omit } from "lodash-es";
import ModalDialog from "./ui/modal-dialog.vue";
@ -135,7 +135,7 @@
<template v-for="(field, idx) in client.types[line.typeId].fields" :key="field.name">
<div class="row mb-3">
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{field.name}}</label>
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9">
<FieldInput
:id="`${id}-${idx}-input`"
@ -156,7 +156,7 @@
class="dropdown-item"
:class="{ active: type.id == line.typeId }"
@click="line.typeId = type.id"
>{{type.name}}</a>
>{{formatTypeName(type.name)}}</a>
</li>
</template>
</DropdownMenu>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { markerValidator, type ID } from "facilmap-types";
import { canControl, getOrderedTypes, mergeObject } from "facilmap-utils";
import { canControl, formatFieldName, formatTypeName, getOrderedTypes, mergeObject } from "facilmap-utils";
import { getUniqueId, getZodValidator, validateRequired } from "../utils/utils";
import { cloneDeep, isEqual } from "lodash-es";
import ModalDialog from "./ui/modal-dialog.vue";
@ -133,7 +133,7 @@
<template v-for="(field, idx) in client.types[marker.typeId].fields" :key="field.name">
<div class="row mb-3">
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{field.name}}</label>
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9">
<FieldInput
:id="`fm-edit-marker-${idx}-input`"
@ -154,7 +154,7 @@
class="dropdown-item"
:class="{ active: type.id == marker.typeId }"
@click="marker.typeId = type.id"
>{{type.name}}</a>
>{{formatTypeName(type.name)}}</a>
</li>
</template>
</DropdownMenu>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { typeValidator, type Field, type ID, type Type, type CRU } from "facilmap-types";
import { canControl } from "facilmap-utils";
import { canControl, formatFieldName } from "facilmap-utils";
import { getUniqueId, getZodValidator, validateRequired } from "../../utils/utils";
import { mergeTypeObject } from "./edit-type-utils";
import { cloneDeep, isEqual } from "lodash-es";
@ -21,11 +21,13 @@
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import StrokePicker from "../ui/stroke-picker.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
typeId: ID | "createMarkerType" | "createLineType";
@ -93,10 +95,10 @@
async function deleteField(field: Field): Promise<void> {
if (!await showConfirm({
title: "Delete field",
message: `Do you really want to delete the field “${field.name}”?`,
title: i18n.t("edit-type-dialog.delete-field-title"),
message: i18n.t("edit-type-dialog.delete-field-message", { fieldName: formatFieldName(field.name) }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("edit-type-dialog.delete-field-button")
}))
return;
@ -116,7 +118,11 @@
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-edit-type-error`, isCreate.value ? "Error creating type" : "Error saving type", err);
toasts.showErrorToast(
`fm${context.id}-edit-type-error`,
isCreate.value ? i18n.t("edit-type-dialog.create-type-error") : i18n.t("edit-type-dialog.save-type-error"),
err
);
}
}
@ -126,21 +132,26 @@
function handleUpdateField(field: Field) {
const idx = type.value.fields.indexOf(editField.value!);
if (idx === -1)
toasts.showErrorToast(`fm${context.id}-edit-type-dropdown-error`, "Error updating field", new Error("The field cannot be found on the type anymore."));
if (idx === -1) {
toasts.showErrorToast(
`fm${context.id}-edit-type-dropdown-error`,
i18n.t("edit-type-dialog.field-update-error"),
new Error(i18n.t("edit-type-dialog.field-disappeared-error"))
);
}
type.value.fields[idx] = field;
}
function validateFieldName(name: string) {
if (type.value.fields.filter((field) => field.name == name).length > 1) {
return "Multiple fields cannot have the same name.";
return i18n.t("edit-type-dialog.unique-field-name-error");
}
}
</script>
<template>
<ModalDialog
title="Edit Type"
:title="i18n.t('edit-type-dialog.title')"
class="fm-edit-type"
:isModified="isModified"
:isCreate="isCreate"
@ -149,7 +160,7 @@
@hidden="emit('hidden')"
>
<div class="row mb-3">
<label :for="`${id}-name-input`" class="col-sm-3 col-form-label">Name</label>
<label :for="`${id}-name-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.name")}}</label>
<ValidatedField
:value="type.name"
:validators="[validateRequired, getZodValidator(typeValidator.update.shape.name)]"
@ -165,7 +176,7 @@
</div>
<div class="row mb-3">
<label :for="`${id}-type-input`" class="col-sm-3 col-form-label">Type</label>
<label :for="`${id}-type-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.type")}}</label>
<div class="col-sm-9">
<select
:id="`${id}-type-input`"
@ -173,8 +184,8 @@
class="form-select"
disabled
>
<option value="marker">Marker</option>
<option value="line">Line</option>
<option value="marker">{{i18n.t("edit-type-dialog.type-marker")}}</option>
<option value="line">{{i18n.t("edit-type-dialog.type-line")}}</option>
</select>
</div>
</div>
@ -183,14 +194,12 @@
<hr/>
<p class="text-muted">
These styles are applied when a new object of this type is created. If Fixed is enabled, the style is applied to all objects
of this type and cannot be changed for an individual object anymore. For more complex style control, dropdown or checkbox fields
can be configured below to change the style based on their selected value.
{{i18n.t("edit-type-dialog.styles-introduction")}}
</p>
<template v-if="resolvedCanControl.includes('colour')">
<div class="row mb-3">
<label :for="`${id}-default-colour-input`" class="col-sm-3 col-form-label">Default colour</label>
<label :for="`${id}-default-colour-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-colour")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -207,7 +216,7 @@
:id="`${id}-default-colour-fixed`"
v-model="type.colourFixed"
/>
<label :for="`${id}-default-colour-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-colour-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -217,7 +226,7 @@
<template v-if="resolvedCanControl.includes('size')">
<div class="row mb-3">
<label :for="`${id}-default-size-input`" class="col-sm-3 col-form-label">Default size</label>
<label :for="`${id}-default-size-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-size")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -235,7 +244,7 @@
:id="`${id}-default-size-fixed`"
v-model="type.sizeFixed"
/>
<label :for="`${id}-default-size-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-size-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -245,7 +254,7 @@
<template v-if="resolvedCanControl.includes('symbol')">
<div class="row mb-3">
<label :for="`${id}-default-symbol-input`" class="col-sm-3 col-form-label">Default icon</label>
<label :for="`${id}-default-symbol-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-icon")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -262,7 +271,7 @@
:id="`${id}-default-symbol-fixed`"
v-model="type.symbolFixed"
/>
<label :for="`${id}-default-symbol-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-symbol-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -272,7 +281,7 @@
<template v-if="resolvedCanControl.includes('shape')">
<div class="row mb-3">
<label :for="`${id}-default-shape-input`" class="col-sm-3 col-form-label">Default shape</label>
<label :for="`${id}-default-shape-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-shape")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -289,7 +298,7 @@
:id="`${id}-default-shape-fixed`"
v-model="type.shapeFixed"
/>
<label :for="`${id}-default-shape-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-shape-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -299,7 +308,7 @@
<template v-if="resolvedCanControl.includes('width')">
<div class="row mb-3">
<label :for="`${id}-default-width-input`" class="col-sm-3 col-form-label">Default width</label>
<label :for="`${id}-default-width-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-width")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -317,7 +326,7 @@
:id="`${id}-default-width-fixed`"
v-model="type.widthFixed"
/>
<label :for="`${id}-default-width-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-width-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -327,7 +336,7 @@
<template v-if="resolvedCanControl.includes('stroke')">
<div class="row mb-3">
<label :for="`${id}-default-stroke-input`" class="col-sm-3 col-form-label">Default stroke</label>
<label :for="`${id}-default-stroke-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-stroke")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -344,7 +353,7 @@
:id="`${id}-default-stroke-fixed`"
v-model="type.strokeFixed"
/>
<label :for="`${id}-default-stroke-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-stroke-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -354,7 +363,7 @@
<template v-if="resolvedCanControl.includes('mode')">
<div class="row mb-3">
<label :for="`${id}-default-mode-input`" class="col-sm-3 col-form-label">Default routing mode</label>
<label :for="`${id}-default-mode-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.default-route-mode")}}</label>
<div class="col-sm-9">
<div class="row align-items-center">
<div class="col-sm-9">
@ -371,7 +380,7 @@
:id="`${id}-default-mode-fixed`"
v-model="type.modeFixed"
/>
<label :for="`${id}-default-mode-fixed`" class="form-check-label">Fixed</label>
<label :for="`${id}-default-mode-fixed`" class="form-check-label">{{i18n.t("edit-type-dialog.fixed")}}</label>
</div>
</div>
</div>
@ -383,7 +392,7 @@
</template>
<div class="row mb-3">
<label :for="`${id}-show-in-legend-input`" class="col-sm-3 col-form-label">Legend</label>
<label :for="`${id}-show-in-legend-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-type-dialog.legend")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -392,23 +401,23 @@
:id="`${id}-show-in-legend-input`"
v-model="type.showInLegend"
/>
<label :for="`${id}-show-in-legend-input`" class="form-check-label">Show in legend</label>
<label :for="`${id}-show-in-legend-input`" class="form-check-label">{{i18n.t("edit-type-dialog.show-in-legend")}}</label>
</div>
<div class="form-text">
An item for this type will be shown in the legend. Any fixed style attributes are applied to it. Dropdown or checkbox fields that control the style generate additional legend items.
{{i18n.t("edit-type-dialog.show-in-legend-description")}}
</div>
</div>
</div>
<h2>Fields</h2>
<div class="table-responseive">
<h2>{{i18n.t("edit-type-dialog.fields")}}</h2>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th style="width: 35%; min-width: 150px">Name</th>
<th style="width: 35%; min-width: 120px">Type</th>
<th style="width: 35%; min-width: 150px">Default value</th>
<th>Delete</th>
<th style="width: 35%; min-width: 150px">{{i18n.t("edit-type-dialog.field-name")}}</th>
<th style="width: 35%; min-width: 120px">{{i18n.t("edit-type-dialog.field-type")}}</th>
<th style="width: 35%; min-width: 150px">{{i18n.t("edit-type-dialog.field-default-value")}}</th>
<th>{{i18n.t("edit-type-dialog.field-delete")}}</th>
<th></th>
</tr>
</thead>
@ -440,13 +449,13 @@
<td>
<div class="input-group">
<select class="form-select" v-model="field.type">
<option value="input">Text field</option>
<option value="textarea">Text area</option>
<option value="dropdown">Dropdown</option>
<option value="checkbox">Checkbox</option>
<option value="input">{{i18n.t("edit-type-dialog.field-type-input")}}</option>
<option value="textarea">{{i18n.t("edit-type-dialog.field-type-textarea")}}</option>
<option value="dropdown">{{i18n.t("edit-type-dialog.field-type-dropdown")}}</option>
<option value="checkbox">{{i18n.t("edit-type-dialog.field-type-checkbox")}}</option>
</select>
<template v-if="['dropdown', 'checkbox'].includes(field.type)">
<button type="button" class="btn btn-secondary" @click="editDropdown(field)">Edit</button>
<button type="button" class="btn btn-secondary" @click="editDropdown(field)">{{i18n.t("edit-type-dialog.field-edit")}}</button>
</template>
</div>
</td>
@ -454,10 +463,10 @@
<FieldInput :field="field" v-model="field.default" ignore-default></FieldInput>
</td>
<td class="td-buttons">
<button type="button" class="btn btn-secondary" @click="deleteField(field)">Delete</button>
<button type="button" class="btn btn-secondary" @click="deleteField(field)">{{i18n.t("edit-type-dialog.field-delete")}}</button>
</td>
<td class="td-buttons">
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" alt="Reorder"></Icon></button>
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" :alt="i18n.t('edit-type-dialog.field-reorder')"></Icon></button>
</td>
</tr>
</template>
@ -465,7 +474,7 @@
<tfoot>
<tr>
<td colspan="4">
<button type="button" class="btn btn-secondary" @click="createField()"><Icon icon="plus" alt="Add"></Icon></button>
<button type="button" class="btn btn-secondary" @click="createField()"><Icon icon="plus" :alt="i18n.t('edit-type-dialog.field-add')"></Icon></button>
</td>
<td class="move"></td>
</tr>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { CRU, Field, FieldOptionUpdate, FieldUpdate, Type } from "facilmap-types";
import { canControl, mergeObject } from "facilmap-utils";
import { canControl, formatCheckboxValue, mergeObject } from "facilmap-utils";
import { getUniqueId } from "../../utils/utils";
import { cloneDeep, isEqual } from "lodash-es";
import ColourPicker from "../ui/colour-picker.vue";
@ -17,6 +17,7 @@
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import StrokePicker from "../ui/stroke-picker.vue";
import { useI18n } from "../../utils/i18n";
function getControlNumber(type: Type<CRU.READ | CRU.CREATE_VALIDATED>, field: FieldUpdate): number {
return [
@ -35,6 +36,7 @@
const context = injectContextRequired();
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
type: Type<CRU.READ | CRU.CREATE_VALIDATED>;
@ -100,10 +102,10 @@
async function deleteOption(option: FieldOptionUpdate): Promise<void> {
if (!await showConfirm({
title: "Delete option",
message: `Do you really want to delete the option “${option.value}”?`,
title: i18n.t("edit-type-dropdown-dialog.delete-option-title"),
message: i18n.t("edit-type-dropdown-dialog.delete-option-message", { value: option.value }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("edit-type-dropdown-dialog.delete-option-button")
}))
return;
@ -124,33 +126,41 @@
function validateOptionValue(value: string): string | undefined {
if (fieldValue.value.type !== "checkbox" && fieldValue.value.options!.filter((op) => op.value === value).length > 1) {
return "Multiple options cannot have the same label.";
return i18n.t("edit-type-dropdown-dialog.unique-value-error");
}
}
const formValidationError = computed(() => {
if (controlNumber.value > 0 && (fieldValue.value.options?.length ?? 0) === 0) {
return "Controlling fields need to have at least one option.";
return i18n.t("edit-type-dropdown-dialog.no-options-error");
} else {
return undefined;
}
});
const typeInterpolation = computed(() => {
if (props.type.type === "marker") {
return i18n.t("edit-type-dropdown-dialog.control-interpolation-marker");
} else {
return i18n.t("edit-type-dropdown-dialog.control-interpolation-line");
}
});
</script>
<template>
<ModalDialog
:title="`Edit ${fieldValue.type == 'checkbox' ? 'Checkbox' : 'Dropdown'}`"
:title="fieldValue.type === 'checkbox' ? i18n.t('edit-type-dropdown-dialog.title-checkbox') : i18n.t('edit-type-dropdown-dialog.title-dropdown')"
class="fm-edit-type-dropdown"
:isModified="isModified"
@submit="save()"
@hidden="emit('hidden')"
:size="fieldValue && controlNumber > 2 ? 'xl' : 'lg'"
:okLabel="isModified ? 'OK' : undefined"
:okLabel="isModified ? i18n.t('edit-type-dropdown-dialog.ok-button') : undefined"
:formValidationError="formValidationError"
ref="modalRef"
>
<div class="row mb-3">
<label class="col-sm-3 col-form-label">Control</label>
<label class="col-sm-3 col-form-label">{{i18n.t("edit-type-dropdown-dialog.control")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -164,7 +174,7 @@
class="form-check-label"
:for="`${id}-control-colour`"
>
Control {{type.type}} colour
{{i18n.t("edit-type-dropdown-dialog.control-colour", { type: typeInterpolation })}}
</label>
</div>
@ -180,7 +190,7 @@
class="form-check-label"
:for="`${id}-control-size`"
>
Control {{type.type}} size
{{i18n.t("edit-type-dropdown-dialog.control-size", { type: typeInterpolation })}}
</label>
</div>
@ -196,7 +206,7 @@
class="form-check-label"
:for="`${id}-control-symbol`"
>
Control {{type.type}} icon
{{i18n.t("edit-type-dropdown-dialog.control-icon", { type: typeInterpolation })}}
</label>
</div>
@ -212,7 +222,7 @@
class="form-check-label"
:for="`${id}-control-shape`"
>
Control {{type.type}} shape
{{i18n.t("edit-type-dropdown-dialog.control-shape", { type: typeInterpolation })}}
</label>
</div>
@ -228,7 +238,7 @@
class="form-check-label"
:for="`${id}-control-width`"
>
Control {{type.type}} width
{{i18n.t("edit-type-dropdown-dialog.control-width", { type: typeInterpolation })}}
</label>
</div>
@ -244,7 +254,7 @@
class="form-check-label"
:for="`${id}-control-stroke`"
>
Control {{type.type}} stroke
{{i18n.t("edit-type-dropdown-dialog.control-stroke", { type: typeInterpolation })}}
</label>
</div>
</div>
@ -252,14 +262,14 @@
<table v-if="fieldValue.type != 'checkbox' || controlNumber > 0" class="table table-striped table-hover">
<thead>
<tr>
<th>Option</th>
<th v-if="fieldValue.type == 'checkbox'">Label (for legend)</th>
<th v-if="fieldValue.controlColour">Colour</th>
<th v-if="fieldValue.controlSize">Size</th>
<th v-if="fieldValue.controlSymbol">Icon</th>
<th v-if="fieldValue.controlShape">Shape</th>
<th v-if="fieldValue.controlWidth">Width</th>
<th v-if="fieldValue.controlStroke">Stroke</th>
<th>{{i18n.t("edit-type-dropdown-dialog.option")}}</th>
<th v-if="fieldValue.type == 'checkbox'">{{i18n.t("edit-type-dropdown-dialog.label")}}</th>
<th v-if="fieldValue.controlColour">{{i18n.t("edit-type-dropdown-dialog.colour")}}</th>
<th v-if="fieldValue.controlSize">{{i18n.t("edit-type-dropdown-dialog.size")}}</th>
<th v-if="fieldValue.controlSymbol">{{i18n.t("edit-type-dropdown-dialog.icon")}}</th>
<th v-if="fieldValue.controlShape">{{i18n.t("edit-type-dropdown-dialog.shape")}}</th>
<th v-if="fieldValue.controlWidth">{{i18n.t("edit-type-dropdown-dialog.width")}}</th>
<th v-if="fieldValue.controlStroke">{{i18n.t("edit-type-dropdown-dialog.stroke")}}</th>
<th v-if="fieldValue.type != 'checkbox'"></th>
<th v-if="fieldValue.type != 'checkbox'" class="move"></th>
</tr>
@ -273,7 +283,7 @@
<template #item="{ element: option, index: idx }">
<tr>
<td v-if="fieldValue.type == 'checkbox'">
<strong>{{idx === 0 ? '✘' : '✔'}}</strong>
<strong>{{formatCheckboxValue(idx === 0 ? "0" : "1")}}</strong>
</td>
<ValidatedField
tag="td"
@ -331,10 +341,10 @@
></StrokePicker>
</td>
<td v-if="fieldValue.type != 'checkbox'" class="td-buttons">
<button type="button" class="btn btn-secondary" @click="deleteOption(option)"><Icon icon="minus" alt="Remove"></Icon></button>
<button type="button" class="btn btn-secondary" @click="deleteOption(option)"><Icon icon="minus" :alt="i18n.t('edit-type-dropdown-dialog.option-remove')"></Icon></button>
</td>
<td v-if="fieldValue.type != 'checkbox'" class="td-buttons">
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" alt="Reorder"></Icon></button>
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" :alt="i18n.t('edit-type-dropdown-dialog.option-reorder')"></Icon></button>
</td>
</tr>
</template>
@ -342,7 +352,7 @@
<tfoot v-if="fieldValue.type != 'checkbox'">
<tr>
<td :colspan="columns">
<button type="button" class="btn btn-secondary" @click="addOption()"><Icon icon="plus" alt="Add"></Icon></button>
<button type="button" class="btn btn-secondary" @click="addOption()"><Icon icon="plus" :alt="i18n.t('edit-type-dropdown-dialog.option-add')"></Icon></button>
</td>
</tr>
</tfoot>

Wyświetl plik

@ -11,7 +11,7 @@
import { useToasts } from "./ui/toasts/toasts.vue";
import copyToClipboard from "copy-to-clipboard";
import type { CustomSubmitEvent } from "./ui/validated-form/validated-form.vue";
import { getOrderedTypes } from "facilmap-utils";
import { formatFieldName, formatTypeName, getOrderedTypes } from "facilmap-utils";
const toasts = useToasts();
@ -290,7 +290,7 @@
>
<template #default="slotProps">
<select class="form-select" v-model="typeId" :id="`${id}-type-select`" :ref="slotProps.inputRef">
<option v-for="type of orderedTypes" :key="type.id" :value="type.id">{{type.name}}</option>
<option v-for="type of orderedTypes" :key="type.id" :value="type.id">{{formatTypeName(type.name)}}</option>
</select>
<div class="invalid-tooltip">
{{slotProps.validationError}}
@ -311,7 +311,7 @@
:checked="!hide.has(key)"
@change="hide.has(key) ? hide.delete(key) : hide.add(key)"
>
<label class="form-check-label" :for="`${id}-show-${key}-checkbox`">{{key}}</label>
<label class="form-check-label" :for="`${id}-show-${key}-checkbox`">{{formatFieldName(key)}}</label>
</div>
</template>
</div>

Wyświetl plik

@ -51,7 +51,7 @@
function provideComponent<K extends keyof FacilMapComponents>(key: K, componentRef: Readonly<Ref<FacilMapComponents[K]>>) {
if (key in components) {
throw new Error(`Component "${key}"" is already provided.`);
throw new Error(`Component "${key}" is already provided.`);
}
watch(componentRef, (component) => {

Wyświetl plik

@ -11,6 +11,7 @@
import Popover from "../ui/popover.vue";
import ModalDialog from "../ui/modal-dialog.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
type HistoryEntryWithLabels = HistoryEntry & {
labels: HistoryEntryLabels;
@ -20,6 +21,7 @@
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -37,7 +39,7 @@
try {
await client.value.listenToHistory();
} catch (err) {
toasts.showErrorToast(`${id}-listen-error`, "Error loading history", err);
toasts.showErrorToast(`${id}-listen-error`, i18n.t("history-dialog.loading-error"), err);
} finally {
isLoading.value = false;
}
@ -55,7 +57,7 @@
toasts.hideToast(`${id}-revert-error`);
if (!await showConfirm({
title: entry.labels.revert!.button,
title: entry.labels.revert!.title,
message: entry.labels.revert!.message,
variant: "warning",
okLabel: entry.labels.revert!.okLabel
@ -67,7 +69,7 @@
try {
await client.value.revertHistoryEntry({ id: entry.id });
} catch (err) {
toasts.showErrorToast(`${id}-revert-error`, "Error loading history", err);
toasts.showErrorToast(`${id}-revert-error`, i18n.t("history-dialog.revert-error"), err);
} finally {
isReverting.value = undefined;
}
@ -96,23 +98,23 @@
<template>
<ModalDialog
title="History"
:title="i18n.t('history-dialog.title')"
size="xl"
class="fm-history"
@hidden="emit('hidden')"
:isBusy="!!isReverting"
>
<p><em>Here you can inspect and revert the last 50 changes to the map.</em></p>
<p><em>{{i18n.t("history-dialog.introduction")}}</em></p>
<div v-if="isLoading" class="d-flex justify-content-center">
<div class="spinner-border"></div>
</div>
<table v-else class="table table-striped table-hover history-entries">
<thead>
<tr>
<th style="min-width: 12rem">Date</th>
<th style="min-width: 15rem">Action</th>
<th style="min-width: 12rem">{{i18n.t("history-dialog.date")}}</th>
<th style="min-width: 15rem">{{i18n.t("history-dialog.action")}}</th>
<th></th>
<th>Restore</th>
<th>{{i18n.t("history-dialog.restore")}}</th>
</tr>
</thead>
<tbody>
@ -141,9 +143,9 @@
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Field</th>
<th>Before</th>
<th>After</th>
<th>{{i18n.t("history-dialog.diff-field")}}</th>
<th>{{i18n.t("history-dialog.diff-before")}}</th>
<th>{{i18n.t("history-dialog.diff-after")}}</th>
</tr>
</thead>
<tbody>

Wyświetl plik

@ -1,6 +1,7 @@
import type { HistoryEntry } from "facilmap-types";
import { getObjectDiff, type ObjectDiffItem } from "facilmap-utils";
import type { ClientContext } from "../facil-map-context-provider/client-context";
import { getI18n } from "../../utils/i18n";
function existsNow(client: ClientContext, entry: HistoryEntry) {
// Look through the history of this particular object and see if the last entry indicates that the object exists now
@ -32,52 +33,125 @@ export interface HistoryEntryLabels {
}
export function getLabelsForHistoryEntry(client: ClientContext, entry: HistoryEntry): HistoryEntryLabels {
const i18n = getI18n();
if(entry.type == "Pad") {
return {
description: "Changed map settings",
description: i18n.t("history-utils.description-update-map"),
revert: {
title: "Revert map settings",
message: "Do you really want to restore the old version of the map settings?",
button: "Revert",
okLabel: "Revert"
title: i18n.t("history-utils.revert-update-map-title"),
message: i18n.t("history-utils.revert-update-map-message"),
button: i18n.t("history-utils.revert-update"),
okLabel: i18n.t("history-utils.revert-update-button")
},
...(entry.objectBefore && entry.objectAfter ? { diff: getObjectDiff(entry.objectBefore, entry.objectAfter) } : {}),
};
}
const nameStrBefore = entry.objectBefore && entry.objectBefore.name ? `${entry.objectBefore.name}` : "";
const nameStrAfter = entry.objectAfter && entry.objectAfter.name ? `${entry.objectAfter.name}` : "";
const nameStrBefore = entry.objectBefore && entry.objectBefore.name ? i18n.t("history-utils.description-interpolation-quotedName", { name: entry.objectBefore.name }) : "";
const nameStrAfter = entry.objectAfter && entry.objectAfter.name ? i18n.t("history-utils.description-interpolation-quotedName", { name: entry.objectAfter.name }) : "";
const exists = existsNow(client, entry);
const descriptionAction = { create: "Created", update: "Changed", delete: "Deleted" }[entry.action];
const descriptionName = (nameStrBefore && nameStrAfter && nameStrBefore != nameStrAfter ? `${nameStrBefore} (new name: ${nameStrAfter})` : (nameStrBefore || nameStrAfter));
const descriptionName = (nameStrBefore && nameStrAfter && nameStrBefore != nameStrAfter ? (
i18n.t("history-utils.description-interpolation-quotedName-renamed", { quotedBefore: nameStrBefore, quotedAfter: nameStrAfter })
) : (nameStrBefore || nameStrAfter));
const diff = entry.objectBefore && entry.objectAfter ? getObjectDiff(entry.objectBefore, entry.objectAfter) : undefined;
const revert = (
entry.action === "create" ? (
exists ? {
title: `Delete ${entry.type}`,
button: "Revert (delete)",
message: "delete",
okLabel: "Delete"
button: i18n.t("history-utils.revert-create"),
title: {
Marker: i18n.t("history-utils.revert-create-marker-title"),
Line: i18n.t("history-utils.revert-create-line-title"),
View: i18n.t("history-utils.revert-create-view-title"),
Type: i18n.t("history-utils.revert-create-type-title")
}[entry.type],
message: {
Marker: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-create-marker-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-create-marker-message-unnamed"),
Line: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-create-line-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-create-line-message-unnamed"),
View: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-create-view-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-create-view-message-unnamed"),
Type: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-create-type-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-create-type-message-unnamed"),
}[entry.type],
okLabel: i18n.t("history-utils.revert-create-button")
} : undefined
) : exists ? {
title: `Revert ${entry.type}`,
button: "Revert",
message: "restore the old version of",
okLabel: "Revert"
button: i18n.t("history-utils.revert-update"),
title: {
Marker: i18n.t("history-utils.revert-update-marker-title"),
Line: i18n.t("history-utils.revert-update-line-title"),
View: i18n.t("history-utils.revert-update-view-title"),
Type: i18n.t("history-utils.revert-update-type-title")
}[entry.type],
message: {
Marker: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-update-marker-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-update-marker-message-unnamed"),
Line: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-update-line-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-update-line-message-unnamed"),
View: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-update-view-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-update-view-message-unnamed"),
Type: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-update-type-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-update-type-message-unnamed"),
}[entry.type],
okLabel: i18n.t("history-utils.revert-update-button")
} : {
title: `Restore ${entry.type}`,
button: "Restore",
message: "restore",
okLabel: "Restore"
button: i18n.t("history-utils.revert-delete"),
title: {
Marker: i18n.t("history-utils.revert-delete-marker-title"),
Line: i18n.t("history-utils.revert-delete-line-title"),
View: i18n.t("history-utils.revert-delete-view-title"),
Type: i18n.t("history-utils.revert-delete-type-title")
}[entry.type],
message: {
Marker: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-delete-marker-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-delete-marker-message-unnamed"),
Line: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-delete-line-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-delete-line-message-unnamed"),
View: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-delete-view-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-delete-view-message-unnamed"),
Type: nameStrBefore || nameStrAfter
? i18n.t("history-utils.revert-delete-type-message-named", { quotedName: nameStrBefore || nameStrAfter })
: i18n.t("history-utils.revert-delete-type-message-unnamed"),
}[entry.type],
okLabel: i18n.t("history-utils.revert-delete-button")
}
);
return {
description: `${descriptionAction} ${entry.type} ${entry.objectId} ${descriptionName}`,
revert: revert && {
...revert,
message: `Do you really want to ${revert.message} ${((nameStrBefore || nameStrAfter) ? `the ${entry.type} ${(nameStrBefore || nameStrAfter)}` : `this ${entry.type}`)}?`
},
description: {
create: {
Marker: i18n.t("history-utils.description-create-marker", { id: entry.objectId, quotedName: descriptionName }),
Line: i18n.t("history-utils.description-create-line", { id: entry.objectId, quotedName: descriptionName }),
View: i18n.t("history-utils.description-create-view", { id: entry.objectId, quotedName: descriptionName }),
Type: i18n.t("history-utils.description-create-type", { id: entry.objectId, quotedName: descriptionName })
}[entry.type],
update: {
Marker: i18n.t("history-utils.description-update-marker", { id: entry.objectId, quotedName: descriptionName }),
Line: i18n.t("history-utils.description-update-line", { id: entry.objectId, quotedName: descriptionName }),
View: i18n.t("history-utils.description-update-view", { id: entry.objectId, quotedName: descriptionName }),
Type: i18n.t("history-utils.description-update-type", { id: entry.objectId, quotedName: descriptionName })
}[entry.type],
delete: {
Marker: i18n.t("history-utils.description-delete-marker", { id: entry.objectId, quotedName: descriptionName }),
Line: i18n.t("history-utils.description-delete-line", { id: entry.objectId, quotedName: descriptionName }),
View: i18n.t("history-utils.description-delete-view", { id: entry.objectId, quotedName: descriptionName }),
Type: i18n.t("history-utils.description-delete-type", { id: entry.objectId, quotedName: descriptionName })
}[entry.type]
}[entry.action],
revert,
...(diff ? { diff } : {})
};
}

Wyświetl plik

@ -16,6 +16,7 @@ import type { ClientContext } from "../facil-map-context-provider/client-context
import type { FacilMapContext } from "../facil-map-context-provider/facil-map-context";
import { requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { type Optional, sleep } from "facilmap-utils";
import { getI18n } from "../../utils/i18n";
type MapContextWithoutComponents = Optional<WritableMapContext, 'components'>;
type OnCleanup = (cleanupFn: () => void) => void;
@ -203,11 +204,11 @@ function useOverpassLayer(map: Ref<Map>, mapContext: MapContextWithoutComponents
if (status == OverpassLoadStatus.COMPLETE)
mapContext.overpassMessage = undefined;
else if (status == OverpassLoadStatus.INCOMPLETE)
mapContext.overpassMessage = "Not all POIs are shown because there are too many results. Zoom in to show all results.";
mapContext.overpassMessage = getI18n().t("leaflet-map-components.pois-too-many-results");
else if (status == OverpassLoadStatus.TIMEOUT)
mapContext.overpassMessage = "Zoom in to show POIs.";
mapContext.overpassMessage = getI18n().t("leaflet-map-components.pois-zoom-in");
else if (status == OverpassLoadStatus.ERROR)
mapContext.overpassMessage = "Error loading POIs: " + error.message;
mapContext.overpassMessage = getI18n().t("leaflet-map-components.pois-error", { message: error.message });
})
.on("clear", () => {
mapContext.overpassMessage = undefined;

Wyświetl plik

@ -4,9 +4,11 @@
import vTooltip from "../../utils/tooltip";
import type { WritableMapContext } from "../facil-map-context-provider/map-context";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const i18n = useI18n();
const innerContainerRef = ref<HTMLElement>();
const mapRef = ref<HTMLElement>();
@ -50,7 +52,7 @@
:href="selfUrl"
target="_blank"
class="fm-open-external"
v-tooltip.right="`Open ${context.appName} in full size`"
v-tooltip.right="i18n.t('leaflet-map.open-full-size', { appName: context.appName })"
></a>
<div class="fm-logo">
<img src="./logo.png"/>
@ -65,7 +67,7 @@
<div class="fm-leaflet-map-disabled-cover" v-show="client.padId && (client.disconnected || (client.serverError && !client.isCreatePad) || client.deleted)"></div>
<div class="fm-leaflet-map-loading" v-show="!loaded && !client.serverError && !client.isCreatePad" :class="{ 'fatal-error': !!fatalError }">
{{fatalError || 'Loading...'}}
{{fatalError || i18n.t("leaflet-map.loading")}}
</div>
</div>
</template>

Wyświetl plik

@ -7,9 +7,11 @@
import { computed, reactive, ref } from "vue";
import { mapRef, vHtmlAsync } from "../../utils/vue";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const props = withDefaults(defineProps<{
legend1?: string;
@ -122,7 +124,7 @@
<p>
<span class="text-break" :style="item.strikethrough ? {'text-decoration': 'line-through'} : {}">{{item.label}}</span>
<br>
<small><em>Click to show/hide objects of this type.</em></small>
<small><em>{{i18n.t("legend-content.click-explanation")}}</em></small>
</p>
</Popover>
</template>

Wyświetl plik

@ -1,6 +1,6 @@
import type { ID, Shape, Stroke, Symbol, Type } from "facilmap-types";
import { symbolList } from "facilmap-leaflet";
import { getOrderedTypes, isBright } from "facilmap-utils";
import { formatTypeName, getOrderedTypes, isBright } from "facilmap-utils";
import type { FacilMapContext } from "../facil-map-context-provider/facil-map-context";
import { requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
@ -51,7 +51,7 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
const item: LegendItem = {
key: `legend-item-${type.id}`,
value: type.name,
label: type.name,
label: formatTypeName(type.name),
filtered: true
};
@ -157,7 +157,7 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
const item: LegendItem = {
key: `legend-item-${type.id}`,
value: type.name,
label: type.name,
label: formatTypeName(type.name),
filtered: true
};
@ -171,7 +171,7 @@ export function getLegendItems(context: FacilMapContext): LegendType[] {
key: `legend-type-${type.id}`,
type: type.type,
typeId: type.id,
name: type.name,
name: formatTypeName(type.name),
items,
filtered: true,
};

Wyświetl plik

@ -7,10 +7,12 @@
import { useDomEventListener } from "../../utils/utils";
import { useResizeObserver } from "../../utils/vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const i18n = useI18n();
const absoluteContainerRef = ref<HTMLElement>();
@ -62,7 +64,7 @@
</div>
</template>
<template v-else>
<SearchBoxTab :id="`fm${context.id}-legend-tab`" title="Legend">
<SearchBoxTab :id="`fm${context.id}-legend-tab`" :title="i18n.t('legend.tab-label')">
<LegendContent :items="legendItems" :legend1="legend1" :legend2="legend2" no-popover></LegendContent>
</SearchBoxTab>
</template>

Wyświetl plik

@ -4,7 +4,7 @@
import SearchBoxTab from "../search-box/search-box-tab.vue";
import { useEventListener } from "../../utils/utils";
import { injectContextRequired, requireClientContext, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { normalizeLineName } from "facilmap-utils";
import { normalizeLineName } from "facilmap-utils";
const context = injectContextRequired();
const client = requireClientContext(context);

Wyświetl plik

@ -7,19 +7,21 @@
import { getZoomDestinationForLine } from "../../utils/zoom";
import RouteForm from "../route-form/route-form.vue";
import vTooltip from "../../utils/tooltip";
import { formatField, formatRouteTime, normalizeLineName, round } from "facilmap-utils";
import { formatDistance, formatFieldName, formatFieldValue, formatRouteTime, formatTypeName, normalizeLineName } from "facilmap-utils";
import { computed, ref } from "vue";
import { useToasts } from "../ui/toasts/toasts.vue";
import { showConfirm } from "../ui/alert.vue";
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ExportDropdown from "../ui/export-dropdown.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
lineId: ID;
@ -41,17 +43,17 @@
const line = computed(() => client.value.lines[props.lineId]);
const typeName = computed(() => client.value.types[line.value.typeId].name);
const typeName = computed(() => formatTypeName(client.value.types[line.value.typeId].name));
const showTypeName = computed(() => Object.values(client.value.types).filter((t) => t.type === 'line').length > 1);
async function deleteLine(): Promise<void> {
toasts.hideToast(`fm${context.id}-line-info-delete`);
if (!await showConfirm({
title: "Delete line",
message: `Do you really want to delete the line “${normalizeLineName(line.value.name)}”?`,
title: i18n.t("line-info.delete-line-title"),
message: i18n.t("line-info.delete-line-message", { name: normalizeLineName(line.value.name) }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("line-info.delete-line-ok")
}))
return;
@ -60,7 +62,7 @@
try {
await client.value.deleteLine({ id: props.lineId });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-delete`, "Error deleting line", err);
toasts.showErrorToast(`fm${context.id}-line-info-delete`, i18n.t("line-info.delete-line-error"), err);
} finally {
isDeleting.value = false;
}
@ -92,7 +94,7 @@
if(save)
await client.value.editLine({ id: line.value.id, routePoints: route.routePoints, mode: route.mode });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, "Error saving line", err);
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, i18n.t("line-info.save-line-error"), err);
} finally {
mapContext.value.components.map.fire('fmInteractionEnd');
isMoving.value = false;
@ -106,17 +108,17 @@
}
};
toasts.showToast(`fm${context.id}-line-info-move`, `Edit waypoints`, "Use the routing form or drag the line around to change it. Click “Finish” to save the changes.", {
toasts.showToast(`fm${context.id}-line-info-move`, i18n.t("line-info.move-line-title"), i18n.t("line-info.move-line-message"), {
noCloseButton: true,
actions: [
{ label: "Finish", variant: "primary", onClick: () => { void done(true); }},
{ label: "Cancel", onClick: () => { void done(false); } }
{ label: i18n.t("line-info.move-line-finish"), variant: "primary", onClick: () => { void done(true); }},
{ label: i18n.t("line-info.move-line-cancel"), onClick: () => { void done(false); } }
]
});
isMoving.value = true;
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, "Error saving line", err);
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, i18n.t("line-info.save-line-error"), err);
toasts.hideToast(`fm${context.id}-line-info-move`);
mapContext.value.components.map.fire('fmInteractionEnd');
@ -150,27 +152,27 @@
class="btn btn-secondary"
:class="{ active: showElevationPlot }"
@click="showElevationPlot = !showElevationPlot"
v-tooltip.right="`${showElevationPlot ? 'Hide' : 'Show'} elevation plot`"
v-tooltip.right="showElevationPlot ? i18n.t('line-info.hide-elevation-plot') : i18n.t('line-info.show-elevation-plot')"
>
<Icon icon="chart-line" :alt="`${showElevationPlot ? 'Hide' : 'Show'} elevation plot`"></Icon>
<Icon icon="chart-line" :alt="showElevationPlot ? i18n.t('line-info.hide-elevation-plot') : i18n.t('line-info.show-elevation-plot')"></Icon>
</button>
</div>
</div>
<div class="fm-search-box-collapse-point" v-if="!isMoving">
<dl class="fm-search-box-dl">
<dt class="distance">Distance</dt>
<dd class="distance">{{round(line.distance, 2)}}&#x202F;km <span v-if="line.time != null">({{formatRouteTime(line.time, line.mode)}})</span></dd>
<dt class="distance">{{i18n.t("line-info.distance")}}</dt>
<dd class="distance">{{formatDistance(line.distance)}} <span v-if="line.time != null">({{formatRouteTime(line.time, line.mode)}})</span></dd>
<template v-if="line.ascent != null">
<dt class="elevation">Climb/drop</dt>
<dt class="elevation">{{i18n.t("line-info.ascent-descent")}}</dt>
<dd class="elevation"><ElevationStats :route="line"></ElevationStats></dd>
</template>
<template v-if="line.ascent == null || !showElevationPlot">
<template v-for="field in client.types[line.typeId].fields" :key="field.name">
<dt>{{field.name}}</dt>
<dd v-html="formatField(field, line.data[field.name], true)"></dd>
<dt>{{formatFieldName(field.name)}}</dt>
<dd v-html="formatFieldValue(field, line.data[field.name], true)"></dd>
</template>
</template>
</dl>
@ -181,7 +183,7 @@
<div v-if="!isMoving" class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="line"
:label="i18n.t('line-info.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>
@ -199,7 +201,7 @@
size="sm"
@click="showEditDialog = true"
:disabled="isDeleting || mapContext.interaction"
>Edit data</button>
>{{i18n.t("line-info.edit-data")}}</button>
<button
v-if="!client.readonly && line.mode != 'track'"
@ -207,7 +209,7 @@
class="btn btn-secondary btn-sm"
@click="moveLine()"
:disabled="isDeleting || mapContext.interaction"
>Edit waypoints</button>
>{{i18n.t("line-info.edit-waypoints")}}</button>
<button
v-if="!client.readonly"
@ -217,7 +219,7 @@
:disabled="isDeleting || mapContext.interaction"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
Delete
{{i18n.t("line-info.delete")}}
</button>
</div>

Wyświetl plik

@ -7,7 +7,7 @@
import ModalDialog from "./ui/modal-dialog.vue";
import { injectContextRequired, requireClientContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import DropdownMenu from "./ui/dropdown-menu.vue";
import { getOrderedTypes } from "facilmap-utils";
import { formatTypeName, getOrderedTypes } from "facilmap-utils";
import Draggable from "vuedraggable";
import Icon from "./ui/icon.vue";
@ -32,7 +32,7 @@
try {
if (!await showConfirm({
title: "Delete type",
message: `Do you really want to delete the type “${type.name}”?`,
message: `Do you really want to delete the type “${formatTypeName(type.name)}”?`,
variant: "danger",
okLabel: "Delete"
})) {
@ -41,7 +41,7 @@
await client.value.deleteType({ id: type.id });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-manage-types-delete-${type.id}`, `Error deleting type “${type.name}`, err);
toasts.showErrorToast(`fm${context.id}-manage-types-delete-${type.id}`, `Error deleting type “${formatTypeName(type.name)}`, err);
} finally {
delete isDeleting.value[type.id];
}
@ -97,7 +97,7 @@
>
<template #item="{ element: type }">
<tr>
<td class="text-break">{{type.name}}</td>
<td class="text-break">{{formatTypeName(type.name)}}</td>
<td>{{type.type}}</td>
<td class="td-buttons">
<button

Wyświetl plik

@ -5,7 +5,7 @@
import { getZoomDestinationForMarker } from "../../utils/zoom";
import Icon from "../ui/icon.vue";
import Coordinates from "../ui/coordinates.vue";
import { formatField, normalizeMarkerName } from "facilmap-utils";
import { formatFieldName, formatFieldValue, formatTypeName, normalizeMarkerName } from "facilmap-utils";
import { computed, ref } from "vue";
import { useToasts } from "../ui/toasts/toasts.vue";
import { showConfirm } from "../ui/alert.vue";
@ -13,11 +13,13 @@
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
markerId: ID;
@ -35,7 +37,7 @@
const marker = computed(() => client.value.markers[props.markerId]);
const typeName = computed(() => client.value.types[marker.value.typeId].name);
const typeName = computed(() => formatTypeName(client.value.types[marker.value.typeId].name));
const showTypeName = computed(() => Object.values(client.value.types).filter((t) => t.type === 'marker').length > 1);
function move(): void {
@ -46,10 +48,10 @@
toasts.hideToast(`fm${context.id}-marker-info-delete`);
if (!await showConfirm({
title: "Delete marker",
message: `Do you really want to delete the marker “${normalizeMarkerName(marker.value.name)}”?`,
title: i18n.t("marker-info.delete-marker-title"),
message: i18n.t("marker-info.delete-marker-message", { name: normalizeMarkerName(marker.value.name) }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("marker-info.delete-marker-ok")
}))
return;
@ -58,7 +60,7 @@
try {
await client.value.deleteMarker({ id: props.markerId });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-marker-info-delete`, "Error deleting marker", err);
toasts.showErrorToast(`fm${context.id}-marker-info-delete`, i18n.t("marker-info.delete-marker-error"), err);
} finally {
isDeleting.value = false;
}
@ -88,19 +90,19 @@
</span>
</h2>
<dl class="fm-search-box-collapse-point fm-search-box-dl">
<dt class="pos">Coordinates</dt>
<dt class="pos">{{i18n.t("marker-info.coordinates")}}</dt>
<dd class="pos"><Coordinates :point="marker" :ele="marker.ele"></Coordinates></dd>
<template v-for="field in client.types[marker.typeId].fields" :key="field.name">
<dt>{{field.name}}</dt>
<dd v-html="formatField(field, marker.data[field.name], true)"></dd>
<dt>{{formatFieldName(field.name)}}</dt>
<dd v-html="formatFieldValue(field, marker.data[field.name], true)"></dd>
</template>
</dl>
<div class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="marker"
:label="i18n.t('marker-info.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>
@ -116,14 +118,14 @@
class="btn btn-secondary btn-sm"
@click="showEditDialog = true"
:disabled="isDeleting || mapContext.interaction"
>Edit data</button>
>{{i18n.t("marker-info.edit-data")}}</button>
<button
v-if="!client.readonly"
type="button"
class="btn btn-secondary btn-sm"
@click="move()"
:disabled="isDeleting || mapContext.interaction"
>Move</button>
>{{i18n.t("marker-info.move")}}</button>
<button
v-if="!client.readonly"
type="button"
@ -132,7 +134,7 @@
:disabled="isDeleting || mapContext.interaction"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
Delete
{{i18n.t("marker-info.delete")}}
</button>
</div>

Wyświetl plik

@ -11,12 +11,14 @@
import ZoomToObjectButton from "../ui/zoom-to-object-button.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import vTooltip from "../../utils/tooltip";
import { isLine, isMarker, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { formatTypeName, isLine, isMarker, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
objects: Array<Marker | Line>;
@ -66,10 +68,10 @@
toasts.hideToast(`fm${context.id}-multiple-info-delete`);
if (!props.objects || !await showConfirm({
title: "Delete objects",
message: `Do you really want to remove ${props.objects.length} objects?`,
title: i18n.t("multiple-info.delete-objects-title", { count: props.objects.length }),
message: i18n.t("multiple-info.delete-objects-message", { count: props.objects.length }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("multiple-info.delete-objects-ok")
}))
return;
@ -83,7 +85,7 @@
await client.value.deleteLine({ id: object.id });
}
} catch (err) {
toasts.showErrorToast(`fm${context.id}-multiple-info-delete`, "Error deleting objects", err);
toasts.showErrorToast(`fm${context.id}-multiple-info-delete`, i18n.t("multiple-info.delete-objects-error"), err);
} finally {
isDeleting.value = false;
}
@ -110,17 +112,17 @@
<span class="text-break">
<a href="javascript:" @click="emit('click-object', object, $event)">{{isMarker(object) ? normalizeMarkerName(object.name) : normalizeLineName(object.name)}}</a>
{{" "}}
<span class="result-type" v-if="client.types[object.typeId]">({{client.types[object.typeId].name}})</span>
<span class="result-type" v-if="client.types[object.typeId]">({{formatTypeName(client.types[object.typeId].name)}})</span>
</span>
<a href="javascript:" @click="zoomToObject(object)" v-tooltip.left="'Zoom to object'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="openObject(object)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a href="javascript:" @click="zoomToObject(object)" v-tooltip.left="i18n.t('multiple-info.zoom-to-object')"><Icon icon="zoom-in" :alt="i18n.t('multiple-info.zoom')"></Icon></a>
<a href="javascript:" @click="openObject(object)" v-tooltip.right="i18n.t('multiple-info.show-details')"><Icon icon="arrow-right" :alt="i18n.t('multiple-info.details')"></Icon></a>
</li>
</ul>
<div class="btn-toolbar mt-2">
<ZoomToObjectButton
v-if="zoomDestination"
label="selection"
:label="i18n.t('multiple-info.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>
@ -133,7 +135,7 @@
:disabled="isDeleting || mapContext.interaction"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
Delete
{{i18n.t("multiple-info.delete")}}
</button>
</div>
</div>

Wyświetl plik

@ -2,13 +2,15 @@
import OverpassForm from "./overpass-form.vue";
import SearchBoxTab from "../search-box/search-box-tab.vue";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const i18n = useI18n();
</script>
<template>
<SearchBoxTab
title="POIs"
:title="i18n.t('overpass-form-tab.pois')"
:id="`fm${context.id}-overpass-form-tab`"
class="fm-overpass-form-tab"
>

Wyświetl plik

@ -1,22 +1,25 @@
<script setup lang="ts">
import { getOverpassPreset, overpassPresets, validateOverpassQuery } from "facilmap-leaflet";
import { getAllOverpassPresets, getOverpassPreset, validateOverpassQuery } from "facilmap-leaflet";
import { computed, ref, watch } from "vue";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import { T, useI18n } from "../../utils/i18n";
import { sortBy } from "lodash-es";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const activeTab = ref(0);
const searchTerm = ref("");
const customQuery = ref("");
const categories = computed(() => {
return overpassPresets.map((cat) => {
const presets = cat.presets.map((presets) => presets.map((preset) => ({
return getAllOverpassPresets().map((cat) => {
const presets = cat.presets.map((presets) => sortBy(presets.map((preset) => ({
...preset,
isChecked: mapContext.value.overpassPresets.some((p) => p.key === preset.key)
})));
})), (preset) => preset.label.toLowerCase()));
return {
...cat,
presets,
@ -73,7 +76,7 @@
class="form-control fm-autofocus"
type="search"
v-model="searchTerm"
placeholder="Filter…"
:placeholder="i18n.t('overpass-form.filter')"
/>
<hr />
@ -168,13 +171,26 @@
<hr />
<p>
Enter an <a href="https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#The_Query_Statement" target="_blank">Overpass query statement</a>
here. Settings and an <code>out</code> statement are added automatically in the background. For ways and relations, a marker will be shown at
the geometric centre, no lines or polygons are drawn.
<T k="overpass-form.custom-explanation-1">
<template #statement>
<a :href="i18n.t('overpass-form.custom-explanation-1-interpolation-statement-url')" target="_blank" rel="noopener">
{{i18n.t("overpass-form.custom-explanation-1-interpolation-statement")}}
</a>
</template>
<template #out>
<code>out</code>
</template>
</T>
</p>
<p>
Example queries are <code>nwr[amenity=parking]</code> to get parking places or
<code>(nwr[amenity=atm];nwr[amenity=bank][atm][atm!=no];)</code> for ATMs.
<T k="overpass-form.custom-explanation-2">
<template #parking>
<code>nwr[amenity=parking]</code>
</template>
<template #atm>
<code>(nwr[amenity=atm];nwr[amenity=bank][atm][atm!=no];)</code>
</template>
</T>
</p>
</template>
@ -186,7 +202,7 @@
class="btn btn-secondary"
:class="{ active: mapContext.overpassIsCustom }"
@click="toggleIsCustom()"
>Custom query</button>
>{{i18n.t("overpass-form.custom-query")}}</button>
</div>
</div>
</template>

Wyświetl plik

@ -5,10 +5,13 @@
import { useEventListener } from "../../utils/utils";
import { computed } from "vue";
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { formatPOIName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const searchBoxContext = requireSearchBoxContext(context);
const i18n = useI18n();
useEventListener(mapContext, "open-selection", handleOpenSelection);
@ -37,7 +40,7 @@
<template v-if="elements.length > 0">
<SearchBoxTab
:id="`fm${context.id}-overpass-info-tab`"
:title="elements.length == 1 ? (elements[0].tags.name || 'Unnamed POI') : `${elements.length} POIs`"
:title="elements.length == 1 ? (formatPOIName(elements[0].tags.name)) : i18n.t('overpass-info-tab.tab-label', { count: elements.length })"
isCloseable
@close="close()"
>

Wyświetl plik

@ -1,5 +1,5 @@
<script setup lang="ts">
import { renderOsmTag } from "facilmap-utils";
import { formatCoordinates, formatPOIName, renderOsmTag } from "facilmap-utils";
import Icon from "../ui/icon.vue";
import { getZoomDestinationForMarker } from "../../utils/zoom";
import type { OverpassElement } from "facilmap-leaflet";
@ -11,6 +11,9 @@
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
import { overpassElementsToMarkersWithTags } from "../../utils/add";
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
import { useI18n } from "../../utils/i18n";
const i18n = useI18n();
const props = withDefaults(defineProps<{
element: OverpassElement;
@ -30,7 +33,7 @@
const routeDestination = computed<RouteDestination>(() => {
return {
query: `${props.element.lat},${props.element.lon}`
query: formatCoordinates(props.element)
};
});
@ -41,10 +44,10 @@
<div class="fm-overpass-info">
<h2>
<a v-if="showBackButton" href="javascript:" @click="emit('back')"><Icon icon="arrow-left"></Icon></a>
{{element.tags.name || 'Unnamed POI'}}
{{formatPOIName(element.tags.name)}}
</h2>
<dl class="fm-search-box-collapse-point fm-search-box-dl">
<dt>Coordinates</dt>
<dt>{{i18n.t("overpass-info.coordinates")}}</dt>
<dd><Coordinates :point="element"></Coordinates></dd>
<template v-for="(value, key) in element.tags" :key="key">
@ -55,7 +58,7 @@
<div class="btn-toolbar">
<ZoomToObjectButton
label="POI"
:label="i18n.t('overpass-info.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>

Wyświetl plik

@ -10,10 +10,13 @@
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { overpassElementsToMarkersWithTags } from "../../utils/add";
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
import { formatPOIName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const i18n = useI18n();
const props = defineProps<{
elements: OverpassElement[];
@ -55,17 +58,17 @@
<ul class="list-group">
<li v-for="element in elements" :key="element.id" class="list-group-item active">
<span>
<a href="javascript:" @click="emit('click-element', element, $event)">{{element.tags.name || 'Unnamed POI'}}</a>
<a href="javascript:" @click="emit('click-element', element, $event)">{{formatPOIName(element.tags.name)}}</a>
</span>
<a href="javascript:" @click="zoomToElement(element)" v-tooltip.left="'Zoom to object'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="openElement(element)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a href="javascript:" @click="zoomToElement(element)" v-tooltip.left="i18n.t('overpass-multiple-info.zoom-to-object')"><Icon icon="zoom-in" :alt="i18n.t('overpass-multiple-info.zoom')"></Icon></a>
<a href="javascript:" @click="openElement(element)" v-tooltip.right="i18n.t('overpass-multiple-info.show-details')"><Icon icon="arrow-right" :alt="i18n.t('overpass-multiple-info.details')"></Icon></a>
</li>
</ul>
<div v-if="client.padData && !client.readonly" class="btn-toolbar">
<div v-if="client.padData && !client.readonly" class="btn-toolbar mt-2">
<ZoomToObjectButton
v-if="zoomDestination"
label="selection"
:label="i18n.t('overpass-multiple-info.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>

Wyświetl plik

@ -4,11 +4,13 @@
import { getUniqueId, getZodValidator, validateRequired } from "../../utils/utils";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import CopyToClipboardInput from "../ui/copy-to-clipboard-input.vue";
import { useI18n } from "../../utils/i18n";
const idProps = ["id", "writeId", "adminId"] as const;
type IdProp = typeof idProps[number];
const context = injectContextRequired();
const i18n = useI18n();
const props = defineProps<{
padData: PadData<CRU.CREATE>;
@ -33,7 +35,7 @@
function validateDistinctPadId(id: string) {
if (idProps.some((p) => p !== props.idProp && props.padData[p] === id)) {
return "The same link cannot be used for different access levels.";
return i18n.t("pad-id-edit.unique-id-error");
}
}
</script>
@ -45,8 +47,8 @@
<CopyToClipboardInput
v-model="value"
:prefix="context.baseUrl"
shortDescription="Map link"
longDescription="The map link"
successTitle="Map link copied"
successMessage="The map link was copied to the clipboard."
:fullUrl="`${context.baseUrl}${encodeURIComponent(value)}`"
:validators="[validateRequired, getZodValidator(padIdValidator), validateDistinctPadId]"
></CopyToClipboardInput>

Wyświetl plik

@ -10,11 +10,13 @@
import PadIdEdit from "./pad-id-edit.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "../ui/validated-form/validated-field.vue";
import { T, useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
proposedAdminId?: string;
@ -30,6 +32,7 @@
const id = getUniqueId("fm-pad-settings");
const isDeleting = ref(false);
const deleteConfirmation = ref("");
const expectedDeleteConfirmation = computed(() => i18n.t('pad-settings-dialog.delete-code'));
const initialPadData: PadData<CRU.CREATE> | undefined = props.isCreate ? {
name: "",
@ -67,7 +70,7 @@
await client.value.editPad(padData.value);
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, props.isCreate ? "Error creating map" : "Error saving map settings", err);
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, props.isCreate ? i18n.t("pad-settings-dialog.create-map-error") : i18n.t("pad-settings-dialog.save-map-error"), err);
}
};
@ -75,10 +78,10 @@
toasts.hideToast(`fm${context.id}-pad-settings-error`);
if (!await showConfirm({
title: "Delete map",
message: `Are you sure you want to delete the map “${padData.value.name}”? Deleted maps cannot be restored!`,
title: i18n.t("pad-settings-dialog.delete-map-title"),
message: i18n.t("pad-settings-dialog.delete-map-message", { name: padData.value.name }),
variant: "danger",
okLabel: "Delete map"
okLabel: i18n.t("pad-settings-dialog.delete-map-ok")
})) {
return;
}
@ -89,7 +92,7 @@
await client.value.deletePad();
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, "Error deleting map", err);
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, i18n.t("pad-settings-dialog.delete-map-error"), err);
} finally {
isDeleting.value = false;
}
@ -98,13 +101,13 @@
<template>
<ModalDialog
:title="props.isCreate ? 'Create collaborative map' : 'Map settings'"
:title="props.isCreate ? i18n.t('pad-settings-dialog.title-create') : i18n.t('pad-settings-dialog.title-edit')"
class="fm-pad-settings"
:noCancel="props.noCancel"
:isBusy="isDeleting"
:isCreate="props.isCreate"
:isModified="isModified"
:okLabel="props.isCreate ? 'Create' : undefined"
:okLabel="props.isCreate ? i18n.t('pad-settings-dialog.create-button') : undefined"
ref="modalRef"
@submit="$event.waitUntil(save())"
@hide="emit('hide')"
@ -115,24 +118,24 @@
:padData="padData"
idProp="adminId"
v-model="padData.adminId"
label="Admin link"
description="When opening the map through this link, all parts of the map can be edited, including the map settings, object types and views."
:label="i18n.t('pad-settings-dialog.admin-link-label')"
:description="i18n.t('pad-settings-dialog.admin-link-description')"
></PadIdEdit>
<PadIdEdit
:padData="padData"
idProp="writeId"
v-model="padData.writeId"
label="Editable link"
description="When opening the map through this link, markers and lines can be added, changed and deleted, but the map settings, object types and views cannot be modified."
:label="i18n.t('pad-settings-dialog.write-link-label')"
:description="i18n.t('pad-settings-dialog.write-link-description')"
></PadIdEdit>
<PadIdEdit
:padData="padData"
idProp="id"
v-model="padData.id"
label="Read-only link"
description="When opening the map through this link, markers, lines and views can be seen, but nothing can be changed."
:label="i18n.t('pad-settings-dialog.read-link-label')"
:description="i18n.t('pad-settings-dialog.read-link-description')"
></PadIdEdit>
<ValidatedField
@ -141,7 +144,7 @@
:validators="[getZodValidator(padDataValidator.update.shape.name)]"
>
<template #default="slotProps">
<label :for="`${id}-pad-name-input`" class="col-sm-3 col-form-label">Map name</label>
<label :for="`${id}-pad-name-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.map-name")}}</label>
<div class="col-sm-9 position-relative">
<input
:id="`${id}-pad-name-input`"
@ -158,7 +161,7 @@
</ValidatedField>
<div class="row mb-3">
<label :for="`${id}-search-engines-input`" class="col-sm-3 col-form-label">Search engines</label>
<label :for="`${id}-search-engines-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.search-engines")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -168,17 +171,17 @@
v-model="padData.searchEngines"
/>
<label :for="`${id}-search-engines-input`" class="form-check-label">
Accessible for search engines
{{i18n.t("pad-settings-dialog.search-engines-label")}}
</label>
</div>
<div class="form-text">
If this is enabled, search engines like Google will be allowed to add the read-only version of this map.
{{i18n.t("pad-settings-dialog.search-engines-description")}}
</div>
</div>
</div>
<div class="row mb-3">
<label :for="`${id}-description-input`" class="col-sm-3 col-form-label">Short description</label>
<label :for="`${id}-description-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.map-description")}}</label>
<div class="col-sm-9">
<input
:id="`${id}-description-input`"
@ -187,13 +190,13 @@
v-model="padData.description"
/>
<div class="form-text">
This description will be shown under the result in search engines.
{{i18n.t("pad-settings-dialog.map-description-description")}}
</div>
</div>
</div>
<div class="row mb-3">
<label :for="`${id}-cluster-markers-input`" class="col-sm-3 col-form-label">Search engines</label>
<label :for="`${id}-cluster-markers-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.cluster-markers")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -203,17 +206,17 @@
v-model="padData.clusterMarkers"
/>
<label :for="`${id}-cluster-markers-input`" class="form-check-label">
Cluster markers
{{i18n.t("pad-settings-dialog.cluster-markers-label")}}
</label>
</div>
<div class="form-text">
If enabled, when there are many markers in one area, they will be replaced by a placeholder at low zoom levels. This improves performance on maps with many markers.
{{i18n.t("pad-settings-dialog.cluster-markers-description")}}
</div>
</div>
</div>
<div class="row mb-3">
<label :for="`${id}-legend1-input`" class="col-sm-3 col-form-label">Legend text</label>
<label :for="`${id}-legend1-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.legend-text")}}</label>
<div class="col-sm-9">
<textarea
:id="`${id}-legend1-input`"
@ -228,7 +231,11 @@
v-model="padData.legend2"
></textarea>
<div class="form-text">
Text that will be shown above and below the legend. Can be formatted with <a href="http://commonmark.org/help/" target="_blank">Markdown</a>.
<T k="pad-settings-dialog.legend-text-description">
<template #markdown>
<a href="http://commonmark.org/help/" target="_blank">{{i18n.t("pad-settings-dialog.legend-text-description-interpolation-markdown")}}</a>
</template>
</T>
</div>
</div>
</div>
@ -238,7 +245,7 @@
<hr/>
<div class="row mb-3">
<label :for="`${id}-delete-input`" class="col-sm-3 col-form-label">Delete map</label>
<label :for="`${id}-delete-input`" class="col-sm-3 col-form-label">{{i18n.t("pad-settings-dialog.delete-map")}}</label>
<div class="col-sm-9">
<div class="input-group">
<input
@ -252,20 +259,24 @@
:form="`${id}-delete-form`"
class="btn btn-danger"
type="submit"
:disabled="isDeleting || modalRef?.formData?.isSubmitting || deleteConfirmation != 'DELETE'"
:disabled="isDeleting || modalRef?.formData?.isSubmitting || deleteConfirmation != expectedDeleteConfirmation"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
Delete map
{{i18n.t("pad-settings-dialog.delete-map-button")}}
</button>
</div>
<div class="form-text">
To delete this map, type <code>DELETE</code> into the field and click the Delete map button.
<T k="pad-settings-dialog.delete-description">
<template #code>
<code>{{expectedDeleteConfirmation}}</code>
</template>
</T>
</div>
</div>
</div>
</template>
</ModalDialog>
<form :id="`${id}-delete-form`" @submit.prevent="deleteConfirmation == 'DELETE' && deletePad()">
<form :id="`${id}-delete-form`" @submit.prevent="deleteConfirmation == expectedDeleteConfirmation && deletePad()">
</form>
</template>

Wyświetl plik

@ -5,9 +5,11 @@
import { readonly, ref, toRef } from "vue";
import { injectContextRequired, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import type { WritableRouteFormTabContext } from "../facil-map-context-provider/route-form-tab-context";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const searchBoxContext = requireSearchBoxContext(context);
const i18n = useI18n();
const routeForm = ref<InstanceType<typeof RouteForm>>();
@ -40,7 +42,7 @@
<template>
<SearchBoxTab
title="Route"
:title="i18n.t('route-form-tab.tab-label')"
:id="`fm${context.id}-route-form-tab`"
:hashQuery="hashQuery"
class="fm-route-form-tab"

Wyświetl plik

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, markRaw, onBeforeUnmount, onMounted, ref, watch, watchEffect } from "vue";
import { computed, markRaw, onBeforeUnmount, onMounted, ref, toRaw, watch, watchEffect } from "vue";
import Icon from "../ui/icon.vue";
import { formatRouteTime, isSearchId, normalizeMarkerName, round, splitRouteQuery } from "facilmap-utils";
import { decodeRouteQuery, encodeRouteQuery, formatCoordinates, formatDistance, formatRouteMode, formatRouteTime, formatTypeName, isSearchId, normalizeMarkerName } from "facilmap-utils";
import { useToasts } from "../ui/toasts/toasts.vue";
import type { ExportFormat, FindOnMapResult, SearchResult } from "facilmap-types";
import { getMarkerIcon, type HashQuery, MarkerLayer, RouteLayer } from "facilmap-leaflet";
@ -10,7 +10,7 @@
import Draggable from "vuedraggable";
import RouteMode from "../ui/route-mode.vue";
import DraggableLines from "leaflet-draggable-lines";
import { throttle } from "lodash-es";
import { cloneDeep, throttle } from "lodash-es";
import ElevationStats from "../ui/elevation-stats.vue";
import ElevationPlot from "../ui/elevation-plot.vue";
import { isMapResult } from "../../utils/search";
@ -22,6 +22,7 @@
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
import ExportDropdown from "../ui/export-dropdown.vue";
import { useI18n } from "../../utils/i18n";
type SearchSuggestion = SearchResult;
type MapSuggestion = FindOnMapResult & { kind: "marker" };
@ -38,7 +39,7 @@
}
function makeCoordDestination(latlng: LatLng) {
const disp = round(latlng.lat, 5) + "," + round(latlng.lng, 5);
const disp = formatCoordinates({ lat: latlng.lat, lon: latlng.lng });
let suggestion = {
lat: latlng.lat,
lon: latlng.lng,
@ -79,6 +80,7 @@
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const submitButton = ref<HTMLButtonElement>();
@ -107,8 +109,7 @@
) : (
[{ query: "" }, { query: "" }]
));
const submittedQuery = ref<string>();
const submittedQueryDescription = ref<string>();
const submittedQuery = ref<{ destinations: Destination[]; mode: string }>();
const routeError = ref<string>();
const hoverDestinationIdx = ref<number>();
const hoverInsertIdx = ref<number>();
@ -199,9 +200,17 @@
const hashQuery = computed(() => {
if (submittedQuery.value) {
return {
query: submittedQuery.value,
query: encodeRouteQuery({
queries: submittedQuery.value.destinations.map((dest) => (getSelectedSuggestionId(dest) ?? dest.query)),
mode: submittedQuery.value.mode
}),
...(zoomDestination.value ? normalizeZoomDestination(mapContext.value.components.map, zoomDestination.value) : {}),
description: `Route from ${submittedQueryDescription.value}`
description: i18n.t("route-form.route-description-outer", {
inner: i18n.t("route-form.route-description-inner", {
destinations: submittedQuery.value.destinations.map((dest) => (getSelectedSuggestionName(dest) ?? dest.query)).join(i18n.t("route-form.route-description-inner-joiner")),
mode: formatRouteMode(submittedQuery.value.mode)
})
})
};
} else
return undefined;
@ -344,7 +353,7 @@
return; // The destination has changed in the meantime
console.warn(err.stack || err);
toasts.showErrorToast(`fm${context.id}-route-form-suggestion-error-${idx}`, `Error finding destination “${query}`, err);
toasts.showErrorToast(`fm${context.id}-route-form-suggestion-error-${idx}`, i18n.t("route-form.find-destination-error", { query }), err);
} finally {
resolveLoadingPromise();
}
@ -411,29 +420,15 @@
try {
const mode = routeMode.value;
submittedQuery.value = [
destinations.value.map((dest) => (getSelectedSuggestionId(dest) ?? dest.query)).join(" to "),
mode
].join(" by ");
submittedQueryDescription.value = [
destinations.value.map((dest) => (getSelectedSuggestionName(dest) ?? dest.query)).join(" to "),
mode
].join(" by ");
submittedQuery.value = { destinations: cloneDeep(toRaw(destinations.value)), mode };
await Promise.all(destinations.value.map((dest) => loadSuggestions(dest)));
const points = destinations.value.map((dest) => getSelectedSuggestion(dest));
submittedQuery.value = [
destinations.value.map((dest) => (getSelectedSuggestionId(dest) ?? dest.query)).join(" to "),
mode
].join(" by ");
submittedQueryDescription.value = [
destinations.value.map((dest) => (getSelectedSuggestionName(dest) ?? dest.query)).join(" to "),
mode
].join(" by ");
submittedQuery.value = { destinations: cloneDeep(toRaw(destinations.value)), mode };
if(points.some((point) => point == null)) {
routeError.value = "Some destinations could not be found.";
routeError.value = i18n.t("route-form.some-destinations-not-found");
return;
}
@ -446,7 +441,7 @@
if (route && zoom)
flyTo(mapContext.value.components.map, getZoomDestinationForRoute(route), smooth);
} catch (err: any) {
toasts.showErrorToast(`fm${context.id}-route-form-error`, "Error calculating route", err);
toasts.showErrorToast(`fm${context.id}-route-form-error`, i18n.t("route-form.route-calculation-error"), err);
}
}
@ -463,7 +458,6 @@
function reset(): void {
toasts.hideToast(`fm${context.id}-route-form-error`);
submittedQuery.value = undefined;
submittedQueryDescription.value = undefined;
routeError.value = undefined;
if(suggestionMarker.value) {
@ -499,7 +493,7 @@
function setQuery(query: string, zoom = true, smooth = true): void {
clear();
const split = splitRouteQuery(query);
const split = decodeRouteQuery(query);
destinations.value = split.queries.map((query) => ({ query }));
while (destinations.value.length < 2)
destinations.value.push({ query: "" });
@ -544,13 +538,13 @@
>
<span class="input-group-text px-2">
<a href="javascript:" class="fm-drag-handle" @contextmenu.prevent>
<Icon icon="resize-vertical" alt="Reorder"></Icon>
<Icon icon="resize-vertical" :alt="i18n.t('route-form.reorder-alt')"></Icon>
</a>
</span>
<input
class="form-control"
v-model="destination.query"
:placeholder="idx == 0 ? 'From' : idx == destinations.length-1 ? 'To' : 'Via'"
:placeholder="idx == 0 ? i18n.t('route-form.from-placeholder') : idx == destinations.length-1 ? i18n.t('route-form.to-placeholder') : i18n.t('route-form.via-placeholder')"
:tabindex="idx+1"
:class="{
'is-invalid': destinationsMeta[idx].isInvalid,
@ -575,14 +569,14 @@
class="dropdown-item fm-route-form-suggestions-zoom"
:class="{ active: suggestion === getSelectedSuggestion(destination) }"
@click.capture.stop.prevent="suggestionZoom(suggestion)"
><Icon icon="zoom-in" alt="Zoom"></Icon></a>
><Icon icon="zoom-in" :alt="i18n.t('route-form.zoom-alt')"></Icon></a>
<a
href="javascript:"
class="dropdown-item"
:class="{ active: suggestion === getSelectedSuggestion(destination) }"
@click="destination.selectedSuggestion = suggestion; reroute(true)"
>{{suggestion.name}} ({{client.types[suggestion.typeId].name}})</a>
>{{suggestion.name}} ({{formatTypeName(client.types[suggestion.typeId].name)}})</a>
</li>
</template>
@ -600,7 +594,7 @@
class="dropdown-item fm-route-form-suggestions-zoom"
:class="{ active: suggestion === getSelectedSuggestion(destination) }"
@click.capture.stop.prevent="suggestionZoom(suggestion)"
><Icon icon="zoom-in" alt="Zoom"></Icon></a>
><Icon icon="zoom-in" :alt="i18n.t('route-form.zoom-alt')"></Icon></a>
<a
href="javascript:"
class="dropdown-item"
@ -616,9 +610,9 @@
type="button"
class="btn btn-secondary"
@click="removeDestination(idx); reroute(false)"
v-tooltip.right="'Remove this destination'"
v-tooltip.right="i18n.t('route-form.remove-destination-tooltip')"
>
<Icon icon="minus" alt="Remove" size="1.0em"></Icon>
<Icon icon="minus" :alt="i18n.t('route-form.remove-destination-alt')" size="1.0em"></Icon>
</button>
</div>
</div>
@ -633,10 +627,10 @@
type="button"
class="btn btn-secondary"
@click="addDestination()"
v-tooltip.bottom="'Add another destination'"
v-tooltip.bottom="i18n.t('route-form.add-destination-tooltip')"
:tabindex="destinations.length+1"
>
<Icon icon="plus" alt="Add"></Icon>
<Icon icon="plus" :alt="i18n.t('route-form.add-destination-alt')"></Icon>
</button>
<RouteMode v-model="routeMode" :tabindex="destinations.length+2" tooltip-placement="bottom"></RouteMode>
@ -646,16 +640,16 @@
class="btn btn-primary flex-grow-1"
:tabindex="destinations.length+7"
ref="submitButton"
>Go!</button>
>{{i18n.t("route-form.submit")}}</button>
<button
v-if="hasRoute && !props.noClear"
type="button"
class="btn btn-secondary"
:tabindex="destinations.length+8"
@click="reset()"
v-tooltip.right="'Clear route'"
v-tooltip.right="i18n.t('route-form.clear-route-tooltip')"
>
<Icon icon="remove" alt="Clear"></Icon>
<Icon icon="remove" :alt="i18n.t('route-form.clear-route-alt')"></Icon>
</button>
</div>
@ -669,11 +663,11 @@
<hr />
<dl class="fm-search-box-dl">
<dt>Distance</dt>
<dd>{{round(routeObj.distance, 2)}}&#x202F;km <span v-if="routeObj.time != null">({{formatRouteTime(routeObj.time, routeObj.mode)}})</span></dd>
<dt>{{i18n.t("route-form.distance")}}</dt>
<dd>{{formatDistance(routeObj.distance)}} <span v-if="routeObj.time != null">({{formatRouteTime(routeObj.time, routeObj.mode)}})</span></dd>
<template v-if="routeObj.ascent != null">
<dt>Climb/drop</dt>
<dt>{{i18n.t("route-form.ascent-descent")}}</dt>
<dd><ElevationStats :route="routeObj"></ElevationStats></dd>
</template>
</dl>
@ -683,7 +677,7 @@
<div v-if="showToolbar && !client.readonly" class="btn-toolbar" role="group">
<ZoomToObjectButton
v-if="zoomDestination"
label="route"
:label="i18n.t('route-form.zoom-to-object-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>
@ -695,7 +689,7 @@
></AddToMapDropdown>
<ExportDropdown
filename="FacilMap route"
:filename="i18n.t('route-form.export-filename')"
:getExport="getExport"
size="sm"
></ExportDropdown>

Wyświetl plik

@ -6,9 +6,11 @@
import type { SearchBoxEventMap, SearchBoxTab, WritableSearchBoxContext } from "../facil-map-context-provider/search-box-context";
import mitt from "mitt";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const tabs = reactive(new Map<string, SearchBoxTab>());
const activeTabId = ref<string | undefined>();
@ -228,7 +230,7 @@
href="javascript:"
@click="tab.onClose()"
draggable="false"
><Icon icon="remove" alt="Close"></Icon></a>
><Icon icon="remove" :alt="i18n.t('search-box.close-alt')"></Icon></a>
</li>
</ul>
</div>
@ -243,7 +245,7 @@
v-show="!context.isNarrow"
href="javascript:"
class="fm-search-box-resize"
v-tooltip.right="'Drag to resize, click to reset'"
v-tooltip.right="i18n.t('search-box.resize-tooltip')"
ref="resizeHandleRef"
><Icon icon="resize-horizontal"></Icon></a>
</div>

Wyświetl plik

@ -7,10 +7,12 @@
import SearchBoxTab from "../search-box/search-box-tab.vue";
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import type { WritableSearchFormTabContext } from "../facil-map-context-provider/search-form-tab-context";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const searchBoxContext = requireSearchBoxContext(context);
const i18n = useI18n();
const searchForm = ref<InstanceType<typeof SearchForm>>();
@ -38,7 +40,7 @@
<template>
<SearchBoxTab
:id="`fm${context.id}-search-form-tab`"
title="Search"
:title="i18n.t('search-form-tab.tab-label')"
:hashQuery="hashQuery"
class="fm-search-form-tab"
>

Wyświetl plik

@ -14,6 +14,7 @@
import { computed, reactive, ref, watch } from "vue";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const emit = defineEmits<{
"hash-query-change": [query: HashQuery | undefined];
@ -24,6 +25,7 @@
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const layerId = Util.stamp(mapContext.value.components.searchResultsLayer);
@ -49,10 +51,10 @@
return {
query: loadedSearchString.value,
...(zoomDestination.value && normalizeZoomDestination(mapContext.value.components.map, zoomDestination.value)),
description: `Search for ${loadedSearchString.value}`
description: i18n.t("search-form.search-description", { query: loadedSearchString.value })
};
} else if (loadingSearchString.value)
return { query: loadingSearchString.value, description: `Search for ${loadedSearchString.value}` };
return { query: loadingSearchString.value, description: i18n.t("search-form.search-description", { query: loadedSearchString.value }) };
else
return undefined;
});
@ -132,7 +134,7 @@
}
}
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-form-error`, "Search error", err);
toasts.showErrorToast(`fm${context.id}-search-form-error`, i18n.t("search-form.search-error"), err);
return;
}
}
@ -194,7 +196,7 @@
type="submit"
class="btn btn-secondary"
>
<Icon icon="search" alt="Search"></Icon>
<Icon icon="search" :alt="i18n.t('search-form.search-alt')"></Icon>
</button>
<button
v-if="searchResults || mapResults || fileResult"
@ -202,7 +204,7 @@
class="btn btn-secondary"
@click="reset()"
>
<Icon icon="remove" alt="Clear"></Icon>
<Icon icon="remove" :alt="i18n.t('search-form.clear-alt')"></Icon>
</button>
<DropdownMenu noWrapper>
<li>
@ -211,7 +213,7 @@
class="dropdown-item"
@click.capture.stop.prevent="storage.autoZoom = !storage.autoZoom"
>
<Icon :icon="storage.autoZoom ? 'check' : 'unchecked'"></Icon> Auto-zoom to results
<Icon :icon="storage.autoZoom ? 'check' : 'unchecked'"></Icon> {{i18n.t("search-form.auto-zoom")}}
</a>
</li>
@ -221,7 +223,7 @@
class="dropdown-item"
@click.capture.stop.prevent="storage.zoomToAll = !storage.zoomToAll"
>
<Icon :icon="storage.zoomToAll ? 'check' : 'unchecked'"></Icon> Zoom to all results
<Icon :icon="storage.zoomToAll ? 'check' : 'unchecked'"></Icon> {{i18n.t("search-form.zoom-to-all")}}
</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -94,7 +94,7 @@
<div class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="search result"
label="Zoom to search result"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>

Wyświetl plik

@ -9,11 +9,13 @@
import { useToasts } from "../ui/toasts/toasts.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { type LineWithTags, type MarkerWithTags, addToMap, searchResultToLineWithTags, searchResultToMarkerWithTags } from "../../utils/add";
import { getOrderedTypes } from "facilmap-utils";
import { formatTypeName, getOrderedTypes } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
customTypes?: FileResultObject["types"];
@ -43,25 +45,25 @@
for (const type of orderedTypes.value) {
if (type.name == customType.name && type.type == customType.type)
recommendedOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${type.name}` });
recommendedOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
if (client.value.writable == 2 && !typeExists(client.value, customType))
recommendedOptions.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
recommendedOptions.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
recommendedOptions.push({ key: "false1", value: false, text: "Do not import" });
recommendedOptions.push({ key: "false1", value: false, text: i18n.t("custom-import-dialog.no-import") });
const otherOptions: Option[] = [];
for (const type of orderedTypes.value) {
if (type.name != customType.name && type.type == customType.type)
otherOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${type.name}` });
otherOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
for (const [customTypeId2, customType2] of Object.entries(props.customTypes)) {
if (client.value.writable == 2 && customType2.type == customType.type && customTypeId2 != customTypeId && !typeExists(client.value, customType2))
otherOptions.push({ key: `i${customTypeId2}`, value: `i${customTypeId2}`, text: `Import type “${customType2.name}` });
otherOptions.push({ key: `i${customTypeId2}`, value: `i${customTypeId2}`, text: i18n.t("custom-import-dialog.import-type", { name: customType2.name }) });
}
@ -89,17 +91,17 @@
const untypedMarkerMappingOptions = computed(() => {
const options: Array<{ key: string; value: string | false; text: string }> = [];
options.push({ key: "false", value: false, text: "Do not import" });
options.push({ key: "false", value: false, text: i18n.t("custom-import-dialog.no-import") });
for (const customTypeId of Object.keys(props.customTypes)) {
const customType = props.customTypes[customTypeId as any];
if (client.value.writable && customType.type == "marker" && !typeExists(client.value, customType))
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
}
for (const type of orderedTypes.value) {
if (type.type == "marker")
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${type.name}` });
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
return options;
@ -107,17 +109,17 @@
const untypedLineMappingOptions = computed(() => {
const options: Array<{ key: string; value: string | false; text: string }> = [];
options.push({ key: "false", value: false, text: "Do not import" });
options.push({ key: "false", value: false, text: i18n.t("custom-import-dialog.no-import") });
for (const customTypeId of Object.keys(props.customTypes)) {
const customType = props.customTypes[customTypeId as any];
if (client.value.writable && customType.type == "line")
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
}
for (const type of orderedTypes.value) {
if (type.type == "line")
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${type.name}` });
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
return options;
@ -155,17 +157,17 @@
modalRef.value?.modal.hide();
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, "Error importing to map", err);
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, i18n.t("custom-import-dialog.import-error"), err);
}
}
</script>
<template>
<ModalDialog
title="Custom Import"
:title="i18n.t('custom-import-dialog.dialog-title')"
class="fm-search-results-custom-import"
isCreate
okLabel="Import"
:okLabel="i18n.t('custom-import-dialog.ok-label')"
@submit="$event.waitUntil(save())"
ref="modalRef"
@hidden="emit('hidden')"
@ -173,14 +175,27 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Map to</th>
<th>{{i18n.t("custom-import-dialog.type")}}</th>
<th>{{i18n.t("custom-import-dialog.map-to")}}</th>
</tr>
</thead>
<tbody>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<tr v-for="(options, importTypeId) in customMappingOptions">
<td><label :for="`${id}-map-type-${importTypeId}`">{{customTypes[importTypeId as number].type == 'marker' ? 'Markers' : 'Lines'}} of type {{customTypes[importTypeId as number].name}} ({{activeFileResultsByType[importTypeId as number].length}})</label></td>
<td>
<label :for="`${id}-map-type-${importTypeId}`">
{{customTypes[importTypeId as number].type == 'marker'
? i18n.t("custom-import-dialog.markers", {
typeName: customTypes[importTypeId as number].name,
count: activeFileResultsByType[importTypeId as number].length
})
: i18n.t("custom-import-dialog.lines", {
typeName: customTypes[importTypeId as number].name,
count: activeFileResultsByType[importTypeId as number].length
})
}}
</label>
</td>
<td>
<select :id="`${id}-map-type-${importTypeId}`" v-model="customMapping[importTypeId as number]">
<option v-for="option in options" :key="option.key" :value="option.value">{{option.text}}</option>
@ -188,7 +203,7 @@
</td>
</tr>
<tr v-if="untypedMarkers.length > 0">
<td><label :for="`${id}-map-untyped-markers`">Untyped markers ({{untypedMarkers.length}})</label></td>
<td><label :for="`${id}-map-untyped-markers`">{{i18n.t("custom-import-dialog.untyped-markers", { count: untypedMarkers.length })}}</label></td>
<td>
<select :id="`${id}-map-untyped-markers`" v-model="untypedMarkerMapping">
<option v-for="option in untypedMarkerMappingOptions" :key="option.key" :value="option.value">{{option.text}}</option>
@ -196,7 +211,7 @@
</td>
</tr>
<tr v-if="untypedLines.length > 0">
<td><label :for="`${id}-map-untyped-lines`">Untyped lines/polygons ({{untypedLines.length}})</label></td>
<td><label :for="`${id}-map-untyped-lines`">{{i18n.t("custom-import-dialog.untyped-lines", { count: untypedLines.length })}}</label></td>
<td>
<select :id="`${id}-map-untyped-lines`" v-model="untypedLineMapping">
<option v-for="option in untypedLineMappingOptions" :key="option.key" :value="option.value">{{option.text}}</option>

Wyświetl plik

@ -14,12 +14,14 @@
import { useCarousel } from "../../utils/carousel";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
import { normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { formatTypeName, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const searchBoxContext = toRef(() => context.components.searchBox);
const i18n = useI18n();
const props = withDefaults(defineProps<{
searchResults?: Array<SearchResult | FileResult>;
@ -168,7 +170,7 @@
v-if="(!searchResults || searchResults.length == 0) && (!mapResults || mapResults.length == 0)"
class="alert alert-danger"
>
No results have been found.
{{i18n.t("search-results.no-results")}}
</div>
<ul v-if="mapResults && mapResults.length > 0" class="list-group">
@ -182,10 +184,10 @@
<span class="text-break">
<a href="javascript:" @click="handleClick(result, $event)">{{isMarkerResult(result) ? normalizeMarkerName(result.name) : normalizeLineName(result.name)}}</a>
{{" "}}
<span class="result-type">({{client.types[result.typeId].name}})</span>
<span class="result-type">({{formatTypeName(client.types[result.typeId].name)}})</span>
</span>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="i18n.t('search-results.zoom-to-result-tooltip')"><Icon icon="zoom-in" :alt="i18n.t('search-results.zoom-to-result-alt')"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="i18n.t('search-results.show-details-tooltip')"><Icon icon="arrow-right" :alt="i18n.t('search-results.show-details-alt')"></Icon></a>
</li>
</ul>
@ -204,8 +206,8 @@
{{" "}}
<span class="result-type" v-if="result.type">({{result.type}})</span>
</span>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="i18n.t('search-results.zoom-to-result-tooltip')"><Icon icon="zoom-in" :alt="i18n.t('search-results.zoom-to-result-alt')"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="i18n.t('search-results.show-details-tooltip')"><Icon icon="arrow-right" :alt="i18n.t('search-results.show-details-alt')"></Icon></a>
</li>
</ul>
@ -218,10 +220,10 @@
class="btn btn-secondary btn-sm"
:class="{ active: isAllSelected }"
@click="toggleSelectAll"
>Select all</button>
>{{i18n.t("search-results.select-all")}}</button>
<AddToMapDropdown
:label="`Add selected item${activeSearchResults.length == 1 ? '' : 's'} to map`"
:label="i18n.t('search-results.add-to-map-label', { count: activeSearchResults.length })"
:markers="activeMarkersWithTags"
:lines="activeLinesWithTags"
size="sm"
@ -233,7 +235,7 @@
href="javascript:"
class="dropdown-item"
@click="customImport = true"
>Custom type mapping</a>
>{{i18n.t("search-results.custom-type-mapping")}}</a>
</li>
</template>
</AddToMapDropdown>

Wyświetl plik

@ -164,8 +164,8 @@
class="mt-2"
:modelValue="url"
readonly
shortDescription="Map link"
longDescription="The map link"
successTitle="Map link copied"
successMessage="The map link was copied to the clipboard."
></CopyToClipboardInput>
</template>
@ -174,8 +174,8 @@
class="mt-2"
:modelValue="embedCode"
readonly
shortDescription="Embed code"
:longDescription="`The code to embed ${context.appName}`"
successTitle="Embed code copied"
:successMessage="`The code to embed ${context.appName} was copied to the clipboard.`"
:rows="2"
noQr
></CopyToClipboardInput>

Wyświetl plik

@ -7,7 +7,8 @@
import { useToasts } from "../ui/toasts/toasts.vue";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { getOrderedTypes, sleep } from "facilmap-utils";
import { formatTypeName, getOrderedTypes, sleep } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const emit = defineEmits<{
"hide-sidebar": [];
@ -17,6 +18,7 @@
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const dialog = ref<
| "manage-types"
@ -42,7 +44,7 @@
:isDisabled="mapContext.interaction"
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Add"
:label="i18n.t('toolbox-add-dropdown.label')"
>
<li v-for="type in orderedTypes" :key="type.id">
<a
@ -51,7 +53,7 @@
href="javascript:"
@click="addObject(type); emit('hide-sidebar')"
draggable="false"
>{{type.name}}</a>
>{{formatTypeName(type.name)}}</a>
</li>
<li v-if="client.writable == 2">
@ -65,7 +67,7 @@
href="javascript:"
@click="dialog = 'manage-types'; emit('hide-sidebar')"
draggable="false"
>Manage types</a>
>{{i18n.t("toolbox-add-dropdown.manage-types")}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -7,10 +7,12 @@
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { normalizePadName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const i18n = useI18n();
const emit = defineEmits<{
"hide-sidebar": [];
@ -44,7 +46,7 @@
:isDisabled="mapContext.interaction"
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Collaborative maps"
:label="i18n.t('toolbox-collab-maps-dropdown.label')"
>
<li v-for="bookmark in storage.bookmarks" :key="bookmark.id">
<a
@ -66,7 +68,7 @@
href="javascript:"
@click="addBookmark()"
draggable="false"
>Bookmark {{normalizePadName(client.padData.name)}}</a>
>{{i18n.t('toolbox-collab-maps-dropdown.bookmark', { padName: normalizePadName(client.padData.name) })}}</a>
</li>
<li v-if="storage.bookmarks.length > 0">
@ -75,7 +77,7 @@
href="javascript:"
@click="dialog = 'manage-bookmarks'; emit('hide-sidebar')"
draggable="false"
>Manage bookmarks</a>
>{{i18n.t('toolbox-collab-maps-dropdown.manage-bookmarks')}}</a>
</li>
<li v-if="(client.padData && !isBookmarked) || storage.bookmarks.length > 0">
@ -88,7 +90,7 @@
href="javascript:"
@click="dialog = 'create-pad'; emit('hide-sidebar')"
draggable="false"
>Create a new map</a>
>{{i18n.t('toolbox-collab-maps-dropdown.create-map')}}</a>
</li>
<li>
@ -97,7 +99,7 @@
href="javascript:"
@click="dialog = 'open-map'; emit('hide-sidebar')"
draggable="false"
>Open {{client.padId ? "another" : "an existing"}} map</a>
>{{client.padId ? i18n.t("toolbox-collab-maps-dropdown.open-other-map") : i18n.t("toolbox-collab-maps-dropdown.open-map")}}</a>
</li>
<li v-if="client.padData">
@ -106,7 +108,7 @@
:href="`${context.baseUrl}#${hash}`"
@click.exact.prevent="client.openPad(undefined)"
draggable="false"
>Close {{client.padData.name}}</a>
>{{i18n.t("toolbox-collab-maps-dropdown.close-map", { padName: client.padData.name })}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -4,8 +4,10 @@
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import Icon from "../ui/icon.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const i18n = useI18n();
const emit = defineEmits<{
"hide-sidebar": [];
@ -23,7 +25,7 @@
isLink
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Help"
:label="i18n.t('toolbox-help-dropdown.label')"
>
<li>
<a
@ -32,7 +34,7 @@
target="_blank"
draggable="false"
>
<span>Documentation</span>
<span>{{i18n.t("toolbox-help-dropdown.documentation")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -44,7 +46,7 @@
target="_blank"
draggable="false"
>
<span>Matrix chat room</span>
<span>{{i18n.t("toolbox-help-dropdown.matrix-chat")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -56,7 +58,7 @@
target="_blank"
draggable="false"
>
<span>Report a problem</span>
<span>{{i18n.t("toolbox-help-dropdown.bugtracker")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -68,7 +70,7 @@
target="_blank"
draggable="false"
>
<span>Ask a question</span>
<span>{{i18n.t("toolbox-help-dropdown.forum")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -79,7 +81,7 @@
@click="dialog = 'about'; emit('hide-sidebar')"
href="javascript:"
draggable="false"
>About {{context.appName}}</a>
>{{i18n.t("toolbox-help-dropdown.about", { appName: context.appName })}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -4,9 +4,11 @@
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import Icon from "../ui/icon.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const links = computed(() => {
const v = mapContext.value;
@ -52,7 +54,7 @@
isLink
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Map style"
:label="i18n.t('toolbox-map-style-dropdown.label')"
>
<li v-for="layerInfo in baseLayers" :key="layerInfo.key">
<a
@ -89,7 +91,7 @@
target="_blank"
draggable="false"
>
<span>OpenStreetMap</span>
<span>{{i18n.t("toolbox-map-style-dropdown.openstreetmap")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -101,7 +103,7 @@
target="_blank"
draggable="false"
>
<span>Google Maps</span>
<span>{{i18n.t("toolbox-map-style-dropdown.google-maps")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -113,7 +115,7 @@
target="_blank"
draggable="false"
>
<span>Google Maps (Satellite)</span>
<span>{{i18n.t("toolbox-map-style-dropdown.google-maps-satellite")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>
@ -125,7 +127,7 @@
target="_blank"
draggable="false"
>
<span>Bing Maps</span>
<span>{{i18n.t("toolbox-map-style-dropdown.bing-maps")}}</span>
<Icon icon="new-window"></Icon>
</a>
</li>

Wyświetl plik

@ -7,9 +7,12 @@
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import ExportDialog from "../export-dialog.vue";
import UserPreferencesDialog from "../user-preferences-dialog.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const i18n = useI18n();
const importTabContext = toRef(() => context.components.importTab);
const props = defineProps<{
@ -26,6 +29,7 @@
| "export"
| "edit-filter"
| "history"
| "user-preferences"
>();
</script>
@ -36,7 +40,7 @@
isLink
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Tools"
:label="i18n.t('toolbox-tools-dropdown.label')"
>
<li v-if="props.interactive">
<a
@ -44,7 +48,7 @@
href="javascript:"
@click="dialog = 'share'; emit('hide-sidebar')"
draggable="false"
>Share</a>
>{{i18n.t("toolbox-tools-dropdown.share")}}</a>
</li>
<li v-if="props.interactive && importTabContext">
@ -53,7 +57,7 @@
href="javascript:"
@click="importTabContext.openFilePicker(); emit('hide-sidebar')"
draggable="false"
>Open file</a>
>{{i18n.t("toolbox-tools-dropdown.open-file")}}</a>
</li>
<li v-if="client.padData">
@ -62,7 +66,7 @@
href="javascript:"
@click="dialog = 'export'; emit('hide-sidebar')"
draggable="false"
>Export</a>
>{{i18n.t("toolbox-tools-dropdown.export")}}</a>
</li>
<li v-if="client.padData">
@ -75,7 +79,7 @@
href="javascript:"
@click="dialog = 'edit-filter'; emit('hide-sidebar')"
draggable="false"
>Filter</a>
>{{i18n.t("toolbox-tools-dropdown.filter")}}</a>
</li>
<li v-if="client.writable == 2 && client.padData">
@ -84,7 +88,7 @@
href="javascript:"
@click="dialog = 'edit-pad'; emit('hide-sidebar')"
draggable="false"
>Settings</a>
>{{i18n.t("toolbox-tools-dropdown.settings")}}</a>
</li>
<li v-if="!client.readonly && client.padData">
@ -93,7 +97,20 @@
href="javascript:"
@click="dialog = 'history'; emit('hide-sidebar')"
draggable="false"
>History</a>
>{{i18n.t("toolbox-tools-dropdown.history")}}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a
class="dropdown-item"
href="javascript:"
@click="dialog = 'user-preferences'; emit('hide-sidebar')"
draggable="false"
>{{i18n.t("toolbox-tools-dropdown.user-preferences")}}</a>
</li>
</DropdownMenu>
@ -123,4 +140,9 @@
v-if="dialog === 'history' && client.padData"
@hidden="dialog = undefined"
></HistoryDialog>
<UserPreferencesDialog
v-if="dialog === 'user-preferences'"
@hidden="dialog = undefined"
></UserPreferencesDialog>
</template>

Wyświetl plik

@ -7,10 +7,12 @@
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { getOrderedViews } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const i18n = useI18n();
const emit = defineEmits<{
"hide-sidebar": [];
@ -35,7 +37,7 @@
isLink
buttonClass="nav-link"
menuClass="dropdown-menu-end"
label="Views"
:label="i18n.t('toolbox-views-dropdown.label')"
>
<li v-for="view in orderedViews" :key="view.id">
<a
@ -56,7 +58,7 @@
href="javascript:"
@click="dialog = 'save-view'; emit('hide-sidebar')"
draggable="false"
>Save current view</a>
>{{i18n.t("toolbox-views-dropdown.save-current-view")}}</a>
</li>
<li v-if="client.writable == 2 && orderedViews.length > 0">
@ -65,7 +67,7 @@
href="javascript:"
@click="dialog = 'manage-views'; emit('hide-sidebar')"
draggable="false"
>Manage views</a>
>{{i18n.t("toolbox-views-dropdown.manage-views")}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -7,23 +7,23 @@
import { type LineWithTags, type MarkerWithTags, addToMap } from '../../utils/add';
import type { ButtonSize } from '../../utils/bootstrap';
import DropdownMenu from "./dropdown-menu.vue";
import { getOrderedTypes } from 'facilmap-utils';
import { formatTypeName, getOrderedTypes } from 'facilmap-utils';
import { useI18n } from '../../utils/i18n';
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = toRef(() => context.components.map);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
const props = defineProps<{
markers?: MarkerWithTags[];
lines?: LineWithTags[];
label?: string;
size?: ButtonSize;
/** If true, the markers/lines entries are assumed to refer to a single object, omitting the prefix "Marker/line/polgon items as" */
isSingle?: boolean;
}>(), {
label: "Add to map"
})
}>();
const emit = defineEmits<{
"update:isAdding": [isAdding: boolean];
@ -52,7 +52,7 @@
mapContext.value?.components.selectionHandler.setSelectedItems(selection, true);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-add-to-map-error`, "Error adding to map", err);
toasts.showErrorToast(`fm${context.id}-add-to-map-error`, i18n.t("add-to-map-dropdown.add-error"), err);
} finally {
isAdding.value = false;
}
@ -74,7 +74,7 @@
<template>
<DropdownMenu
v-if="client.padData && !client.readonly && ((props.markers && markerTypes.length > 0) || (props.lines && lineTypes.length > 0))"
:label="props.label"
:label="props.label ?? i18n.t('add-to-map-dropdown.fallback-label')"
:isDisabled="(props.markers ?? []).length === 0 && (props.lines ?? []).length === 0"
:isBusy="isAdding"
:size="props.size"
@ -86,7 +86,7 @@
href="javascript:"
class="dropdown-item"
@click="addMarkers(type)"
>{{!props.isSingle && props.lines ? 'Marker items as ' : ''}}{{type.name}}</a>
>{{!props.isSingle && props.lines ? i18n.t("add-to-map-dropdown.add-marker-items", { typeName: formatTypeName(type.name) }) : formatTypeName(type.name)}}</a>
</li>
</template>
</template>
@ -97,7 +97,7 @@
href="javascript:"
class="dropdown-item"
@click="addLines(type)"
>{{!props.isSingle && props.markers ? 'Line/polygon items as ' : ''}}{{type.name}}</a>
>{{!props.isSingle && props.markers ? i18n.t("add-to-map-dropdown.add-line-items", { typeName: formatTypeName(type.name) }) : formatTypeName(type.name)}}</a>
</li>
</template>
</template>

Wyświetl plik

@ -4,6 +4,7 @@
import type { ThemeColour } from "../../utils/bootstrap";
import ModalDialog from "./modal-dialog.vue";
import ValidatedField from "./validated-form/validated-field.vue";
import { useI18n } from "../../utils/i18n";
export type AlertProps = {
title: string;
@ -108,9 +109,10 @@
</script>
<script setup lang="ts">
const i18n = useI18n();
const props = withDefaults(defineProps<AlertProps>(), {
type: "alert",
okLabel: "OK"
type: "alert"
});
const emit = defineEmits<{
@ -135,7 +137,7 @@
<ModalDialog
:title="props.title"
:isCreate="props.type === 'confirm'"
:okLabel="props.okLabel"
:okLabel="props.okLabel ?? i18n.t('alert.fallback-ok-label')"
:okVariant="props.variant"
@shown="emit('shown')"
@hide="emit('hide', result)"

Wyświetl plik

@ -6,6 +6,7 @@
import { arrowNavigation } from "../../utils/ui";
import { type StyleValue, computed, nextTick, ref } from "vue";
import type { Validator } from "./validated-form/validated-field.vue";
import { getI18n } from "../../utils/i18n";
function normalizeData(value: string) {
return ColorMixin.data.apply({ modelValue: value }).val;
@ -17,7 +18,7 @@
function validateColour(colour: string): string | undefined {
if (!isValidColour(colour)) {
return "Needs to be in 6-digit hex format, for example 0000ff.";
return getI18n().t("colour-picker.format-error");
}
}

Wyświetl plik

@ -1,13 +1,15 @@
<script setup lang="ts">
import type { Point } from "facilmap-types";
import copyToClipboard from "copy-to-clipboard";
import { formatCoordinates } from "facilmap-utils";
import { formatCoordinates, formatElevation } from "facilmap-utils";
import Icon from "./icon.vue";
import { computed } from "vue";
import { useToasts } from "./toasts/toasts.vue";
import vTooltip from "../../utils/tooltip";
import { useI18n } from "../../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
point: Point;
@ -18,7 +20,7 @@
function copy(): void {
copyToClipboard(formattedCoordinates.value);
toasts.showToast(undefined, "Coordinates copied", "The coordinates were copied to the clipboard.", { variant: "success", autoHide: true });
toasts.showToast(undefined, i18n.t("coordinates.copied-title"), i18n.t("coordinates.copied-message"), { variant: "success", autoHide: true });
}
</script>
@ -29,12 +31,12 @@
type="button"
class="btn btn-secondary"
@click="copy()"
v-tooltip="'Copy to clipboard'"
v-tooltip="i18n.t('coordinates.copy-to-clipboard')"
>
<Icon icon="copy" alt="Copy to clipboard"></Icon>
<Icon icon="copy" :alt="i18n.t('coordinates.copy-to-clipboard')"></Icon>
</button>
<span v-if="props.ele != null" v-tooltip="'Elevation'">
({{props.ele}}&#x202F;m)
<span v-if="props.ele != null" v-tooltip="i18n.t('coordinates.elevation')">
({{formatElevation(props.ele)}})
</span>
</span>
</template>

Wyświetl plik

@ -9,24 +9,23 @@
import type { Validator } from "./validated-form/validated-field.vue";
import ValidatedField from "./validated-form/validated-field.vue";
import type { ThemeColour } from "../../utils/bootstrap";
import { useI18n } from "../../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
const props = defineProps<{
prefix?: string;
noQr?: boolean;
shortDescription?: string;
longDescription?: string;
successTitle?: string;
successMessage?: string;
readonly?: boolean;
rows?: number;
/** If specified, will be used for the clipboard/QR code instead of `${prefix}${modelValue}` */
fullUrl?: string;
validators?: Array<Validator<string>>;
variant?: ThemeColour;
}>(), {
shortDescription: "Link",
longDescription: "The link"
});
}>();
const modelValue = defineModel<string>({ required: true });
@ -37,7 +36,7 @@
function copy(): void {
copyToClipboard(fullUrl.value);
toasts.showToast(undefined, `${props.shortDescription} copied`, `${props.longDescription} was copied to the clipboard.`, { variant: "success", autoHide: true });
toasts.showToast(undefined, props.successTitle ?? i18n.t("copy-to-clipboard-input.copied-fallback-title"), props.successMessage ?? i18n.t("copy-to-clipboard-input.copied-fallback-message"), { variant: "success", autoHide: true });
}
</script>
@ -68,7 +67,7 @@
:ref="slotProps.inputRef"
/>
</template>
<button type="button" :class="`btn btn-${props.variant ?? 'secondary'}`" @click="copy()">Copy</button>
<button type="button" :class="`btn btn-${props.variant ?? 'secondary'}`" @click="copy()">{{i18n.t("copy-to-clipboard-input.copy")}}</button>
<button
v-if="!props.noQr"
type="button"
@ -77,8 +76,8 @@
ref="qrButtonRef"
@click="showQr = !showQr"
>
<Icon icon="qrcode" alt="QR code"></Icon>
<span v-if="!showQr" class="fm-copy-to-clipboard-input-qr-tooltip" v-tooltip="'Show QR code'"></span>
<Icon icon="qrcode" :alt="i18n.t('copy-to-clipboard-input.qr-code-alt')"></Icon>
<span v-if="!showQr" class="fm-copy-to-clipboard-input-qr-tooltip" v-tooltip="i18n.t('copy-to-clipboard-input.qr-code-tooltip')"></span>
</button>
<div class="invalid-tooltip">
{{slotProps.validationError}}

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { type SlotsType, computed, defineComponent, h, ref, shallowRef, useSlots, watch, watchEffect } from "vue";
import { maxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
import { getMaxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
import Dropdown from "bootstrap/js/dist/dropdown";
import vLinkDisabled from "../../utils/link-disabled";
import type { TooltipPlacement } from "../../utils/tooltip";
@ -26,6 +26,7 @@
tabindex?: number;
tooltip?: string; // TODO
tooltipPlacement?: TooltipPlacement;
maxWidth?: string;
}>(), {
isOpen: undefined,
noWrapper: false,
@ -67,7 +68,7 @@
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
...getMaxSizeModifiers({ maxWidth: props.maxWidth })
],
strategy: "fixed"
})

Wyświetl plik

@ -3,7 +3,7 @@
import type { LineWithTrackPoints, RouteWithTrackPoints } from "facilmap-client";
import { createElevationStats } from "../../utils/heightgraph";
import Icon from "./icon.vue";
import { numberKeys, round } from "facilmap-utils";
import { formatAscentDescent, formatDistance, numberKeys } from "facilmap-utils";
import { computed, ref } from "vue";
import Popover from "./popover.vue";
import vTooltip from "../../utils/tooltip";
@ -22,9 +22,9 @@
</script>
<template>
<span class="fm-elevation-stats">
<span class="fm-elevation-stats" v-if="route.ascent != null && route.descent != null">
<span>
<Icon icon="triangle-top" alt="Ascent"></Icon> {{route.ascent}}&#x202F;m / <Icon icon="triangle-bottom" alt="Descent"></Icon> {{route.descent}}&#x202F;m
<Icon icon="triangle-top" alt="Ascent"></Icon> {{formatAscentDescent(route.ascent)}} / <Icon icon="triangle-bottom" alt="Descent"></Icon> {{formatAscentDescent(route.descent)}}
</span>
<span ref="statsButtonContainerRef">
@ -46,14 +46,14 @@
>
<dl class="row">
<dt class="col-6">Total ascent</dt>
<dd class="col-6">{{route.ascent}}&#x202F;m</dd>
<dd class="col-6">{{formatAscentDescent(route.ascent)}}</dd>
<dt class="col-6">Total descent</dt>
<dd class="col-6">{{route.descent}}&#x202F;m</dd>
<dd class="col-6">{{formatAscentDescent(route.descent)}}</dd>
<template v-for="stat in statsArr" :key="stat.i">
<dt class="col-6">{{stat.i == 0 ? '0%' : stat.i < 0 ? "≤ "+stat.i+"%" : "≥ "+stat.i+"%"}}</dt>
<dd class="col-6">{{round(stat.distance, 2)}}&#x202F;km</dd>
<dd class="col-6">{{formatDistance(stat.distance)}}</dd>
</template>
</dl>
</Popover>

Wyświetl plik

@ -4,7 +4,7 @@
import Tooltip from "bootstrap/js/dist/tooltip";
import { useResizeObserver } from "../../utils/vue";
import { useDomEventListener } from "../../utils/utils";
import { maxSizeModifiers } from "../../utils/bootstrap";
import { getMaxSizeModifiers } from "../../utils/bootstrap";
/**
* Like Bootstrap Popover, but uses an existing popover element rather than creating a new one. This way, the popover
@ -70,7 +70,7 @@
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
...getMaxSizeModifiers()
],
strategy: "fixed"
})

Wyświetl plik

@ -1,4 +1,4 @@
<script lang="ts">
<script setup lang="ts">
import type { RouteMode as RouteModeType } from "facilmap-types";
import { type DecodedRouteMode, decodeRouteMode, encodeRouteMode } from "facilmap-utils";
import Icon from "./icon.vue";
@ -6,11 +6,29 @@
import { getUniqueId } from "../../utils/utils";
import vTooltip, { type TooltipPlacement } from "../../utils/tooltip";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { useI18n } from "../../utils/i18n";
type Mode = Exclude<DecodedRouteMode['mode'], 'track'>;
type Type = DecodedRouteMode['type'];
const constants: {
const i18n = useI18n();
const props = withDefaults(defineProps<{
modelValue: RouteModeType;
tabindex?: number;
disabled?: boolean;
tooltipPlacement?: TooltipPlacement;
}>(), {
tooltipPlacement: "top"
});
const emit = defineEmits<{
"update:modelValue": [value: RouteModeType];
}>();
const id = getUniqueId("fm-route-mode");
const constants = computed((): {
modes: Array<Mode>;
modeIcon: Record<Mode, string>;
modeAlt: Record<Mode, string>;
@ -22,7 +40,7 @@
avoid: DecodedRouteMode['avoid'];
avoidAllowed: Record<DecodedRouteMode['avoid'][0], (mode: DecodedRouteMode['mode'], type: Type) => boolean>;
avoidText: Record<DecodedRouteMode['avoid'][0], string>;
} = {
} => ({
modes: ["car", "bicycle", "pedestrian", ""],
modeIcon: {
@ -33,17 +51,17 @@
},
modeAlt: {
car: "Car",
bicycle: "Bicycle",
pedestrian: "Foot",
"": "Straight"
car: i18n.t("route-mode.car-alt"),
bicycle: i18n.t("route-mode.bicycle-alt"),
pedestrian: i18n.t("route-mode.pedestrian-alt"),
"": i18n.t("route-mode.straight-alt")
},
modeTitle: {
car: "by car",
bicycle: "by bicycle",
pedestrian: "on foot",
"": "in a straight line"
car: i18n.t("route-mode.car-title"),
bicycle: i18n.t("route-mode.bicycle-title"),
pedestrian: i18n.t("route-mode.pedestrian-title"),
"": i18n.t("route-mode.straight-title")
},
types: {
@ -55,30 +73,30 @@
typeText: {
car: {
"": "Car",
"hgv": "HGV"
"": i18n.t("route-mode.car"),
"hgv": i18n.t("route-mode.hgv")
},
bicycle: {
"": "Bicycle",
road: "Road bike",
mountain: "Mountain bike",
electric: "Electric bike"
"": i18n.t("route-mode.bicycle"),
road: i18n.t("route-mode.road-bike"),
mountain: i18n.t("route-mode.mountain-bike"),
electric: i18n.t("route-mode.electric-bike")
},
pedestrian: {
"": "Walking",
hiking: "Hiking",
wheelchair: "Wheelchair"
"": i18n.t("route-mode.walking"),
hiking: i18n.t("route-mode.hiking"),
wheelchair: i18n.t("route-mode.wheelchair")
},
"": {
"": "Straight line"
"": i18n.t("route-mode.straight")
}
},
preferences: ["fastest", "shortest"],
preferenceText: {
fastest: "Fastest",
shortest: "Shortest"
fastest: i18n.t("route-mode.fastest"),
shortest: i18n.t("route-mode.shortest")
},
avoid: ["highways", "tollways", "ferries", "fords", "steps"],
@ -96,30 +114,13 @@
},
avoidText: {
highways: "highways",
tollways: "toll roads",
ferries: "ferries",
fords: "fords",
steps: "steps"
highways: i18n.t("route-mode.avoid-highways"),
tollways: i18n.t("route-mode.avoid-toll-roads"),
ferries: i18n.t("route-mode.avoid-ferries"),
fords: i18n.t("route-mode.avoid-fords"),
steps: i18n.t("route-mode.avoid-steps")
}
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: RouteModeType;
tabindex?: number;
disabled?: boolean;
tooltipPlacement?: TooltipPlacement;
}>(), {
tooltipPlacement: "top"
});
const emit = defineEmits<{
"update:modelValue": [value: RouteModeType];
}>();
const id = getUniqueId("fm-route-mode");
}));
const decodedMode = ref(decodeRouteMode(props.modelValue));
@ -134,7 +135,7 @@
}
}, { deep: true });
const types = computed(() => (Object.keys(constants.types) as Mode[]).map((mode) => constants.types[mode].map((type) => ([mode, type] as [Mode, Type]))).flat());
const types = computed(() => (Object.keys(constants.value.types) as Mode[]).map((mode) => constants.value.types[mode].map((type) => ([mode, type] as [Mode, Type]))).flat());
function isTypeActive(mode: DecodedRouteMode['mode'], type: DecodedRouteMode['type']): boolean {
return (!mode && !decodedMode.value.mode || mode == decodedMode.value.mode) && (!type && !decodedMode.value.type || type == decodedMode.value.type);
@ -146,7 +147,7 @@
if(decodedMode.value.avoid) {
for(let i=0; i < decodedMode.value.avoid.length; i++) {
if(!constants.avoidAllowed[decodedMode.value.avoid[i]](decodedMode.value.mode, decodedMode.value.type))
if(!constants.value.avoidAllowed[decodedMode.value.avoid[i]](decodedMode.value.mode, decodedMode.value.type))
decodedMode.value.avoid.splice(i--, 1);
}
}
@ -176,10 +177,9 @@
v-model="decodedMode.mode"
:value="mode"
:tabindex="tabindex != null ? tabindex + idx : undefined"
v-tooltip:[tooltipPlacement]="`Go ${constants.modeTitle[mode]}`"
:disabled="disabled"
/>
<label class="btn btn-secondary" :for="`${id}-mode-${mode}`">
<label class="btn btn-secondary" :for="`${id}-mode-${mode}`" v-tooltip:[tooltipPlacement]="constants.modeTitle[mode]">
<Icon :icon="constants.modeIcon[mode]" :alt="constants.modeAlt[mode]"></Icon>
</label>
</template>
@ -192,9 +192,10 @@
:isDisabled="disabled"
noWrapper
menuClass="fm-route-mode-customize"
maxWidth="32rem"
>
<template #label>
<Icon icon="cog" alt="Custom"/>
<Icon icon="cog" :alt="i18n.t('route-mode.custom-alt')"/>
</template>
<template #default>
@ -204,7 +205,7 @@
href="javascript:"
@click.capture.stop.prevent="decodedMode.details = !decodedMode.details"
>
<Icon :icon="decodedMode.details ? 'check' : 'unchecked'"></Icon> Load route details (elevation, road types, )
<Icon :icon="decodedMode.details ? 'check' : 'unchecked'"></Icon> {{i18n.t("route-mode.load-details")}}
</a>
</li>
@ -248,7 +249,7 @@
href="javascript:"
@click.capture.stop.prevent="toggleAvoid(avoid)"
>
<Icon :icon="decodedMode.avoid.includes(avoid) ? 'check' : 'unchecked'"></Icon> Avoid {{constants.avoidText[avoid]}}
<Icon :icon="decodedMode.avoid.includes(avoid) ? 'check' : 'unchecked'"></Icon> {{constants.avoidText[avoid]}}
</a>
</li>
</template>
@ -269,7 +270,6 @@
}
.fm-route-mode-customize {
width: 380px;
font-size: 0; /* https://stackoverflow.com/a/5647640/242365 */
li {

Wyświetl plik

@ -3,12 +3,14 @@
import { useToasts } from "./toasts.vue";
import type { ToastOptions } from "./toasts.vue";
/* eslint-disable vue/valid-template-root */
const toasts = useToasts();
const props = defineProps<Omit<ToastOptions, "onHidden"> & {
id: string;
title: string;
message: string;
message: string | Error;
}>();
const emit = defineEmits<{
@ -17,15 +19,23 @@
onMounted(() => {
const { id, title, message, ...options } = props;
toasts.showToast(id, title, message, {
const resolvedOptions: ToastOptions = {
...options,
onHidden: () => {
emit("hidden");
}
});
};
if (message instanceof Error) {
toasts.showErrorToast(id, title, message, resolvedOptions);
} else {
toasts.showToast(id, title, message, resolvedOptions);
}
});
onBeforeUnmount(() => {
toasts.hideToast(props.id);
});
</script>
</script>
<template>
</template>

Wyświetl plik

@ -7,6 +7,7 @@
import { mapRef } from "../../../utils/vue";
import { getUniqueId } from "../../../utils/utils";
import type { ThemeColour } from "../../../utils/bootstrap";
import { getI18n, useI18n } from "../../../utils/i18n";
export interface ToastContext {
showErrorToast(id: string | undefined, title: string, err: any, options?: ToastOptions): Promise<void>;
@ -79,12 +80,12 @@
try {
const res = callback(...args);
Promise.resolve(res).catch((err) => {
void result.showErrorToast(undefined, 'Unexpected error', err);
void result.showErrorToast(undefined, getI18n().t("toasts.unexpected-error"), err);
throw err;
});
return res;
} catch (err: any) {
void result.showErrorToast(undefined, 'Unexpected error', err);
void result.showErrorToast(undefined, getI18n().t("toasts.unexpected-error"), err);
}
}) as C;
},
@ -157,6 +158,8 @@
<script setup lang="ts">
// This script must not be empty, otherwise Vue assumes this component is using the Options API
const i18n = useI18n();
</script>
<template>
@ -176,7 +179,7 @@
:class="toast.variant && `bg-${toast.variant} bg-opacity-25`"
>
<strong class="me-auto">{{toast.title}}</strong>
<button v-if="!toast.noCloseButton" type="button" class="btn-close" @click="hideToastInstance(toast)" aria-label="Close"></button>
<button v-if="!toast.noCloseButton" type="button" class="btn-close" @click="hideToastInstance(toast)" :aria-label="i18n.t('toasts.close-label')"></button>
</div>
<div
class="toast-body bg-opacity-10 text-break"
@ -184,7 +187,7 @@
>
<div>
<div v-if="toast.spinner" class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden">{{i18n.t("toasts.spinner-label")}}</span>
</div>
{{toast.message}}
</div>
@ -216,9 +219,6 @@
<style lang="scss">
.fm-toast-container {
position: absolute;
}
.fm-toasts {
z-index: 10002;
z-index: 10002; /* Above .fm-leaflet-map-disabled-cover */
}
</style>

Wyświetl plik

@ -5,6 +5,7 @@
import { getValidatedForm } from "./validated-form.vue";
import { useToasts } from "../toasts/toasts.vue";
import pDebounce from "p-debounce";
import { useI18n } from "../../../utils/i18n";
type SyncValidationResult = string | undefined;
type AsyncValidationResult = Promise<SyncValidationResult>;
@ -49,6 +50,7 @@
<script setup lang="ts" generic="T">
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
value: T;
@ -114,8 +116,8 @@
}
}).catch((err) => {
if (validationErrorPromise.value === promise) {
toasts.showErrorToast("fm-validity-error", "Error while validating form field", err);
resolvedValidationError.value = "Error while validating form field";
toasts.showErrorToast("fm-validity-error", i18n.t("validated-field.validation-error"), err);
resolvedValidationError.value = i18n.t("validated-field.validation-error");
isValidating.value = false;
}
});
@ -125,9 +127,9 @@
isValidating.value = false;
}
} catch (err: any) {
toasts.showErrorToast("fm-validity-error", "Error while validating form field", err);
toasts.showErrorToast("fm-validity-error", i18n.t("validated-field.validation-error"), err);
validationErrorPromise.value = undefined;
resolvedValidationError.value = "Error while validating form field";
resolvedValidationError.value = i18n.t("validated-field.validation-error");
isValidating.value = false;
}
});

Wyświetl plik

@ -5,9 +5,11 @@
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import Icon from "./icon.vue";
import vTooltip from "../../utils/tooltip";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const props = withDefaults(defineProps<{
destination: ZoomDestination;
@ -17,7 +19,7 @@
label: "object"
});
const tooltip = computed(() => `Zoom to ${props.label}`);
const tooltip = computed(() => props.label ?? i18n.t("zoom-to-object-button.fallback-label"));
function zoom(): void {
flyTo(mapContext.value.components.map, props.destination);

Wyświetl plik

@ -0,0 +1,92 @@
<script setup lang="ts">
import { getCurrentLanguage, getCurrentUnits, getLocalizedLanguageList } from "facilmap-utils";
import { Units } from "facilmap-types";
import ModalDialog from "./ui/modal-dialog.vue";
import { computed, reactive, ref, toRef } from "vue";
import { T, useI18n } from "../utils/i18n";
import { getUniqueId } from "../utils/utils";
import { setLangCookie, setUnitsCookie } from "../utils/cookies";
import { isEqual } from "lodash-es";
import { injectContextOptional } from "./facil-map-context-provider/facil-map-context-provider.vue";
const i18n = useI18n();
const context = injectContextOptional();
const client = toRef(() => context?.components.client);
const emit = defineEmits<{
hidden: [];
}>();
const modalRef = ref<InstanceType<typeof ModalDialog>>();
const id = getUniqueId("fm-user-preferences-dialog");
const initialValues = {
lang: getCurrentLanguage(),
units: getCurrentUnits()
};
const values = reactive({ ...initialValues });
const isModified = computed(() => {
return !isEqual(values, reactive(initialValues));
});
const languageList = computed(() => getLocalizedLanguageList());
async function save() {
await setLangCookie(values.lang);
await setUnitsCookie(values.units);
if (client.value) {
await client.value.setLanguage({
lang: values.lang,
units: values.units
});
}
await i18n.changeLanguage(values.lang);
modalRef.value?.modal.hide();
}
</script>
<template>
<ModalDialog
:title="i18n.t('user-preferences-dialog.title')"
class="fm-user-preferences"
:isModified="isModified"
@submit="save"
ref="modalRef"
@hidden="emit('hidden')"
>
<p>{{i18n.t("user-preferences-dialog.introduction")}}</p>
<div class="row mb-3">
<label :for="`${id}-language-input`" class="col-sm-3 col-form-label">{{i18n.t("user-preferences-dialog.language")}}</label>
<div class="col-sm-9">
<select :id="`${id}-language-input`" class="form-select" v-model="values.lang">
<option v-for="(label, key) in languageList" :key="key" :value="key">{{label}}</option>
</select>
<div class="form-text">
<T k="user-preferences-dialog.language-description">
<template #weblate>
<a href="https://hosted.weblate.org/projects/facilmap/" target="_blank" rel="noopener">
{{i18n.t("user-preferences-dialog.language-description-interpolation-weblate")}}
</a>
</template>
</T>
</div>
</div>
</div>
<div class="row mb-3">
<label :for="`${id}-units-input`" class="col-sm-3 col-form-label">{{i18n.t("user-preferences-dialog.units")}}</label>
<div class="col-sm-9">
<select :id="`${id}-units-input`" class="form-select" v-model="values.units">
<option :value="Units.METRIC">{{i18n.t("user-preferences-dialog.units-metric")}}</option>
<option :value="Units.US_CUSTOMARY">{{i18n.t("user-preferences-dialog.units-us")}}</option>
</select>
</div>
</div>
</ModalDialog>
</template>

Wyświetl plik

@ -10,6 +10,7 @@ export * from "./utils/draw";
export * from "./utils/files";
export { default as FmHeightgraph } from "./utils/heightgraph";
export * from "./utils/heightgraph";
export * from "./utils/i18n";
export { default as vLinkDisabled } from "./utils/link-disabled";
export * from "./utils/link-disabled";
export * from "./utils/modal";

Wyświetl plik

@ -57,7 +57,7 @@ export type ButtonSize = "lg" | "sm";
* An array of popper modifiers that uses popper-max-size-modifier to shrink the popover to prevent overflow
* rather than move it, as is the default in Bootstrap.
*/
export const maxSizeModifiers: Array<Partial<Modifier<any, any>>> = [
export const getMaxSizeModifiers = ({ maxWidth = "30rem" }: { maxWidth?: string } = {}): Array<Partial<Modifier<any, any>>> => [
{
...maxSize,
options: {
@ -75,7 +75,7 @@ export const maxSizeModifiers: Array<Partial<Modifier<any, any>>> = [
state.styles.popper = {
...state.styles.popper,
maxWidth: `min(30rem, ${width}px)`,
maxWidth: `min(${maxWidth}, ${width}px)`,
maxHeight: `${height}px`
}
}

Wyświetl plik

@ -0,0 +1,67 @@
import { Units, unitsValidator } from "facilmap-types";
import Cookies from "js-cookie";
import { computed, reactive, readonly, ref } from "vue";
import * as z from "zod";
const cookieValidators = {
lang: z.string().optional(),
units: unitsValidator.optional()
};
export type Cookies = {
[Name in keyof typeof cookieValidators]: z.infer<typeof cookieValidators[Name]>;
}
const cookieCounter = ref(0);
function cookie<Name extends keyof Cookies>(name: Name) {
return computed((): Cookies[Name] | undefined => {
cookieCounter.value;
const value = Cookies.get(name);
if (value == null) {
return undefined;
}
const result = cookieValidators[name].safeParse(value);
return result.success ? result.data as Cookies[Name] : undefined;
});
}
export const cookies = readonly(reactive({
lang: cookie("lang"),
units: cookie("units")
}));
const hasStorageAccessP = (async () => {
if ("hasStorageAccess" in document) {
return await document.hasStorageAccess();
} else {
return true;
}
})();
async function setLongTermCookie(name: keyof Cookies, value: string): Promise<void> {
try {
Cookies.set(name, value, {
expires: 3650,
partitioned: !(await hasStorageAccessP)
});
} finally {
cookieCounter.value++;
}
}
export async function setLangCookie(value: string): Promise<void> {
await setLongTermCookie("lang", value);
}
export async function setUnitsCookie(value: Units): Promise<void> {
await setLongTermCookie("units", value);
}
// Renew long-term cookies (see https://developer.chrome.com/blog/cookie-max-age-expires)
if (cookies.lang) {
void setLangCookie(cookies.lang);
}
if (cookies.units) {
void setUnitsCookie(cookies.units);
}

Wyświetl plik

@ -4,6 +4,7 @@ import type { ToastContext } from "../components/ui/toasts/toasts.vue";
import type { FacilMapContext } from "../components/facil-map-context-provider/facil-map-context";
import { requireClientContext, requireMapContext } from "../components/facil-map-context-provider/facil-map-context-provider.vue";
import { addToMap } from "./add";
import { formatTypeName } from "facilmap-utils";
export function drawMarker(type: Type, context: FacilMapContext, toasts: ToastContext): void {
const mapContext = requireMapContext(context);
@ -23,7 +24,7 @@ export function drawMarker(type: Type, context: FacilMapContext, toasts: ToastCo
}
});
toasts.showToast("fm-draw-add-marker", `Add ${type.name}`, "Please click on the map to add a marker.", {
toasts.showToast("fm-draw-add-marker", `Add ${formatTypeName(type.name)}`, "Please click on the map to add a marker.", {
noCloseButton: true,
actions: [
{
@ -98,7 +99,7 @@ export async function drawLine(type: Type, context: FacilMapContext, toasts: Toa
const lineTemplate = await client.value.getLineTemplate({ typeId: type.id });
toasts.showToast("fm-draw-add-line", `Add ${type.name}`, "Click on the map to draw a line. Click “Finish” to save it.", {
toasts.showToast("fm-draw-add-line", `Add ${formatTypeName(type.name)}`, "Click on the map to draw a line. Click “Finish” to save it.", {
noCloseButton: true,
actions: [
{

Wyświetl plik

@ -1,54 +1,77 @@
/// <reference types="vite/client" />
import { type i18n } from "i18next";
import { defineComponent, ref } from "vue";
import messagesEn from "../../i18n/en";
import messagesDe from "../../i18n/de";
import { getRawI18n, onI18nReady } from "facilmap-utils";
import messagesEn from "../../i18n/en.json";
import messagesDe from "../../i18n/de.json";
import messagesNbNo from "../../i18n/nb-NO.json";
import messagesRu from "../../i18n/ru.json";
import { decodeQueryString, getAcceptHotI18n, getRawI18n, onI18nReady, setCurrentUnitsGetter } from "facilmap-utils";
import { cookies } from "./cookies";
import { unitsValidator } from "facilmap-types";
const namespace = "facilmap-frontend";
onI18nReady((i18n) => {
i18n.addResourceBundle("en", namespace, messagesEn);
i18n.addResourceBundle("de", namespace, messagesDe);
i18n.addResourceBundle("nb-NO", namespace, messagesNbNo);
i18n.addResourceBundle("ru", namespace, messagesRu);
});
if (import.meta.hot) {
import.meta.hot.accept("../../i18n/en", (m) => {
onI18nReady((i18n) => {
i18n.addResourceBundle("en", namespace, m!.default);
});
});
import.meta.hot.accept("../../i18n/de", (m) => {
onI18nReady((i18n) => {
i18n.addResourceBundle("de", namespace, m!.default);
});
});
import.meta.hot!.accept(`../../i18n/en.json`, getAcceptHotI18n("en", namespace));
import.meta.hot!.accept(`../../i18n/de.json`, getAcceptHotI18n("de", namespace));
import.meta.hot!.accept(`../../i18n/nb-NO.json`, getAcceptHotI18n("nb-NO", namespace));
import.meta.hot!.accept(`../../i18n/ru.json`, getAcceptHotI18n("ru", namespace));
}
const rerenderCounter = ref(0);
const rerender = () => {
rerenderCounter.value++;
const i18nResourceChangeCounter = ref(0);
const onI18nResourceChange = () => {
i18nResourceChangeCounter.value++;
};
onI18nReady((i18n) => {
i18n.store.on("added", rerender);
i18n.store.on("removed", rerender);
i18n.on("languageChanged", rerender);
i18n.on("loaded", rerender);
i18n.store.on("added", onI18nResourceChange);
i18n.store.on("removed", onI18nResourceChange);
i18n.on("languageChanged", onI18nResourceChange);
i18n.on("loaded", onI18nResourceChange);
let tBkp = i18n.t;
i18n.t = function(this: any, ...args: any) {
// Consume resource change counter to make calls to t() reactive to i18n resource changes
i18nResourceChangeCounter.value;
return tBkp.apply(this, args);
} as any;
});
export function useI18n(): Pick<i18n, "t"> {
setCurrentUnitsGetter(() => {
const queryParams = decodeQueryString(location.search);
const query = queryParams.format ? unitsValidator.safeParse(queryParams.format) : undefined;
return query?.success ? query.data : cookies.units;
});
export function getI18n(): {
t: i18n["t"];
changeLanguage: (lang: string) => Promise<void>;
} {
return {
t: new Proxy(getRawI18n().getFixedT(null, namespace), {
apply: (target, thisArg, argumentsList) => {
rerenderCounter.value;
return target.apply(thisArg, argumentsList as any);
}
})
t: getRawI18n().getFixedT(null, namespace),
changeLanguage: async (lang) => {
await getRawI18n().changeLanguage(lang);
}
};
}
export function useI18n(): ReturnType<typeof getI18n> {
return getI18n();
}
/**
* Renders a translated message. Each interpolation variable needs to be specified as a slot, making it possible to interpolate
* components and rich text.
*/
export const T = defineComponent({
props: {
k: { type: String, required: true }

Wyświetl plik

@ -5,7 +5,7 @@ import type { SelectedItem } from "./selection";
import type { FindOnMapLine, FindOnMapMarker, FindOnMapResult, Line, Marker, SearchResult } from "facilmap-types";
import type { Geometry } from "geojson";
import { isMapResult } from "./search";
import { decodeLonLatUrl, normalizeLineName, normalizeMarkerName, splitRouteQuery } from "facilmap-utils";
import { decodeLonLatUrl, decodeRouteQuery, encodeRouteQuery, normalizeLineName, normalizeMarkerName, parseRouteQuery } from "facilmap-utils";
import type { ClientContext } from "../components/facil-map-context-provider/client-context";
import type { FacilMapContext } from "../components/facil-map-context-provider/facil-map-context";
import { requireClientContext, requireMapContext } from "../components/facil-map-context-provider/facil-map-context-provider.vue";
@ -141,12 +141,19 @@ export async function openSpecialQuery(query: string, context: FacilMapContext,
const routeFormTabContext = toRef(() => context.components.routeFormTab);
if(searchBoxContext.value && routeFormTabContext.value) {
const split = splitRouteQuery(query);
if (split.queries.length >= 2) {
const split1 = decodeRouteQuery(query); // A route hash query encoded in a predictable format in English by the route form
if (split1.queries.length >= 2) {
routeFormTabContext.value.setQuery(query, zoom, smooth);
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
return true;
}
const split2 = parseRouteQuery(query); // A free-text route query specified by the user in the current language
if (split2.queries.length >= 2) {
routeFormTabContext.value.setQuery(encodeRouteQuery(split2), zoom, smooth);
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
return true;
}
}
const lonlat = decodeLonLatUrl(query);

Wyświetl plik

@ -2,7 +2,7 @@
<html class="fm-facilmap-map">
<head>
<meta charset="utf-8">
<title><%=normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, appName)%></title>
<title><%=normalizePageTitle(padData?.name != null ? normalizePadName(padData.name) : undefined, appName)%></title>
<meta name="description" content="<%=normalizePageDescription(padData?.description)%>" />
<% if(!padData || (isReadOnly && padData.searchEngines)) { -%>
<meta name="robots" content="index,nofollow" />
@ -78,9 +78,9 @@
<% } -%>
</head>
<body>
<noscript><p><strong><%=appName%> requires JavaScript to work.</strong></p></noscript>
<noscript><p><strong><%=i18n.t("frontend.requires-javascript", { appName })%></strong></p></noscript>
<div id="loading">
Loading...
<%=i18n.t("frontend.loading")%>
<div id="spinner"></div>
</div>
@ -93,7 +93,7 @@
var loading = document.getElementById("loading");
if(loading) {
loading.className += " error";
loading.innerHTML = "Could not load <%=appName%>!";
loading.innerHTML = <%-JSON.stringify(i18n.t("frontend.load-error", { appName }))%>;
}
};
})();

Wyświetl plik

@ -1,4 +1,4 @@
import { createApp, defineComponent, h, ref, watch } from "vue";
import { computed, createApp, defineComponent, h, ref, watch } from "vue";
import { FacilMap } from "../lib";
import { decodeQueryString, encodeQueryString, normalizePadName } from "facilmap-utils";
import decodeURIComponent from "decode-uri-component";
@ -57,12 +57,12 @@ const Root = defineComponent({
history.replaceState(null, "", baseUrl + (padId.value ? encodeURIComponent(padId.value) : "") + location.search + location.hash);
});
watch(padName, () => {
const title = padName.value != null ? `${normalizePadName(padName.value)}${config.appName}` : config.appName;
const pageTitle = computed(() => padName.value != null ? `${normalizePadName(padName.value)}${config.appName}` : config.appName);
watch(pageTitle, () => {
// We have to call history.replaceState() in order for the new title to end up in the browser history
window.history && history.replaceState({ }, title);
document.title = title;
window.history && history.replaceState({ }, pageTitle.value);
document.title = pageTitle.value;
});
return () => h(FacilMap, {

Wyświetl plik

@ -47,7 +47,7 @@
-%>
<%-renderSingleTable(type.id, {
before: (
`\t\t\t<h2 role="button" data-bs-toggle="collapse" data-bs-target="#type-${quoteHtml(type.id)}" aria-expanded="true" aria-controls="type-${quoteHtml(type.id)}"><svg class="d-print-none" viewbox="0 0 11 15" height="15"><path d="M10.195 7.5l-7.5 7.5L0 12.305 4.805 7.5 0 2.695 2.695 0z"/></svg> ${quoteHtml(type.name)}</h2>\n` +
`\t\t\t<h2 role="button" data-bs-toggle="collapse" data-bs-target="#type-${quoteHtml(type.id)}" aria-expanded="true" aria-controls="type-${quoteHtml(type.id)}"><svg class="d-print-none" viewbox="0 0 11 15" height="15"><path d="M10.195 7.5l-7.5 7.5L0 12.305 4.805 7.5 0 2.695 2.695 0z"/></svg> ${quoteHtml(formatTypeName(type.name))}</h2>\n` +
`\t\t\t<div id="type-${quoteHtml(type.id)}" class="collapse show">\n`
),
after: (

Wyświetl plik

@ -18,5 +18,5 @@
{ "path": "../utils/tsconfig.json" },
{ "path": "../tsconfig.json" }
],
"include": ["src/**/*"]
"include": ["src/**/*", "src/**/*.json"]
}

Wyświetl plik

@ -0,0 +1,163 @@
{
"overpass-presets": {
"category-amenities": "Infrastruktur",
"atm": "Geldautomat",
"bank": "Bank (Kreditinstitut)",
"bench": "Sitzbank",
"bicycleparking": "Fahrradparkplatz",
"bicyclerental": "Fahrradverleih",
"cinema": "Kino",
"clinic": "Klinik",
"drinkingwater": "Trinkwasser",
"embassy": "Botschaft",
"firestation": "Feuerwehr",
"fuel": "Tankstelle",
"hospital": "Krankenhaus",
"library": "Bibliothek",
"musicschool": "Musikschule",
"parking": "Kfz-Parkplatz",
"pharmacy": "Apotheke",
"police": "Polizei",
"postbox": "Briefkasten",
"postoffice": "Post",
"school": "Schule",
"shower": "Dusche",
"taxi": "Taxi",
"theatre": "Theater",
"toilets": "Toilette",
"university": "Universität",
"worship": "Gotteshaus",
"church": "Kirche",
"mosque": "Moschee",
"buddhist": "Buddhistischer Tempel",
"hindu": "Hinduistischer Tempel",
"synagogue": "Synagoge",
"cemetery": "Friedhof",
"category-tourism": "Tourismus",
"abandoned": "Verlassen",
"artscentre": "Kunstzentrum",
"artwork": "Kunstwerk",
"attraction": "Attraktion",
"casino": "Kasino",
"castle": "Burg",
"gallery": "Gallerie",
"heritage": "Kulturerbe",
"historic": "Historisch",
"touristinformation": "Information",
"monument": "Monument/Gedenkstätte",
"monumentaltree": "Monumentaler Baum",
"museum": "Museum",
"nudism": "FKK",
"picnic": "Picknick",
"statue": "Statue",
"themepark": "Freizeitpark",
"viewpoint": "Aussichtspunkt",
"vineyard": "Weinberg",
"windmill": "Windmühle",
"watermill": "Wassermühle",
"zoo": "Zoo",
"tourism": "Tourism=yes",
"category-hotels": "Hotels",
"alpinehut": "Alpenhütte",
"apartment": "Ferienwohnung",
"campsite": "Campingplatz",
"chalet": "Chalet",
"guesthouse": "Gästehaus",
"hostel": "Hostel",
"hotel": "Hotel",
"motel": "Motel",
"spa": "Wellness",
"sauna": "Sauna",
"category-sports": "Sport",
"americanfootball": "American Football",
"baseball": "Baseball",
"basketball": "Basketball",
"cycling": "Radsport",
"gymnastics": "Gymnastik",
"golfcourse": "Golf",
"hockey": "Hockey",
"horseracing": "Pferderennen",
"icehockey": "Eishockey",
"soccer": "Fußball",
"sportscentre": "Sportzentrum",
"surfing": "Surfen",
"swimming": "Schwimmen",
"tabletennis": "Tischtennis",
"tennis": "Tennis",
"volleyball": "Volleyball",
"category-shops": "Geschäfte",
"beautyshop": "Kosmetiksalon",
"bicycleshop": "Fahrrad",
"bookshop": "Bücher",
"carshop": "Auto",
"chemist": "Drogerie",
"clothesshop": "Kleidung",
"copyshop": "Copyshop",
"cosmeticsshop": "Kosmetik",
"departmentstore": "Warenhaus",
"diyshop": "Baumarkt",
"florist": "Blumen",
"gardencentre": "Gartencenter",
"generalshop": "Allgemein",
"giftshop": "Geschenke",
"hairdresser": "Friseur",
"jewelleryshop": "Juwellier",
"kiosk": "Kiosk",
"leathershop": "Leder",
"marketplace": "Marktplatz",
"musicshop": "Musikinstrumente",
"optician": "Optiker",
"petshop": "Haustiere",
"phoneshop": "Handys",
"photoshop": "Fotozubehör",
"shoeshop": "Schuhe",
"mall": "Einkaufszentrum",
"textileshop": "Textilien",
"toyshop": "Spielzeug",
"category-food-shops": "Lebensmittel",
"alcoholshop": "Alkohol",
"bakery": "Bäckerei",
"beverageshop": "Getränke",
"butcher": "Metzgerei",
"cheeseshop": "Käse",
"confectionery": "Konditorei",
"coffeeshop": "Kaffee",
"dairyshop": "Milchprodukte",
"delishop": "Delikatessen",
"groceryshop": "Lebensmittel",
"organicshop": "Bioladen",
"seafoodshop": "Meeresfrüchte",
"supermarket": "Supermarkt",
"wineshop": "Wein",
"category-restaurants": "Restaurants",
"bar": "Bar",
"bbq": "Barbecue",
"biergarten": "Biergarten",
"cafe": "Café",
"fastfood": "Fast Food",
"foodcourt": "Food-Court",
"icecream": "Eis",
"pub": "Kneipe",
"restaurant": "Restaurant",
"category-various": "Sonstige",
"busstop": "Bushaltestelle",
"bicyclecharging": "E-Bike-Ladepunkt",
"kindergarten": "Kindergarten",
"office": "Büro",
"recycling": "Recycling",
"travelagency": "Reisebüro",
"defibrillator": "Defibrillator",
"fireextinguisher": "Feuerlöscher",
"fixme": "fixme",
"notenode": "Note-Node",
"noteway": "Note-Way",
"construction": "Baustelle",
"image": "Bild",
"camera": "Kamera",
"city": "Großstadt",
"town": "Stadt",
"village": "Dorf",
"hamlet": "Weiler",
"suburb": "Stadtteil"
}
}

Wyświetl plik

@ -0,0 +1,163 @@
{
"overpass-presets": {
"category-amenities": "Amenities",
"atm": "ATM",
"bank": "Bank",
"bench": "Bench",
"bicycleparking": "Bicycle parking",
"bicyclerental": "Bicycle rental",
"cinema": "Cinema",
"clinic": "Clinic",
"drinkingwater": "Drinking water",
"embassy": "Embassy",
"firestation": "Firestation",
"fuel": "Fuel",
"hospital": "Hospital",
"library": "Library",
"musicschool": "Music school",
"parking": "Car parking",
"pharmacy": "Pharmacy",
"police": "Police",
"postbox": "Letter box",
"postoffice": "Post office",
"school": "School/college",
"shower": "Shower",
"taxi": "Taxi",
"theatre": "Theatre",
"toilets": "Toilets",
"university": "University",
"worship": "Place of worship",
"church": "Church",
"mosque": "Mosque",
"buddhist": "Buddhist Temple",
"hindu": "Hindu Temple",
"synagogue": "Synagogue",
"cemetery": "Cemetery",
"category-tourism": "Tourism",
"abandoned": "Abandoned",
"artscentre": "Arts centre",
"artwork": "Artwork",
"attraction": "Attraction",
"casino": "Casino",
"castle": "Castle",
"gallery": "Gallery",
"heritage": "Heritage",
"historic": "Historic",
"touristinformation": "Information",
"monument": "Monument/memorial",
"monumentaltree": "Monumental Tree",
"museum": "Museum",
"nudism": "Nudism",
"picnic": "Picnic",
"statue": "Statue",
"themepark": "Theme park",
"viewpoint": "Viewpoint",
"vineyard": "Vineyard",
"windmill": "Windmill",
"watermill": "Watermill",
"zoo": "Zoo",
"tourism": "Tourism=yes",
"category-hotels": "Hotels",
"alpinehut": "Alpine hut",
"apartment": "Apartment",
"campsite": "Camp site",
"chalet": "Chalet",
"guesthouse": "Guest house",
"hostel": "Hostel",
"hotel": "Hotel",
"motel": "Motel",
"spa": "Spa",
"sauna": "Sauna",
"category-sports": "Sports",
"americanfootball": "American football",
"baseball": "Baseball",
"basketball": "Basketball",
"cycling": "Cycling",
"gymnastics": "Gymnastics",
"golfcourse": "Golf",
"hockey": "Hockey",
"horseracing": "Horse racing",
"icehockey": "Ice hockey",
"soccer": "Soccer",
"sportscentre": "Sports centre",
"surfing": "Surfing",
"swimming": "Swimming",
"tabletennis": "Table tennis",
"tennis": "Tennis",
"volleyball": "Volleyball",
"category-shops": "Shops",
"beautyshop": "Beauty",
"bicycleshop": "Bicycle",
"bookshop": "Books/Stationary",
"carshop": "Car",
"chemist": "Chemist",
"clothesshop": "Clothes",
"copyshop": "Copyshop",
"cosmeticsshop": "Cosmetics",
"departmentstore": "Department store",
"diyshop": "DIY/hardware",
"florist": "Florist",
"gardencentre": "Garden centre",
"generalshop": "General",
"giftshop": "Gift",
"hairdresser": "Hairdresser",
"jewelleryshop": "Jewelry",
"kiosk": "Kiosk",
"leathershop": "Leather",
"marketplace": "Marketplace",
"musicshop": "Musical instrument",
"optician": "Optician",
"petshop": "Pets",
"phoneshop": "Phone",
"photoshop": "Photo",
"shoeshop": "Shoes",
"mall": "Shopping centre",
"textileshop": "Textiles",
"toyshop": "Toys",
"category-food-shops": "Food shops",
"alcoholshop": "Alcohol",
"bakery": "Bakery",
"beverageshop": "Beverages",
"butcher": "Butcher",
"cheeseshop": "Cheese",
"confectionery": "Chocolate/Confectionery",
"coffeeshop": "Coffee",
"dairyshop": "Dairy",
"delishop": "Deli",
"groceryshop": "Grocery",
"organicshop": "Organic",
"seafoodshop": "Seafood",
"supermarket": "Supermarket",
"wineshop": "Wine",
"category-restaurants": "Restaurants",
"bar": "Bar",
"bbq": "BBQ",
"biergarten": "Biergarten",
"cafe": "Cafe",
"fastfood": "Fast food",
"foodcourt": "Food court",
"icecream": "Ice cream",
"pub": "Pub",
"restaurant": "Restaurant",
"category-various": "Various",
"busstop": "Busstop",
"bicyclecharging": "E-bike charging",
"kindergarten": "Kindergarten",
"office": "Office",
"recycling": "Recycling",
"travelagency": "Travel agency",
"defibrillator": "Defibrillator - AED",
"fireextinguisher": "Fire hose/extinguisher",
"fixme": "fixme",
"notenode": "Note-Node",
"noteway": "Note-Way",
"construction": "Construction",
"image": "Image",
"camera": "Public camera",
"city": "City",
"town": "Town",
"village": "Village",
"hamlet": "Hamlet",
"suburb": "Suburb"
}
}

Wyświetl plik

@ -0,0 +1 @@
{}

Wyświetl plik

@ -0,0 +1 @@
{}

Wyświetl plik

@ -1,3 +1,5 @@
import { getI18n } from "../utils/i18n";
export interface OverpassPresetCategory {
label: string;
presets: OverpassPreset[][];
@ -10,246 +12,251 @@ export interface OverpassPreset {
}
// These are mostly copied from OpenPoiMap. See https://github.com/marczoutendijk/openpoimap/blob/master/js_source/opm.js.
export const overpassPresets: OverpassPresetCategory[] = [
{
label: "Amenities",
presets: [
[
{ key: "atm", query: "(node[amenity=atm];way[amenity=atm];node[amenity=bank][atm][atm!=no];way[amenity=bank][atm][atm!=no];rel[amenity=bank][atm][atm!=no];)", label: "ATM" },
{ key: "bank", query: "(node[amenity=bank];way[amenity=bank];rel[amenity=bank];)", label: "Bank" },
{ key: "bench", query: "(node[amenity=bench];node(w);)", label: "Bench" },
{ key: "bicycleparking", query: "(node[amenity=bicycle_parking];way[amenity=bicycle_parking];rel[amenity=bicycle_parking];)", label: "Bicycle parking" },
{ key: "bicyclerental", query: "(node[amenity=bicycle_rental];way[amenity=bicycle_rental];rel[amenity=bicycle_rental];)", label: "Bicycle rental" },
{ key: "cinema", query: "(node[amenity=cinema];way[amenity=cinema];rel[amenity=cinema];)", label: "Cinema" },
{ key: "clinic", query: "(node[amenity=clinic];way[amenity=clinic];rel[amenity=clinic];)", label: "Clinic" },
{ key: "drinkingwater", query: "(node[amenity=drinking_water];way[amenity=drinking_water];rel[amenity=drinking_water];)", label: "Drinking water" },
{ key: "embassy", query: "(node[amenity=embassy];way[amenity=embassy];rel[amenity=embassy];)", label: "Embassy" },
{ key: "firestation", query: "(node[amenity=fire_station];way[amenity=fire_station];rel[amenity=fire_station];)", label: "Firestation" },
{ key: "fuel", query: "(node[amenity=fuel];way[amenity=fuel];rel[amenity=fuel];)", label: "Fuel" },
{ key: "hospital", query: "(node[amenity=hospital];way[amenity=hospital];rel[amenity=hospital];)", label: "Hospital" },
{ key: "library", query: "(node[amenity=library];way[amenity=library];rel[amenity=library];)", label: "Library" },
{ key: "musicschool", query: "(node[amenity=music_school];way[amenity=music_school];rel[amenity=music_school];)", label: "Music school" },
{ key: "parking", query: "(node[amenity=parking];way[amenity=parking];rel[amenity=parking];)", label: "Parking" },
{ key: "pharmacy", query: "(node[amenity=pharmacy];way[amenity=pharmacy];rel[amenity=pharmacy];)", label: "Pharmacy" },
{ key: "police", query: "(node[amenity=police];way[amenity=police];rel[amenity=police];)", label: "Police" },
{ key: "postbox", query: "(node[amenity=post_box];node(w);)", label: "Letter box" },
{ key: "postoffice", query: "(node[amenity=post_office];way[amenity=post_office];rel[amenity=post_office];)", label: "Post office" },
{ key: "school", query: "(node[amenity~'^school$|^college$'];way[amenity~'^school$|^college$'];rel[amenity~'^school$|^college$'];)", label: "School/college" },
{ key: "shower", query: "(nwr[amenity=shower];nwr[shower=yes];)", label: "Shower" },
{ key: "taxi", query: "(node[amenity=taxi];way[amenity=taxi];rel[amenity=taxi];)", label: "Taxi" },
{ key: "theatre", query: "(node[amenity=theatre];way[amenity=theatre];rel[amenity=theatre];)", label: "Theatre" },
{ key: "toilets", query: "(node[amenity=toilets];way[amenity=toilets];rel[amenity=toilets];)", label: "Toilets" },
{ key: "university", query: "(node[amenity=university];way[amenity=university];rel[amenity=university];)", label: "University" }
], [
// Check for various religions. We check on 5 religions AND also on a general place_of_worship but excluding the others.
// zaterdag 9 januari 2016 Included rel for the stand-alone religions
{ key: "worship", query: "(node[amenity=place_of_worship][religion!~'christian|muslim|buddhist|hindu|jewish'];way[amenity=place_of_worship][religion!~'christian|muslim|buddhist|hindu|jewish'];rel[amenity=place_of_worship][religion!~'christian|muslim|buddhist|hindu|jewish'];)", label: "Place of worship" },
{ key: "church", query: "(node[amenity=place_of_worship][religion=christian];way[amenity=place_of_worship][religion=christian];rel[amenity=place_of_worship][religion=christian];)", label: "Church" },
{ key: "mosque", query: "(node[amenity=place_of_worship][religion=muslim];way[amenity=place_of_worship][religion=muslim];rel[amenity=place_of_worship][religion=muslim];)", label: "Mosque" },
{ key: "buddhist", query: "(node[amenity=place_of_worship][religion=buddhist];way[amenity=place_of_worship][religion=buddhist];rel[amenity=place_of_worship][religion=buddhist];)", label: "Buddhist Temple" },
{ key: "hindu", query: "(node[amenity=place_of_worship][religion=hindu];way[amenity=place_of_worship][religion=hindu];rel[amenity=place_of_worship][religion=hindu];)", label: "Hindu Temple" },
{ key: "synagogue", query: "(node[amenity=place_of_worship][religion=jewish];way[amenity=place_of_worship][religion=jewish];rel[amenity=place_of_worship][religion=jewish];)", label: "Synagogue" },
// Check only for cemetery for human beings
{ key: "cemetery", query: "(node[landuse=cemetery][animal!~'.'];way[landuse=cemetery][animal!~'.'];rel[landuse=cemetery][animal!~'.'];)", label: "Cemetery" }
export function getAllOverpassPresets(): OverpassPresetCategory[] {
const i18n = getI18n();
return [
{
label: i18n.t("overpass-presets.category-amenities"),
presets: [
[
{ key: "atm", query: "(nwr[amenity=atm];nwr[amenity=bank][atm][atm!=no];)", label: i18n.t("overpass-presets.atm") },
{ key: "bank", query: "nwr[amenity=bank]", label: i18n.t("overpass-presets.bank") },
{ key: "bench", query: "(node[amenity=bench];node(w);)", label: i18n.t("overpass-presets.bench") },
{ key: "bicycleparking", query: "nwr[amenity=bicycle_parking]", label: i18n.t("overpass-presets.bicycleparking") },
{ key: "bicyclerental", query: "nwr[amenity=bicycle_rental]", label: i18n.t("overpass-presets.bicyclerental") },
{ key: "cinema", query: "nwr[amenity=cinema]", label: i18n.t("overpass-presets.cinema") },
{ key: "clinic", query: "nwr[amenity=clinic]", label: i18n.t("overpass-presets.clinic") },
{ key: "drinkingwater", query: "nwr[amenity=drinking_water]", label: i18n.t("overpass-presets.drinkingwater") },
{ key: "embassy", query: "nwr[amenity=embassy]", label: i18n.t("overpass-presets.embassy") },
{ key: "firestation", query: "nwr[amenity=fire_station]", label: i18n.t("overpass-presets.firestation") },
{ key: "fuel", query: "nwr[amenity=fuel]", label: i18n.t("overpass-presets.fuel") },
{ key: "hospital", query: "nwr[amenity=hospital]", label: i18n.t("overpass-presets.hospital") },
{ key: "library", query: "nwr[amenity=library]", label: i18n.t("overpass-presets.library") },
{ key: "musicschool", query: "nwr[amenity=music_school]", label: i18n.t("overpass-presets.musicschool") },
{ key: "parking", query: "nwr[amenity=parking]", label: i18n.t("overpass-presets.parking") },
{ key: "pharmacy", query: "nwr[amenity=pharmacy]", label: i18n.t("overpass-presets.pharmacy") },
{ key: "police", query: "nwr[amenity=police]", label: i18n.t("overpass-presets.police") },
{ key: "postbox", query: "(node[amenity=post_box];node(w);)", label: i18n.t("overpass-presets.postbox") },
{ key: "postoffice", query: "nwr[amenity=post_office]", label: i18n.t("overpass-presets.postoffice") },
{ key: "school", query: "nwr[amenity~'^school$|^college$']", label: i18n.t("overpass-presets.school") },
{ key: "shower", query: "(nwr[amenity=shower];nwr[shower=yes];)", label: i18n.t("overpass-presets.shower") },
{ key: "taxi", query: "nwr[amenity=taxi]", label: i18n.t("overpass-presets.taxi") },
{ key: "theatre", query: "nwr[amenity=theatre]", label: i18n.t("overpass-presets.theatre") },
{ key: "toilets", query: "nwr[amenity=toilets]", label: i18n.t("overpass-presets.toilets") },
{ key: "university", query: "nwr[amenity=university]", label: i18n.t("overpass-presets.university") }
], [
// Check for various religions. We check on 5 religions AND also on a general place_of_worship but excluding the others.
// zaterdag 9 januari 2016 Included rel for the stand-alone religions
{ key: "worship", query: "nwr[amenity=place_of_worship][religion!~'christian|muslim|buddhist|hindu|jewish']", label: i18n.t("overpass-presets.worship") },
{ key: "church", query: "nwr[amenity=place_of_worship][religion=christian]", label: i18n.t("overpass-presets.church") },
{ key: "mosque", query: "nwr[amenity=place_of_worship][religion=muslim]", label: i18n.t("overpass-presets.mosque") },
{ key: "buddhist", query: "nwr[amenity=place_of_worship][religion=buddhist]", label: i18n.t("overpass-presets.buddhist") },
{ key: "hindu", query: "nwr[amenity=place_of_worship][religion=hindu]", label: i18n.t("overpass-presets.hindu") },
{ key: "synagogue", query: "nwr[amenity=place_of_worship][religion=jewish]", label: i18n.t("overpass-presets.synagogue") },
// Check only for cemetery for human beings
{ key: "cemetery", query: "nwr[landuse=cemetery][animal!~'.']", label: i18n.t("overpass-presets.cemetery") }
]
]
]
},
{
label: "Tourism",
presets: [
[
// places to see
{ key: "abandoned", query: `(nwr[~"^abandoned(:|$)"~"."][!"abandoned:highway"];nwr[~"^ruins(:|$)"~"."];)`, label: "Abandoned" },
{ key: "artscentre", query: "(node[amenity=arts_centre];way[amenity=arts_centre];rel[amenity=arts_centre];)", label: "Arts centre" },
{ key: "artwork", query: "(node[tourism=artwork][artwork_type!~'statue'];way[tourism=artwork];rel[tourism=artwork];)", label: "Artwork" },
{ key: "attraction", query: "(node[tourism=attraction];way[tourism=attraction];rel[tourism=attraction];)", label: "Attraction" },
{ key: "casino", query: "(node[leisure=casino];way[leisure=casino];rel[leisure=casino];)", label: "Casino" },
{ key: "castle", query: "(node[historic=castle];way[historic=castle];rel[historic=castle];)", label: "Castle" },
{ key: "gallery", query: "(node[tourism=gallery];way[tourism=gallery];rel[tourism=gallery];)", label: "Gallery" },
{ key: "heritage", query: "(node[heritage];way[heritage];rel[heritage];)", label: "Heritage" },
// Check for all historic tags but exclude those that already have their own
{ key: "historic", query: "(node[historic][historic!~'memorial|monument|statue|castle'];way[historic][historic!~'memorial|monument|statue|castle'];rel[historic][historic!~'memorial|monument|statue|castle'];)", label: "Historic" },
{ key: "touristinformation", query: "(node[tourism=information];way[tourism=information];)", label: "Information" },
{ key: "monument", query: "(node[historic~'^monument$|^memorial$'];way[historic~'^monument$|^memorial$'];rel[historic~'^monument$|^memorial$'];)", label: "Monument/memorial" },
{ key: "monumentaltree", query: "(node[natural=tree][monument=yes];)", label: "Monumental Tree" },
{ key: "museum", query: "(node[tourism=museum];way[tourism=museum];rel[tourism=museum];)", label: "Museum" },
{ key: "nudism", query: "nwr[nudism][nudism!=no]", label: "Nudism" },
{ key: "picnic", query: "(node[tourism=picnic_site];way[tourism=picnic_site];rel[tourism=picnic_site];node[leisure=picnic_table];)", label: "Picnic" },
{ key: "statue", query: "(node[historic=statue];node[landmark=statue];node[tourism=artwork][artwork_type=statue];)", label: "Statue" },
{ key: "themepark", query: "(node[tourism=theme_park];way[tourism=theme_park];rel[tourism=theme_park];)", label: "Theme park" },
{ key: "viewpoint", query: "(node[tourism=viewpoint];way[tourism=viewpoint];rel[tourism=viewpoint];)", label: "Viewpoint" },
{ key: "vineyard", query: "(node[landuse=vineyard];way[landuse=vineyard];rel[landuse=vineyard];)", label: "Vineyard" },
{ key: "windmill", query: "(node[man_made=windmill];way[man_made=windmill];rel[man_made=windmill];)", label: "Windmill" },
{ key: "watermill", query: "(node[man_made=watermill];way[man_made=watermill];rel[man_made=watermill];)", label: "Watermill" },
{ key: "zoo", query: "(node[tourism=zoo];way[tourism=zoo];rel[tourism=zoo];)", label: "ZOO" }
], [
{ key: "tourism", query: "(node[tourism=yes];way[tourism=yes];rel[tourism=yes];)", label: "Tourism=yes" }
},
{
label: i18n.t("overpass-presets.category-tourism"),
presets: [
[
// places to see
{ key: "abandoned", query: `(nwr[~"^abandoned(:|$)"~"."][!"abandoned:highway"];nwr[~"^ruins(:|$)"~"."];)`, label: i18n.t("overpass-presets.abandoned") },
{ key: "artscentre", query: "(node[amenity=arts_centre];way[amenity=arts_centre];rel[amenity=arts_centre];)", label: i18n.t("overpass-presets.artscentre") },
{ key: "artwork", query: "(node[tourism=artwork][artwork_type!~'statue'];way[tourism=artwork];rel[tourism=artwork];)", label: i18n.t("overpass-presets.artwork") },
{ key: "attraction", query: "(node[tourism=attraction];way[tourism=attraction];rel[tourism=attraction];)", label: i18n.t("overpass-presets.attraction") },
{ key: "casino", query: "(node[leisure=casino];way[leisure=casino];rel[leisure=casino];)", label: i18n.t("overpass-presets.casino") },
{ key: "castle", query: "(node[historic=castle];way[historic=castle];rel[historic=castle];)", label: i18n.t("overpass-presets.castle") },
{ key: "gallery", query: "(node[tourism=gallery];way[tourism=gallery];rel[tourism=gallery];)", label: i18n.t("overpass-presets.gallery") },
{ key: "heritage", query: "(node[heritage];way[heritage];rel[heritage];)", label: i18n.t("overpass-presets.heritage") },
// Check for all historic tags but exclude those that already have their own
{ key: "historic", query: "(node[historic][historic!~'memorial|monument|statue|castle'];way[historic][historic!~'memorial|monument|statue|castle'];rel[historic][historic!~'memorial|monument|statue|castle'];)", label: i18n.t("overpass-presets.historic") },
{ key: "touristinformation", query: "(node[tourism=information];way[tourism=information];)", label: i18n.t("overpass-presets.touristinformation") },
{ key: "monument", query: "(node[historic~'^monument$|^memorial$'];way[historic~'^monument$|^memorial$'];rel[historic~'^monument$|^memorial$'];)", label: i18n.t("overpass-presets.monument") },
{ key: "monumentaltree", query: "(node[natural=tree][monument=yes];)", label: i18n.t("overpass-presets.monumentaltree") },
{ key: "museum", query: "(node[tourism=museum];way[tourism=museum];rel[tourism=museum];)", label: i18n.t("overpass-presets.museum") },
{ key: "nudism", query: "nwr[nudism][nudism!=no]", label: i18n.t("overpass-presets.nudism") },
{ key: "picnic", query: "(node[tourism=picnic_site];way[tourism=picnic_site];rel[tourism=picnic_site];node[leisure=picnic_table];)", label: i18n.t("overpass-presets.picnic") },
{ key: "statue", query: "(node[historic=statue];node[landmark=statue];node[tourism=artwork][artwork_type=statue];)", label: i18n.t("overpass-presets.statue") },
{ key: "themepark", query: "(node[tourism=theme_park];way[tourism=theme_park];rel[tourism=theme_park];)", label: i18n.t("overpass-presets.themepark") },
{ key: "viewpoint", query: "(node[tourism=viewpoint];way[tourism=viewpoint];rel[tourism=viewpoint];)", label: i18n.t("overpass-presets.viewpoint") },
{ key: "vineyard", query: "(node[landuse=vineyard];way[landuse=vineyard];rel[landuse=vineyard];)", label: i18n.t("overpass-presets.vineyard") },
{ key: "windmill", query: "(node[man_made=windmill];way[man_made=windmill];rel[man_made=windmill];)", label: i18n.t("overpass-presets.windmill") },
{ key: "watermill", query: "(node[man_made=watermill];way[man_made=watermill];rel[man_made=watermill];)", label: i18n.t("overpass-presets.watermill") },
{ key: "zoo", query: "(node[tourism=zoo];way[tourism=zoo];rel[tourism=zoo];)", label: i18n.t("overpass-presets.zoo") }
], [
{ key: "tourism", query: "(node[tourism=yes];way[tourism=yes];rel[tourism=yes];)", label: i18n.t("overpass-presets.tourism") }
]
]
]
},
{
label: "Hotels",
presets: [
[
// Places to stay
{ key: "alpinehut", query: "(node[tourism=alpine_hut];way[tourism=alpine_hut];rel[tourism=alpine_hut];)", label: "Alpine hut" },
{ key: "apartment", query: "(node[tourism=apartment];way[tourism=apartment];rel[tourism=apartment];)", label: "Apartment" },
{ key: "campsite", query: "(node[tourism=camp_site][backcountry!=yes];way[tourism=camp_site][backcountry!=yes];rel[tourism=camp_site][backcountry!=yes];)", label: "Camp site" },
{ key: "chalet", query: "(node[tourism=chalet];way[tourism=chalet];rel[tourism=chalet];)", label: "Chalet" },
{ key: "guesthouse", query: "(node[tourism~'guest_house|bed_and_breakfast'];way[tourism~'guest_house|bed_and_breakfast'];rel[tourism~'guest_house|bed_and_breakfast'];)", label: "Guest house" },
{ key: "hostel", query: "(node[tourism=hostel];way[tourism=hostel];rel[tourism=hostel];)", label: "Hostel" },
{ key: "hotel", query: "(node[tourism=hotel];way[tourism=hotel];rel[tourism=hotel];)", label: "Hotel" },
{ key: "motel", query: "(node[tourism=motel];way[tourism=motel];rel[tourism=motel];)", label: "Motel" }
], [
{ key: "casino", query: "(node[amenity=casino];way[amenity=casino];rel[amenity=casino];)", label: "Casino" },
{ key: "spa", query: "(node[leisure=spa];way[leisure=spa];rel[leisure=spa];)", label: "Spa" },
{ key: "sauna", query: "(node[leisure=sauna];way[leisure=sauna];rel[leisure=sauna];)", label: "Sauna" }
},
{
label: i18n.t("overpass-presets.category-hotels"),
presets: [
[
// Places to stay
{ key: "alpinehut", query: "(node[tourism=alpine_hut];way[tourism=alpine_hut];rel[tourism=alpine_hut];)", label: i18n.t("overpass-presets.alpinehut") },
{ key: "apartment", query: "(node[tourism=apartment];way[tourism=apartment];rel[tourism=apartment];)", label: i18n.t("overpass-presets.apartment") },
{ key: "campsite", query: "(node[tourism=camp_site][backcountry!=yes];way[tourism=camp_site][backcountry!=yes];rel[tourism=camp_site][backcountry!=yes];)", label: i18n.t("overpass-presets.campsite") },
{ key: "chalet", query: "(node[tourism=chalet];way[tourism=chalet];rel[tourism=chalet];)", label: i18n.t("overpass-presets.chalet") },
{ key: "guesthouse", query: "(node[tourism~'guest_house|bed_and_breakfast'];way[tourism~'guest_house|bed_and_breakfast'];rel[tourism~'guest_house|bed_and_breakfast'];)", label: i18n.t("overpass-presets.guesthouse") },
{ key: "hostel", query: "(node[tourism=hostel];way[tourism=hostel];rel[tourism=hostel];)", label: i18n.t("overpass-presets.hostel") },
{ key: "hotel", query: "(node[tourism=hotel];way[tourism=hotel];rel[tourism=hotel];)", label: i18n.t("overpass-presets.hotel") },
{ key: "motel", query: "(node[tourism=motel];way[tourism=motel];rel[tourism=motel];)", label: i18n.t("overpass-presets.motel") }
], [
{ key: "casino", query: "(node[amenity=casino];way[amenity=casino];rel[amenity=casino];)", label: i18n.t("overpass-presets.casino") },
{ key: "spa", query: "(node[leisure=spa];way[leisure=spa];rel[leisure=spa];)", label: i18n.t("overpass-presets.spa") },
{ key: "sauna", query: "(node[leisure=sauna];way[leisure=sauna];rel[leisure=sauna];)", label: i18n.t("overpass-presets.sauna") }
]
]
]
},
{
label: "Sports",
presets: [
[
{ key: "americanfootball", query: "(way[sport=american_football];way[sport=american_football];)", label: "American football" },
{ key: "baseball", query: "(way[sport=baseball];node[sport=baseball];)", label: "Baseball" },
{ key: "basketball", query: "(way[sport=basketball];node[sport=basketball];)", label: "Basketball" },
{ key: "cycling", query: "(way[sport=cycling];node[sport=cycling];rel[sport=cycling];)", label: "Cycling" },
{ key: "gymnastics", query: "(way[sport=gymnastics];node[sport=gymnastics];rel[sport=gymnastics];)", label: "Gymnastics" },
{ key: "golfcourse", query: "(way[leisure=golf_course];way[sport=golf];node[leisure=golf_course];node[sport=golf];rel[leisure=golf_course];rel[sport=golf];)", label: "Golf" },
{ key: "hockey", query: "(way[sport=hockey];node[sport=hockey];rel[sport=hockey];way[sport=field_hockey];node[sport=field_hockey];rel[sport=field_hockey];)", label: "Hockey" },
{ key: "horseracing", query: "(way[sport=horse_racing];way[sport=equestrian];node[sport=horse_racing];node[sport=equestrian];)", label: "Horse racing" },
{ key: "icehockey", query: "(way[sport=ice_hockey];node[sport=ice_hockey];rel[sport=ice_hockey];way[leisure=ice_rink];node[leisure=ice_rink];)", label: "Ice hockey" },
{ key: "soccer", query: "(way[sport=soccer];node[sport=soccer];rel[sport=soccer];)", label: "Soccer" },
{ key: "sportscentre", query: "(way[leisure=sports_centre];node[leisure=sports_centre];rel[leisure=sports_centre];)", label: "Sports centre" },
{ key: "surfing", query: "(way[sport=surfing];node[sport=surfing];rel[sport=surfing];)", label: "Surfing" },
{ key: "swimming", query: "(way[sport=swimming];node[sport=swimming];rel[sport=swimming];)", label: "Swimming" },
{ key: "tabletennis", query: "(way[sport=table_tennis];node[sport=table_tennis];)", label: "Table tennis" },
{ key: "tennis", query: "(way[sport=tennis];node[sport=tennis];)", label: "Tennis" },
{ key: "volleyball", query: "(way[sport=volleyball];node[sport=volleyball];)", label: "Volleyball" }
},
{
label: i18n.t("overpass-presets.category-sports"),
presets: [
[
{ key: "americanfootball", query: "(way[sport=american_football];way[sport=american_football];)", label: i18n.t("overpass-presets.americanfootball") },
{ key: "baseball", query: "(way[sport=baseball];node[sport=baseball];)", label: i18n.t("overpass-presets.baseball") },
{ key: "basketball", query: "(way[sport=basketball];node[sport=basketball];)", label: i18n.t("overpass-presets.basketball") },
{ key: "cycling", query: "(way[sport=cycling];node[sport=cycling];rel[sport=cycling];)", label: i18n.t("overpass-presets.cycling") },
{ key: "gymnastics", query: "(way[sport=gymnastics];node[sport=gymnastics];rel[sport=gymnastics];)", label: i18n.t("overpass-presets.gymnastics") },
{ key: "golfcourse", query: "(way[leisure=golf_course];way[sport=golf];node[leisure=golf_course];node[sport=golf];rel[leisure=golf_course];rel[sport=golf];)", label: i18n.t("overpass-presets.golfcourse") },
{ key: "hockey", query: "(way[sport=hockey];node[sport=hockey];rel[sport=hockey];way[sport=field_hockey];node[sport=field_hockey];rel[sport=field_hockey];)", label: i18n.t("overpass-presets.hockey") },
{ key: "horseracing", query: "(way[sport=horse_racing];way[sport=equestrian];node[sport=horse_racing];node[sport=equestrian];)", label: i18n.t("overpass-presets.horseracing") },
{ key: "icehockey", query: "(way[sport=ice_hockey];node[sport=ice_hockey];rel[sport=ice_hockey];way[leisure=ice_rink];node[leisure=ice_rink];)", label: i18n.t("overpass-presets.icehockey") },
{ key: "soccer", query: "(way[sport=soccer];node[sport=soccer];rel[sport=soccer];)", label: i18n.t("overpass-presets.soccer") },
{ key: "sportscentre", query: "(way[leisure=sports_centre];node[leisure=sports_centre];rel[leisure=sports_centre];)", label: i18n.t("overpass-presets.sportscentre") },
{ key: "surfing", query: "(way[sport=surfing];node[sport=surfing];rel[sport=surfing];)", label: i18n.t("overpass-presets.surfing") },
{ key: "swimming", query: "(way[sport=swimming];node[sport=swimming];rel[sport=swimming];)", label: i18n.t("overpass-presets.swimming") },
{ key: "tabletennis", query: "(way[sport=table_tennis];node[sport=table_tennis];)", label: i18n.t("overpass-presets.tabletennis") },
{ key: "tennis", query: "(way[sport=tennis];node[sport=tennis];)", label: i18n.t("overpass-presets.tennis") },
{ key: "volleyball", query: "(way[sport=volleyball];node[sport=volleyball];)", label: i18n.t("overpass-presets.volleyball") }
]
]
]
},
{
label: "Shops",
presets: [
[
//Various shops (excluding food)
{ key: "beautyshop", query: "(node[shop=beauty];way[shop=beauty];rel[shop=beauty];)", label: "Beauty" },
{ key: "bicycleshop", query: "(node[shop=bicycle];way[shop=bicycle];rel[shop=bicycle];)", label: "Bicycle" },
{ key: "bookshop", query: "(node[shop~'books|stationary'];way[shop~'books|stationary'];rel[shop~'books|stationary'];)", label: "Books/Stationary" },
{ key: "carshop", query: "(node[shop=car];way[shop=car];rel[shop=car];)", label: "Car" },
{ key: "chemist", query: "(node[shop=chemist];way[shop=chemist];rel[shop=chemist];)", label: "Chemist" },
{ key: "clothesshop", query: "(node[shop=clothes];way[shop=clothes];rel[shop=clothes];)", label: "Clothes" },
{ key: "copyshop", query: "(node[shop=copyshop];way[shop=copyshop];rel[shop=copyshop];)", label: "Copyshop" },
{ key: "cosmeticsshop", query: "(node[shop=cosmetics];way[shop=cosmetics];rel[shop=cosmetics];)", label: "Cosmetics" },
{ key: "departmentstore", query: "(node[shop=department_store];way[shop=department_store];rel[shop=department_store];)", label: "Department store" },
{ key: "diyshop", query: "(node[shop~'doityourself|hardware'];way[shop~'doityourself|hardware'];rel[shop~'doityourself|hardware'];)", label: "DIY/hardware" },
{ key: "florist", query: "(nwr[shop=florist];nwr[shop=garden_centre];)", label: "Florist" },
{ key: "gardencentre", query: "(node[shop=garden_centre];way[shop=garden_centre];rel[shop=garden_centre];)", label: "Garden centre" },
{ key: "generalshop", query: "(node[shop=general];way[shop=general];rel[shop=general];)", label: "General" },
{ key: "giftshop", query: "(node[shop=gift];way[shop=gift];rel[shop=gift];)", label: "Gift" },
{ key: "hairdresser", query: "(node[shop=hairdresser];way[shop=hairdresser];rel[shop=hairdresser];)", label: "Hairdresser" },
// See tagging-list january 2016
{ key: "jewelleryshop", query: "(node[shop~'jewelry|jewellery'];way[shop~'jewelry|jewellery'];rel[shop~'jewelry|jewellery'];)", label: "Jewelry" },
{ key: "kiosk", query: "(node[shop=kiosk];way[shop=kiosk];rel[shop=kiosk];)", label: "Kiosk" },
{ key: "leathershop", query: "(node[shop=leather];way[shop=leather];rel[shop=leather];)", label: "Leather" },
{ key: "marketplace", query: "(node[amenity=marketplace];way[amenity=marketplace];rel[amenity=marketplace];)", label: "Marketplace" },
{ key: "musicshop", query: "(node[shop=musical_instrument];way[shop=musical_instrument];rel[shop=musical_instrument];)", label: "Musical instrument" },
{ key: "optician", query: "(node[shop=optician];way[shop=optician];rel[shop=optician];)", label: "Optician" },
{ key: "petshop", query: "(node[shop=pets];way[shop=pets];rel[shop=pets];)", label: "Pets" },
{ key: "phoneshop", query: "(node[shop=mobile_phone];way[shop=mobile_phone];rel[shop=mobile_phone];)", label: "Phone" },
{ key: "photoshop", query: "(node[shop=photo];way[shop=photo];rel[shop=photo];)", label: "Photo" },
{ key: "shoeshop", query: "(node[shop=shoes];way[shop=shoes];)", label: "Shoes" },
{ key: "mall", query: "(node[shop=mall];way[shop=mall];rel[shop=mall];)", label: "Shopping centre" },
{ key: "textileshop", query: "(node[shop=textiles];way[shop=textiles];rel[shop=textiles];)", label: "Textiles" },
{ key: "toyshop", query: "(node[shop=toys];way[shop=toys];rel[shop=toys];)", label: "Toys" }
},
{
label: i18n.t("overpass-presets.category-shops"),
presets: [
[
//Various shops (excluding food)
{ key: "beautyshop", query: "(node[shop=beauty];way[shop=beauty];rel[shop=beauty];)", label: i18n.t("overpass-presets.beautyshop") },
{ key: "bicycleshop", query: "(node[shop=bicycle];way[shop=bicycle];rel[shop=bicycle];)", label: i18n.t("overpass-presets.bicycleshop") },
{ key: "bookshop", query: "(node[shop~'books|stationary'];way[shop~'books|stationary'];rel[shop~'books|stationary'];)", label: i18n.t("overpass-presets.bookshop") },
{ key: "carshop", query: "(node[shop=car];way[shop=car];rel[shop=car];)", label: i18n.t("overpass-presets.carshop") },
{ key: "chemist", query: "(node[shop=chemist];way[shop=chemist];rel[shop=chemist];)", label: i18n.t("overpass-presets.chemist") },
{ key: "clothesshop", query: "(node[shop=clothes];way[shop=clothes];rel[shop=clothes];)", label: i18n.t("overpass-presets.clothesshop") },
{ key: "copyshop", query: "(node[shop=copyshop];way[shop=copyshop];rel[shop=copyshop];)", label: i18n.t("overpass-presets.copyshop") },
{ key: "cosmeticsshop", query: "(node[shop=cosmetics];way[shop=cosmetics];rel[shop=cosmetics];)", label: i18n.t("overpass-presets.cosmeticsshop") },
{ key: "departmentstore", query: "(node[shop=department_store];way[shop=department_store];rel[shop=department_store];)", label: i18n.t("overpass-presets.departmentstore") },
{ key: "diyshop", query: "(node[shop~'doityourself|hardware'];way[shop~'doityourself|hardware'];rel[shop~'doityourself|hardware'];)", label: i18n.t("overpass-presets.diyshop") },
{ key: "florist", query: "(nwr[shop=florist];nwr[shop=garden_centre];)", label: i18n.t("overpass-presets.florist") },
{ key: "gardencentre", query: "(node[shop=garden_centre];way[shop=garden_centre];rel[shop=garden_centre];)", label: i18n.t("overpass-presets.gardencentre") },
{ key: "generalshop", query: "(node[shop=general];way[shop=general];rel[shop=general];)", label: i18n.t("overpass-presets.generalshop") },
{ key: "giftshop", query: "(node[shop=gift];way[shop=gift];rel[shop=gift];)", label: i18n.t("overpass-presets.giftshop") },
{ key: "hairdresser", query: "(node[shop=hairdresser];way[shop=hairdresser];rel[shop=hairdresser];)", label: i18n.t("overpass-presets.hairdresser") },
// See tagging-list january 2016
{ key: "jewelleryshop", query: "(node[shop~'jewelry|jewellery'];way[shop~'jewelry|jewellery'];rel[shop~'jewelry|jewellery'];)", label: i18n.t("overpass-presets.jewelleryshop") },
{ key: "kiosk", query: "(node[shop=kiosk];way[shop=kiosk];rel[shop=kiosk];)", label: i18n.t("overpass-presets.kiosk") },
{ key: "leathershop", query: "(node[shop=leather];way[shop=leather];rel[shop=leather];)", label: i18n.t("overpass-presets.leathershop") },
{ key: "marketplace", query: "(node[amenity=marketplace];way[amenity=marketplace];rel[amenity=marketplace];)", label: i18n.t("overpass-presets.marketplace") },
{ key: "musicshop", query: "(node[shop=musical_instrument];way[shop=musical_instrument];rel[shop=musical_instrument];)", label: i18n.t("overpass-presets.musicshop") },
{ key: "optician", query: "(node[shop=optician];way[shop=optician];rel[shop=optician];)", label: i18n.t("overpass-presets.optician") },
{ key: "petshop", query: "(node[shop=pets];way[shop=pets];rel[shop=pets];)", label: i18n.t("overpass-presets.petshop") },
{ key: "phoneshop", query: "(node[shop=mobile_phone];way[shop=mobile_phone];rel[shop=mobile_phone];)", label: i18n.t("overpass-presets.phoneshop") },
{ key: "photoshop", query: "(node[shop=photo];way[shop=photo];rel[shop=photo];)", label: i18n.t("overpass-presets.photoshop") },
{ key: "shoeshop", query: "(node[shop=shoes];way[shop=shoes];)", label: i18n.t("overpass-presets.shoeshop") },
{ key: "mall", query: "(node[shop=mall];way[shop=mall];rel[shop=mall];)", label: i18n.t("overpass-presets.mall") },
{ key: "textileshop", query: "(node[shop=textiles];way[shop=textiles];rel[shop=textiles];)", label: i18n.t("overpass-presets.textileshop") },
{ key: "toyshop", query: "(node[shop=toys];way[shop=toys];rel[shop=toys];)", label: i18n.t("overpass-presets.toyshop") }
]
]
]
},
{
label: "Food shops",
presets: [
[
// food shops
{ key: "alcoholshop", query: "(node[shop=alcohol];way[shop=alcohol];rel[shop=alcohol];)", label: "Alcohol" },
{ key: "bakery", query: "(node[shop=bakery];way[shop=bakery];)", label: "Bakery" },
{ key: "beverageshop", query: "(node[shop=beverages];way[shop=beverages];rel[shop=beverages];)", label: "Beverages" },
{ key: "butcher", query: "(node[shop=butcher];way[shop=butcher];rel[shop=butcher];)", label: "Butcher" },
{ key: "cheeseshop", query: "(node[shop=cheese];way[shop=cheese];rel[shop=cheese];)", label: "Cheese" },
{ key: "confectionery", query: "(node[shop~'chocolate|confectionery'];way[shop~'chocolate|confectionery'];rel[shop~'chocolate|confectionery'];)", label: "Chocolate/Confectionery" },
{ key: "coffeeshop", query: "(node[shop=coffee];way[shop=coffee];rel[shop=coffee];)", label: "Coffee" },
{ key: "dairyshop", query: "(node[shop=dairy];way[shop=dairy];)", label: "Dairy" },
{ key: "delishop", query: "(node[shop=deli];way[shop=deli];node[shop=delicatessen];way[shop=delicatessen];)", label: "Deli" },
{ key: "groceryshop", query: "(node[shop=grocery];way[shop=grocery];)", label: "Grocery" },
{ key: "organicshop", query: "(node[shop=organic];way[shop=organic];rel[shop=organic];)", label: "Organic" },
{ key: "seafoodshop", query: "(node[shop=seafood];way[shop=seafood];rel[shop=seafood];)", label: "Seafood" },
{ key: "supermarket", query: "(node[shop=supermarket];way[shop=supermarket];)", label: "Supermarket" },
{ key: "wineshop", query: "(node[shop=wine];way[shop=wine];rel[shop=wine];)", label: "Wine" }
},
{
label: i18n.t("overpass-presets.category-food-shops"),
presets: [
[
// food shops
{ key: "alcoholshop", query: "(node[shop=alcohol];way[shop=alcohol];rel[shop=alcohol];)", label: i18n.t("overpass-presets.alcoholshop") },
{ key: "bakery", query: "(node[shop=bakery];way[shop=bakery];)", label: i18n.t("overpass-presets.bakery") },
{ key: "beverageshop", query: "(node[shop=beverages];way[shop=beverages];rel[shop=beverages];)", label: i18n.t("overpass-presets.beverageshop") },
{ key: "butcher", query: "(node[shop=butcher];way[shop=butcher];rel[shop=butcher];)", label: i18n.t("overpass-presets.butcher") },
{ key: "cheeseshop", query: "(node[shop=cheese];way[shop=cheese];rel[shop=cheese];)", label: i18n.t("overpass-presets.cheeseshop") },
{ key: "confectionery", query: "(node[shop~'chocolate|confectionery'];way[shop~'chocolate|confectionery'];rel[shop~'chocolate|confectionery'];)", label: i18n.t("overpass-presets.confectionery") },
{ key: "coffeeshop", query: "(node[shop=coffee];way[shop=coffee];rel[shop=coffee];)", label: i18n.t("overpass-presets.coffeeshop") },
{ key: "dairyshop", query: "(node[shop=dairy];way[shop=dairy];)", label: i18n.t("overpass-presets.dairyshop") },
{ key: "delishop", query: "(node[shop=deli];way[shop=deli];node[shop=delicatessen];way[shop=delicatessen];)", label: i18n.t("overpass-presets.delishop") },
{ key: "groceryshop", query: "(node[shop=grocery];way[shop=grocery];)", label: i18n.t("overpass-presets.groceryshop") },
{ key: "organicshop", query: "(node[shop=organic];way[shop=organic];rel[shop=organic];)", label: i18n.t("overpass-presets.organicshop") },
{ key: "seafoodshop", query: "(node[shop=seafood];way[shop=seafood];rel[shop=seafood];)", label: i18n.t("overpass-presets.seafoodshop") },
{ key: "supermarket", query: "(node[shop=supermarket];way[shop=supermarket];)", label: i18n.t("overpass-presets.supermarket") },
{ key: "wineshop", query: "(node[shop=wine];way[shop=wine];rel[shop=wine];)", label: i18n.t("overpass-presets.wineshop") }
]
]
]
},
{
label: "Restaurants",
presets: [
[
// places to eat
{ key: "bar", query: "(node[amenity=bar];way[amenity=bar];rel[amenity=bar];)", label: "Bar" },
{ key: "bbq", query: "(node[amenity=bbq];way[amenity=bbq];)", label: "BBQ" },
{ key: "biergarten", query: "(node[amenity=biergarten];way[amenity=biergarten];)", label: "Biergarten" },
{ key: "cafe", query: "(node[amenity=cafe];way[amenity=cafe];rel[amenity=cafe];)", label: "Cafe" },
{ key: "fastfood", query: "(node[amenity=fast_food];way[amenity=fast_food];rel[amenity=fast_food];)", label: "Fast food" },
{ key: "foodcourt", query: "(node[amenity=food_court];way[amenity=food_court];)", label: "Food court" },
{ key: "icecream", query: "(node[amenity=ice_cream];way[amenity=ice_cream];rel[amenity=ice_cream];node[cuisine=ice_cream];way[cuisine=ice_cream];rel[cuisine=ice_cream];)", label: "Ice cream" },
{ key: "pub", query: "(node[amenity=pub];way[amenity=pub];rel[amenity=pub];)", label: "Pub" },
{ key: "restaurant", query: "(node[amenity=restaurant];way[amenity=restaurant];rel[amenity=restaurant];)", label: "Restaurant" }
},
{
label: i18n.t("overpass-presets.category-restaurants"),
presets: [
[
// places to eat
{ key: "bar", query: "(node[amenity=bar];way[amenity=bar];rel[amenity=bar];)", label: i18n.t("overpass-presets.bar") },
{ key: "bbq", query: "(node[amenity=bbq];way[amenity=bbq];)", label: i18n.t("overpass-presets.bbq") },
{ key: "biergarten", query: "(node[amenity=biergarten];way[amenity=biergarten];)", label: i18n.t("overpass-presets.biergarten") },
{ key: "cafe", query: "(node[amenity=cafe];way[amenity=cafe];rel[amenity=cafe];)", label: i18n.t("overpass-presets.cafe") },
{ key: "fastfood", query: "(node[amenity=fast_food];way[amenity=fast_food];rel[amenity=fast_food];)", label: i18n.t("overpass-presets.fastfood") },
{ key: "foodcourt", query: "(node[amenity=food_court];way[amenity=food_court];)", label: i18n.t("overpass-presets.foodcourt") },
{ key: "icecream", query: "(node[amenity=ice_cream];way[amenity=ice_cream];rel[amenity=ice_cream];node[cuisine=ice_cream];way[cuisine=ice_cream];rel[cuisine=ice_cream];)", label: i18n.t("overpass-presets.icecream") },
{ key: "pub", query: "(node[amenity=pub];way[amenity=pub];rel[amenity=pub];)", label: i18n.t("overpass-presets.pub") },
{ key: "restaurant", query: "(node[amenity=restaurant];way[amenity=restaurant];rel[amenity=restaurant];)", label: i18n.t("overpass-presets.restaurant") }
]
]
]
},
{
label: "Various",
presets: [
[
{ key: "busstop", query: "(node[highway=bus_stop];)", label: "Busstop" },
{ key: "bicyclecharging", query: "(node[amenity=charging_station][bicycle=yes];rel[amenity=charging_station][bicycle=yes];)", label: "E-bike charging" },
{ key: "kindergarten", query: "(node[amenity~'childcare|kindergarten'];way[amenity~'childcare|kindergarten'];rel[amenity~'childcare|kindergarten'];)", label: "Kindergarten" },
{ key: "marketplace", query: "(node[amenity=marketplace];way[amenity=marketplace];rel[amenity=marketplace];)", label: "Marketplace" },
{ key: "office", query: "(node[office];way[office];rel[office];)", label: "Office" },
{ key: "recycling", query: "(node[amenity=recycling];way[amenity=recycling];rel[amenity=recycling];)", label: "Recycling" },
{ key: "travelagency", query: "(node[shop=travel_agency];way[shop=travel_agency];rel[shop=travel_agency];)", label: "Travel agency" }
],
[
{ key: "defibrillator", query: "(node[emergency=defibrillator];way[emergency=defibrillator];rel[emergency=defibrillator];)", label: "Defibrillator - AED" },
{ key: "fireextinguisher", query: "(node[emergency=fire_extinguisher];node[emergency=fire_hose];)", label: "Fire hose/extinguisher" },
],
[
// Do not include a relation for the fixme, as it produces a lot of extraneous data
{ key: "fixme", query: "(node[fixme];way[fixme];node[FIXME];way[FIXME];)", label: "fixme" },
// { key: "", query: "(node[~'^fixme$',i];way[~'^fixme$',i];)", label: "fixme" },
{ key: "notenode", query: "(node[note];way[note];)", label: "Note-Node" },
{ key: "noteway", query: "(way[note];)", label: "Note-Way" },
{ key: "construction", query: "(node[highway=construction];way[highway=construction];)", label: "Construction" },
{ key: "image", query: "(node[image];way[image];)", label: "Image" },
{ key: "camera", query: "(node['surveillance:type'~'camera|webcam'];)", label: "Public camera" },
],
[
{ key: "city", query: "(node[place=city];)", label: "City" },
{ key: "town", query: "(node[place=town];)", label: "Town" },
{ key: "village", query: "(node[place=village];)", label: "Village" },
{ key: "hamlet", query: "(node[place=hamlet];)", label: "Hamlet" },
{ key: "suburb", query: "(node[place=suburb];)", label: "Suburb" }
},
{
label: i18n.t("overpass-presets.category-various"),
presets: [
[
{ key: "busstop", query: "(node[highway=bus_stop];)", label: i18n.t("overpass-presets.busstop") },
{ key: "bicyclecharging", query: "(node[amenity=charging_station][bicycle=yes];rel[amenity=charging_station][bicycle=yes];)", label: i18n.t("overpass-presets.bicyclecharging") },
{ key: "kindergarten", query: "(node[amenity~'childcare|kindergarten'];way[amenity~'childcare|kindergarten'];rel[amenity~'childcare|kindergarten'];)", label: i18n.t("overpass-presets.kindergarten") },
{ key: "marketplace", query: "(node[amenity=marketplace];way[amenity=marketplace];rel[amenity=marketplace];)", label: i18n.t("overpass-presets.marketplace") },
{ key: "office", query: "(node[office];way[office];rel[office];)", label: i18n.t("overpass-presets.office") },
{ key: "recycling", query: "(node[amenity=recycling];way[amenity=recycling];rel[amenity=recycling];)", label: i18n.t("overpass-presets.recycling") },
{ key: "travelagency", query: "(node[shop=travel_agency];way[shop=travel_agency];rel[shop=travel_agency];)", label: i18n.t("overpass-presets.travelagency") }
],
[
{ key: "defibrillator", query: "(node[emergency=defibrillator];way[emergency=defibrillator];rel[emergency=defibrillator];)", label: i18n.t("overpass-presets.defibrillator") },
{ key: "fireextinguisher", query: "(node[emergency=fire_extinguisher];node[emergency=fire_hose];)", label: i18n.t("overpass-presets.fireextinguisher") },
],
[
// Do not include a relation for the fixme, as it produces a lot of extraneous data
{ key: "fixme", query: "(node[fixme];way[fixme];node[FIXME];way[FIXME];)", label: i18n.t("overpass-presets.fixme") },
// { key: "", query: "(node[~'^fixme$',i];way[~'^fixme$',i];)", label: i18n.t("overpass-presets.") },
{ key: "notenode", query: "(node[note];way[note];)", label: i18n.t("overpass-presets.notenode") },
{ key: "noteway", query: "(way[note];)", label: i18n.t("overpass-presets.noteway") },
{ key: "construction", query: "(node[highway=construction];way[highway=construction];)", label: i18n.t("overpass-presets.construction") },
{ key: "image", query: "(node[image];way[image];)", label: i18n.t("overpass-presets.image") },
{ key: "camera", query: "(node['surveillance:type'~'camera|webcam'];)", label: i18n.t("overpass-presets.camera") },
],
[
{ key: "city", query: "(node[place=city];)", label: i18n.t("overpass-presets.city") },
{ key: "town", query: "(node[place=town];)", label: i18n.t("overpass-presets.town") },
{ key: "village", query: "(node[place=village];)", label: i18n.t("overpass-presets.village") },
{ key: "hamlet", query: "(node[place=hamlet];)", label: i18n.t("overpass-presets.hamlet") },
{ key: "suburb", query: "(node[place=suburb];)", label: i18n.t("overpass-presets.suburb") }
// { key: "", query: "(way[name~'^[Ff]ietspad'];)->.fietspaden;(way(foreach.fietspaden)[highway=cycleway][name][name~'^[Ff]ietspad$'])", naam:"fietspad" }
// { key: "", query: "(way[name~'^Fietspad|^fietspad|^pad$|^Pad$|cycleway|^path$|^Path$'];node(w);way[highway=cycleway][name!~'.'];node(w);)", naam:"fietspad" }
// { key: "", query: "(way[name~'^[Ff]ietspad'];)->.fietspaden;(way(foreach.fietspaden)[highway=cycleway][name][name~'^[Ff]ietspad$'])", naam:"fietspad" }
// { key: "", query: "(way[name~'^Fietspad|^fietspad|^pad$|^Pad$|cycleway|^path$|^Path$'];node(w);way[highway=cycleway][name!~'.'];node(w);)", naam:"fietspad" }
]
]
]
}
];
}
]
};
// For backwards compatibility. Does not provide i18n reactivity.
export const overpassPresets = getAllOverpassPresets();
export function getOverpassPreset(key: string): OverpassPreset | undefined {
return overpassPresets.map((p) => p.presets).flat().flat().find((p) => p.key == key) as OverpassPreset | undefined;
return getAllOverpassPresets().map((p) => p.presets).flat().flat().find((p) => p.key == key) as OverpassPreset | undefined;
}
export function getOverpassPresets(keys: string[]): OverpassPreset[] {
return overpassPresets.map((p) => p.presets).flat().flat().filter((p) => keys.includes(p.key)) as OverpassPreset[];
return getAllOverpassPresets().map((p) => p.presets).flat().flat().filter((p) => keys.includes(p.key)) as OverpassPreset[];
}

Wyświetl plik

@ -0,0 +1,31 @@
/// <reference types="vite/client" />
import type { i18n } from "i18next";
import messagesDe from "../i18n/de.json";
import messagesEn from "../i18n/en.json";
import messagesNbNo from "../i18n/nb-NO.json";
import messagesRu from "../i18n/ru.json";
import { getAcceptHotI18n, getRawI18n, onI18nReady } from "facilmap-utils";
const namespace = "facilmap-leaflet";
onI18nReady((i18n) => {
i18n.addResourceBundle("en", namespace, messagesEn);
i18n.addResourceBundle("de", namespace, messagesDe);
i18n.addResourceBundle("nb-NO", namespace, messagesNbNo);
i18n.addResourceBundle("ru", namespace, messagesRu);
});
if (import.meta.hot) {
if (import.meta.hot) {
import.meta.hot!.accept(`../i18n/en.json`, getAcceptHotI18n("en", namespace));
import.meta.hot!.accept(`../i18n/de.json`, getAcceptHotI18n("de", namespace));
import.meta.hot!.accept(`../i18n/nb-NO.json`, getAcceptHotI18n("nb-NO", namespace));
import.meta.hot!.accept(`../i18n/ru.json`, getAcceptHotI18n("ru", namespace));
}
}
export function getI18n(): Pick<i18n, "t"> {
return {
t: getRawI18n().getFixedT(null, namespace)
};
}

Wyświetl plik

@ -15,5 +15,5 @@
{ "path": "../types/tsconfig.json" },
{ "path": "../utils/tsconfig.json" }
],
"include": ["src/**/*", "example.html", "icontest.html"]
"include": ["src/**/*", "src/**/*.json", "example.html", "icontest.html"]
}

Wyświetl plik

@ -43,6 +43,7 @@
"cheerio": "^1.0.0-rc.12",
"compression": "^1.7.4",
"compressjs": "^1.0.3",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.6",
"csv-stringify": "^6.4.6",
"dotenv": "^16.4.5",
@ -63,6 +64,7 @@
"p-throttle": "^6.1.0",
"pg": "^8.11.3",
"sequelize": "^6.37.1",
"serialize-error": "^11.0.3",
"socket.io": "^4.7.4",
"string-similarity": "^4.0.4",
"strip-bom-buf": "^4.0.0",
@ -72,6 +74,7 @@
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/cookie": "^0.6.0",
"@types/cookie-parser": "^1.4.7",
"@types/debug": "^4.1.12",
"@types/ejs": "^3.1.5",

Wyświetl plik

@ -4,6 +4,7 @@ import Database from "./database.js";
import { cloneDeep, isEqual } from "lodash-es";
import type { PadModel } from "./pad";
import { arrayToAsyncIterator } from "../utils/streams";
import { getI18n } from "../i18n.js";
const ITEMS_PER_BATCH = 5000;
@ -124,7 +125,7 @@ export default class DatabaseHelpers {
if(!types[object.typeId]) {
types[object.typeId] = await this._db.types.getType(padId, object.typeId);
if(types[object.typeId] == null)
throw new Error("Type "+object.typeId+" does not exist.");
throw new Error(getI18n().t("database.type-not-found-error", { typeId: object.typeId }));
}
const type = types[object.typeId];
@ -145,7 +146,7 @@ export default class DatabaseHelpers {
return entry != null;
}
async _getPadObject<T>(type: string, padId: PadId, id: ID): Promise<T> {
async _getPadObject<T>(type: "Marker" | "Line" | "Type" | "View" | "History", padId: PadId, id: ID): Promise<T> {
const includeData = [ "Marker", "Line" ].includes(type);
const entry = await this._db._conn.model(type).findOne({
@ -154,8 +155,9 @@ export default class DatabaseHelpers {
nest: true
});
if(entry == null)
throw new Error(type + " " + id + " of pad " + padId + " could not be found.");
if(entry == null) {
throw new Error(getI18n().t("database.object-not-found-in-pad-error", { type, id, padId }));
}
const data: any = entry.toJSON();
@ -213,7 +215,7 @@ export default class DatabaseHelpers {
return result;
}
async _updatePadObject<T>(type: string, padId: PadId, objId: ID, data: any, _noHistory?: boolean): Promise<T> {
async _updatePadObject<T>(type: "Marker" | "Line" | "View" | "Type" | "History", padId: PadId, objId: ID, data: any, _noHistory?: boolean): Promise<T> {
const includeData = [ "Marker", "Line" ].includes(type);
const makeHistory = !_noHistory && [ "Marker", "Line", "View", "Type" ].includes(type);
@ -241,7 +243,7 @@ export default class DatabaseHelpers {
return newObject;
}
async _deletePadObject<T>(type: string, padId: PadId, objId: ID): Promise<T> {
async _deletePadObject<T>(type: "Marker" | "Line" | "View" | "Type" | "History", padId: PadId, objId: ID): Promise<T> {
const includeData = [ "Marker", "Line" ].includes(type);
const makeHistory = [ "Marker", "Line", "View", "Type" ].includes(type);

Wyświetl plik

@ -3,6 +3,7 @@ import Database from "./database.js";
import type { HistoryEntry, HistoryEntryAction, HistoryEntryCreate, HistoryEntryType, ID, PadData, PadId } from "facilmap-types";
import { createModel, getDefaultIdType, makeNotNullForeignKey } from "./helpers.js";
import { cloneDeep } from "lodash-es";
import { getI18n } from "../i18n.js";
interface HistoryModel extends Model<InferAttributes<HistoryModel>, InferCreationAttributes<HistoryModel>> {
id: CreationOptional<ID>;
@ -118,12 +119,12 @@ export default class DatabaseHistory {
if(entry.type == "Pad") {
if (!entry.objectBefore) {
throw new Error("Old pad data not available.");
throw new Error(getI18n().t("database.old-pad-data-not-available-error"));
}
await this._db.pads.updatePadData(padId, entry.objectBefore);
return;
} else if (!["Marker", "Line", "View", "Type"].includes(entry.type)) {
throw new Error(`Unknown type "${entry.type}.`);
throw new Error(getI18n().t("database.unknown-type-error", { type: entry.type }));
}
const existsNow = await this._db.helpers._padObjectExists(entry.type, padId, entry.objectId);

Some files were not shown because too many files have changed in this diff Show More