Porównaj commity

...

29 Commity

Autor SHA1 Wiadomość Data
Candid Dauth 85704f17a0 Show compact checkbox fields on small screens 2024-04-12 23:35:46 +02:00
Candid Dauth f9c68cc1f5 Add parameters to hide route tab, POI tab and locate control (#251) 2024-04-12 23:04:59 +02:00
Candid Dauth 2ceab20877 Hide routing possibilities if Mapbox/ORS tokens are not set (#233) 2024-04-12 21:49:06 +02:00
Candid Dauth 381d8ee071 Fix example docker-compose file (#264) 2024-04-12 11:44:24 +02:00
Candid Dauth 1075b0730e Add button to move marker to current location 2024-04-12 00:38:27 +02:00
Candid Dauth 43de05bec1 Add "Actions" menu to marker/line info 2024-04-12 00:11:36 +02:00
Candid Dauth 6718c776fe Improve translations 2024-04-11 23:47:35 +02:00
Candid Dauth 2aad260d8c Add option to create marker at current location 2024-04-11 23:47:22 +02:00
Candid Dauth a00190e05e Fix moving marker that is hidden by cluster 2024-04-11 23:13:41 +02:00
Candid Dauth 81cd918afc Pan to location when clicking locate control again 2024-04-11 22:16:22 +02:00
Candid Dauth e1130c0a78 Disable save line button until there are 2 route points 2024-04-11 22:05:10 +02:00
Candid Dauth 8f1688ff8c Fix toast options 2024-04-11 17:24:21 +02:00
Candid Dauth 32568b1682 Clarify port publishing in example docker config (#264) 2024-04-11 10:10:42 +02:00
Candid Dauth 5698d0b8cc v4.1.0 2024-04-10 00:19:11 +02:00
Candid Dauth 5d952d3474 Also clean out directories with yarn clean 2024-04-10 00:00:30 +02:00
Candid Dauth 89071f96c5 Upgrade dependencies 2024-04-09 21:12:34 +02:00
Candid Dauth 39e02dce13 Fix broken translation strings 2024-04-09 18:54:56 +02:00
Candid Dauth 92b5bfdb83 Transmit language to Nominatim 2024-04-09 18:47:32 +02:00
Candid Dauth f2816f4269 Add last missing translations 2024-04-09 17:50:51 +02:00
Candid Dauth f53990bce5 Restructure POI categories, add a few types of "all" (#261) 2024-04-09 16:12:08 +02:00
Candid Dauth 82cfd0f9e4 Make toasts reactive 2024-04-09 15:54:56 +02:00
Candid Dauth fdc541bc92 Add more translations 2024-04-09 14:24:51 +02:00
Candid Dauth b8d7d597df Correct filter spelling mistakes (#242) 2024-04-09 12:33:19 +02:00
Candid Dauth 51a79c7a3b Add more translations 2024-04-09 12:31:07 +02:00
Candid Dauth b43d281134 Fix revalidation on rerender 2024-04-09 12:12:00 +02:00
Candid Dauth 77bdaa9a3a Add more translations 2024-04-09 12:11:53 +02:00
Candid Dauth 56ec7bf7dc Add more translations 2024-04-08 18:19:24 +02:00
Candid Dauth 56090599af Fix failing test 2024-04-08 01:13:29 +02:00
Candid Dauth 51f072524f Translate and fix heightgraph 2024-04-08 01:12:13 +02:00
103 zmienionych plików z 3494 dodań i 1833 usunięć

Wyświetl plik

@ -1,7 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
ignorePatterns: ["**/dist/*", "**/out/*", "**/out.*/*"],
ignorePatterns: ["**/dist/*", "**/out/*", "**/out.*/*", "**/vite.config.ts.timestamp-*.mjs"],
parserOptions: {
parser: "@typescript-eslint/parser",
project: ["*/tsconfig.json", "*/tsconfig.node.json"],

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-client",
"version": "4.0.0",
"version": "4.1.0",
"description": "A library that acts as a client to FacilMap and makes it possible to retrieve and modify objects on a collaborative map.",
"keywords": [
"maps",
@ -27,20 +27,20 @@
],
"scripts": {
"build": "vite build",
"clean": "rimraf dist",
"clean": "rimraf dist out out.node",
"dev-server": "vite",
"check-types": "tsc -b --emitDeclarationOnly"
},
"dependencies": {
"facilmap-types": "workspace:^",
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.4"
"socket.io-client": "^4.7.5"
},
"devDependencies": {
"@types/geojson": "^7946.0.14",
"rimraf": "^5.0.5",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-plugin-dts": "^3.7.3"
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-dts": "^3.8.1"
}
}

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-docs",
"version": "4.0.0",
"version": "4.1.0",
"description": "Documentation for FacilMap.",
"author": "Candid Dauth <cdauth@cdauth.eu>",
"repository": {

Wyświetl plik

@ -14,9 +14,12 @@ map (unless the `interactive` parameter is `false`).
You can control the display of different components by using the following query parameters:
* `toolbox`: Show the toolbox (default: `true`)
* `search`: Show the search bar (default: `true`)
* `search`: Show the search box (default: `true`)
* `route`: Show the route tab in the search box (default: `true`)
* `pois`: Show the POIs tab in the search box (default: `true`)
* `autofocus`: Autofocus the search field (default: `false`)
* `legend`: Show the legend if available (default: `true`)
* `locate`: Show the locate control to zoom to the users location (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.

Wyświetl plik

@ -8,18 +8,18 @@ The config of the FacilMap server can be set either by using environment variabl
| `APP_NAME` | | | If specified, will replace “FacilMap” as the name of the app throughout the UI. |
| `TRUST_PROXY` | | | Whether to trust the X-Forwarded-* headers. Can be `true` or a comma-separated list of IP subnets (see the [express documentation](https://expressjs.com/en/guide/behind-proxies.html)). Currently only used to calculate the base URL for the `opensearch.xml` file. |
| `BASE_URL` | | | If `TRUST_PROXY` does not work for your particular setup, you can manually specify the base URL where FacilMap can be publicly reached here. |
| `HOST` | | | The ip address to listen on (leave empty to listen on all addresses) |
| `PORT` | | `8080` | The port to listen on. |
| `DB_TYPE` | | `mysql` | The type of database. Either `mysql`, `postgres`, `mariadb`, `sqlite`, or `mssql`. |
| `DB_HOST` | | `localhost` | The host name of the database server. |
| `DB_PORT` | | | The port of the database server (optional). |
| `DB_NAME` | | `facilmap` | The name of the database. |
| `DB_USER` | | `facilmap` | The username to connect to the database with. |
| `DB_PASSWORD` | | `facilmap` | The password to connect to the database with. |
| `ORS_TOKEN` | * | | [OpenRouteService API key](https://openrouteservice.org/). |
| `MAPBOX_TOKEN` | * | | [Mapbox API key](https://www.mapbox.com/signup/). |
| `MAXMIND_USER_ID` | | | [MaxMind user ID](https://www.maxmind.com/en/geolite2/signup). |
| `MAXMIND_LICENSE_KEY` | | | MaxMind license key. |
| `HOST` | | | The ip address to listen on (leave empty to listen on all addresses) |
| `PORT` | | `8080` | The port to listen on. |
| `DB_TYPE` | | `mysql` | The type of database. Either `mysql`, `postgres`, `mariadb`, `sqlite`, or `mssql`. |
| `DB_HOST` | | `localhost` | The host name of the database server. |
| `DB_PORT` | | | The port of the database server (optional). |
| `DB_NAME` | | `facilmap` | The name of the database. |
| `DB_USER` | | `facilmap` | The username to connect to the database with. |
| `DB_PASSWORD` | | `facilmap` | The password to connect to the database with. |
| `ORS_TOKEN` | | | [OpenRouteService API key](https://openrouteservice.org/). If not specified, advanced routing settings will not be shown. |
| `MAPBOX_TOKEN` | | | [Mapbox API key](https://www.mapbox.com/signup/). If neither this nor `ORS_TOKEN` are specified, the routing tab and any routing options will be hidden. |
| `MAXMIND_USER_ID` | | | [MaxMind user ID](https://www.maxmind.com/en/geolite2/signup). |
| `MAXMIND_LICENSE_KEY` | | | MaxMind license key. |
| `LIMA_LABS_TOKEN` | | | [Lima Labs](https://maps.lima-labs.com/) API key |
| `HIDE_COMMERCIAL_MAP_LINKS` | | | Set to `1` to hide the links to Google/Bing Maps in the “Map style” menu. |
| `CUSTOM_CSS_FILE` | | | The path of a CSS file that should be included ([see more details below](#custom-css-file)). |

Wyświetl plik

@ -10,93 +10,84 @@ FacilMap needs a database supported by [Sequelize](https://sequelize.org/master/
## docker-compose
To run FacilMap with MySQL using [docker-compose](https://docs.docker.com/compose/), here is an example `docker-compose.yml`:
To run FacilMap with MariaDB using [docker-compose](https://docs.docker.com/compose/), here is an example `docker-compose.yml`:
```yaml
version: "2"
services:
facilmap:
image: facilmap/facilmap
ports:
- 8080
links:
- mysql
depends_on:
mysql:
condition: service_healthy
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: mysql
DB_HOST: db
DB_NAME: facilmap
DB_USER: facilmap
DB_PASSWORD: password
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
MAXMIND_LICENSE_KEY:
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
restart: unless-stopped
mysql:
image: mariadb
environment:
MYSQL_DATABASE: facilmap
MYSQL_USER: facilmap
MYSQL_PASSWORD: password
MYSQL_RANDOM_ROOT_PASSWORD: "true"
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
restart: unless-stopped
facilmap:
image: facilmap/facilmap
ports:
- 8080:8080
links:
- mariadb
depends_on:
mariadb:
condition: service_healthy
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: mysql
DB_HOST: mariadb
DB_NAME: facilmap
DB_USER: facilmap
DB_PASSWORD: password
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
MAXMIND_LICENSE_KEY:
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
restart: unless-stopped
mariadb:
image: mariadb
environment:
MYSQL_DATABASE: facilmap
MYSQL_USER: facilmap
MYSQL_PASSWORD: password
MYSQL_RANDOM_ROOT_PASSWORD: "true"
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: healthcheck.sh --su-mysql --connect --innodb_initialized
restart: unless-stopped
```
Here is an example with Postgres:
```yaml
version: "2"
services:
facilmap:
image: facilmap/facilmap
ports:
- 8080
links:
- postgres
depends_on:
postgres:
condition: service_healthy
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: postgres
DB_HOST: db
DB_NAME: facilmap
DB_USER: facilmap
DB_PASSWORD: password
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
MAXMIND_LICENSE_KEY:
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
restart: unless-stopped
postgres:
image: postgis/postgis
environment:
POSTGRES_USER: facilmap
POSTGRES_PASSWORD: password
POSTGRES_DB: facilmap
healthcheck:
test: pg_isready -d $$POSTGRES_DB
restart: unless-stopped
facilmap:
image: facilmap/facilmap
ports:
- 8080:8080
links:
- postgres
depends_on:
postgres:
condition: service_healthy
environment:
USER_AGENT: My FacilMap (https://facilmap.example.org/, facilmap@example.org)
TRUST_PROXY: "true"
DB_TYPE: postgres
DB_HOST: db
DB_NAME: facilmap
DB_USER: facilmap
DB_PASSWORD: password
ORS_TOKEN: # Get an API key on https://go.openrouteservice.org/ (needed for routing)
MAPBOX_TOKEN: # Get an API key on https://www.mapbox.com/signup/ (needed for routing)
MAXMIND_USER_ID: # Sign up here https://www.maxmind.com/en/geolite2/signup (needed for geoip lookup to show initial map state)
MAXMIND_LICENSE_KEY:
LIMA_LABS_TOKEN: # Get an API key on https://maps.lima-labs.com/ (optional, needed for double-resolution tiles)
restart: unless-stopped
postgres:
image: postgis/postgis
environment:
POSTGRES_USER: facilmap
POSTGRES_PASSWORD: password
POSTGRES_DB: facilmap
healthcheck:
test: pg_isready -d $$POSTGRES_DB
restart: unless-stopped
```
To start FacilMap, run `docker-compose up -d` in the directory of the `docker-compose.yml` file. To upgrade FacilMap, run `docker-compose pull` and then restart it by running `docker-compose up -d`.
## docker create
To manually create the necessary docker containers, use these commands:
```bash
docker create --name=facilmap_db -e MYSQL_DATABASE=facilmap -e MYSQL_USER=facilmap -e MYSQL_PASSWORD=password -e MYSQL_RANDOM_ROOT_PASSWORD=true --restart=unless-stopped mariadb --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
docker create --link=facilmap_db -p 8080 --name=facilmap -e "USER_AGENT=My FacilMap (https://facilmap.example.org/, facilmap@example.org)" -e TRUST_PROXY=true -e DB_TYPE=mysql -e DB_HOST=facilmap_db -e DB_NAME=facilmap -e DB_USER=facilmap -e DB_PASSWORD=facilmap -e ORS_TOKEN= -e MAPBOX_TOKEN= -e MAXMIND_USER_ID= -e MAXMIND_LICENSE_KEY= -e LIMA_LABS_TOKEN= --restart=unless-stopped facilmap/facilmap
```
Note that this exposes FacilMap through unencrypted HTTP on port 8080. In a production setup, FacilMap should be served by a reverse proxy that provides HTTPS. Usually, the `ports` directive can be removed then.

Wyświetl plik

@ -16,7 +16,7 @@ Field values are available as `data`. By default, markers and objects have only
Be aware that in filter expression, comparison is case sensitive. So the above expression would not match an object whose status is “done”. For case-insensitive comparison, compare the lower-case values: `lower(data.Status) == "done"`.
Checkbox field values are internally represented by the values `0` (unchecked) and `1` (checked). For example, to how only values where the checkbox field “Confirmed” is checked, use `data.Confirmed == 1`.
Checkbox field values are internally represented by the values `0` (unchecked) and `1` (checked). For example, to show only values where the checkbox field “Confirmed” is checked, use `data.Confirmed == 1`.
The regular expression operator `~=` allows for more advanced text matching. For example, to show all objects whose name contains “Untitled”, use `lower(name) ~= "untitled"`. Regular expressions allow to define very complex criteria what the text should look like. There are plenty of tutorials online how to use regular expressions, for example [RegexOne](https://regexone.com/).

Wyświetl plik

@ -23,9 +23,7 @@ To create a link pointing to a particular map view on FacilMap, click on “Tool
The following options are available:
* “Include current map view” defines whether the generated link should point to the current view (map position, active map object, ...) of the map. If this option is disabled, the link will point to the default view ([saved default view](../views/#default-view) for [collaborative maps](../collaborative/), otherwise the rough geographical location of the user).
* “Show toolbox”: If this is disabled, the [toolbox](../ui/#toolbox) will be hidden when opening the link.
* “Show search box”: If this is disabled, the [search box](../ui/#search-box) will be hidden when opening the link.
* “Show legend” (only for [collaborative maps](../collaborative/) with a [legend](../legend/)): If this is disabled, the [legend](../legend/) will be hidden when opening the link.
* “Show controls”: Uncheck different UI elements here to hide them when opening the link. This is particularly useful when embedding the map into a web page.
* “Link type” (only for [collaborative maps](../collaborative/)): The [permissions](../collaborative/#urls) that users will have when opening the link.
Click on “Copy” to copy the link to the clipboard.

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-frontend",
"version": "4.0.0",
"version": "4.1.0",
"description": "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.",
"keywords": [
"maps",
@ -32,7 +32,7 @@
"build": "yarn build:lib && yarn build:app",
"build:lib": "vite --config vite-lib.config.ts build",
"build:app": "NODE_OPTIONS='--import tsx' vite build",
"clean": "rimraf dist",
"clean": "rimraf dist out out.node",
"dev-server": "NODE_OPTIONS='--import tsx' vite",
"test": "NODE_OPTIONS='--import tsx' vitest run",
"test-watch": "NODE_OPTIONS='--import tsx' vitest",
@ -52,7 +52,7 @@
"facilmap-utils": "workspace:^",
"file-saver": "^2.0.5",
"hammerjs": "^2.0.8",
"i18next": "^23.10.1",
"i18next": "^23.11.1",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
@ -71,9 +71,9 @@
"popper-max-size-modifier": "^0.2.0",
"qrcode.vue": "^3.4.1",
"tablesorter": "^2.31.3",
"vite": "^5.1.5",
"vite-plugin-css-injected-by-js": "^3.4.0",
"vite-plugin-dts": "^3.7.3",
"vite": "^5.2.8",
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-dts": "^3.8.1",
"vue": "^3.4.21",
"vuedraggable": "^4.1.0",
"zod": "^3.22.4"
@ -85,19 +85,19 @@
"@types/hammerjs": "^2.0.45",
"@types/jquery": "^3.5.29",
"@types/js-cookie": "^3.0.6",
"@types/leaflet": "^1.9.8",
"@types/leaflet": "^1.9.9",
"@types/leaflet-mouse-position": "^1.2.4",
"@types/leaflet.locatecontrol": "^0.74.4",
"@types/lodash-es": "^4.17.12",
"@types/pluralize": "^0.0.33",
"happy-dom": "^13.6.2",
"happy-dom": "^14.7.1",
"rimraf": "^5.0.5",
"sass": "^1.71.1",
"sass": "^1.74.1",
"svgo": "^3.2.0",
"tsx": "^4.7.1",
"typescript": "^5.4.2",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.1",
"vue-tsc": "^2.0.5"
"tsx": "^4.7.2",
"typescript": "^5.4.4",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.4.0",
"vue-tsc": "^2.0.11"
}
}

Wyświetl plik

@ -21,6 +21,19 @@
"programs-libraries": "Programme/Bibliotheken",
"icons": "Symbole"
},
"add": {
"hidden-markers-added-title_one": "Marker erstellt",
"hidden-markers-added-title_other": "{{count}} Marker erstellt",
"hidden-markers-added-message_one": "Der Marker wurde erstellt, aber der aktive Filter verhindert, dass er angezeigt wird.",
"hidden-markers-added-message_other": "{{count}} Marker wurden erstellt, aber der aktive Filter verhindert, dass sie angezeigt werden.",
"hidden-lines-added-title_one": "Linie erstellt",
"hidden-lines-added-title_other": "{{count}} Linien erstellt",
"hidden-lines-added-message_one": "Die Linie wurde erstellt, aber der aktive Filter verhindert, dass sie angezeigt wird.",
"hidden-lines-added-message_other": "{{count}} Linien wurden erstellt, aber der aktive Filter verhindert, dass sie angezeigt werden.",
"hidden-objects-added-title": "{{count}} Objekte erstellt",
"hidden-objects-added-message": "{{count}} Objekte wurden erstellt, aber der aktive Filter verhindert, dass sie angezeigt werden",
"hidden-objects-added-message-some": "{{count}} Objekte wurden erstellt, aber der aktive Filter verhindert, dass manche von ihnen angezeigt werden."
},
"add-to-map-dropdown": {
"fallback-label": "Zur Karte hinzufügen",
"add-error": "Fehler beim Hinzufügen der Objekte",
@ -78,6 +91,24 @@
"untyped-lines_one": "Linie/Polygon ohne Typ ({{count}})",
"untyped-lines_other": "Linien/Polygone ohne Typ ({{count}})"
},
"draw": {
"add-marker-error": "Fehler beim Erstellen des Markers",
"add-marker-title": "{{typeName}} erstellen",
"add-marker-message": "Klicken Sie auf die Karte, um den Marker dort zu erstellen.",
"add-marker-current": "Am Standort erstellen",
"add-marker-cancel": "Abbrechen",
"move-marker-error": "Fehler beim Bewegen des Markers",
"move-marker-title": "Marker bewegen",
"move-marker-message": "Ziehen Sie den Marker herum, um seine Position zu ändern.",
"move-marker-save": "Speichern",
"move-marker-current": "Zum Standort bewegen",
"move-marker-cancel": "Abbrechen",
"add-line-title": "{{typeName}} erstellen",
"add-line-message": "Klicken Sie mehrfach auf die Karte, um eine Linie zu malen. Klicken Sie dann auf „Speichern“, um die Linie zu erstellen.",
"add-line-finish": "Speichern",
"add-line-cancel": "Abbrechen",
"add-line-error": "Fehler beim Erstellen der Linie"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Anwenden",
@ -120,6 +151,26 @@
"functions-description": "Mathematische Funktionen ({{abs}}: Betrag, {{log}}: Natürlicher Logarithmus, {{sqrt}}: Quadratwurzel)",
"min-max-description": "Kleinster/größter Wert"
},
"edit-line-dialog": {
"save-error": "Fehler beim Speichern der Linie",
"title": "Linie bearbeiten",
"name": "Name",
"routing-mode": "Routenmodus",
"colour": "Farbe",
"width": "Dicke",
"stroke": "Kontur",
"change-type": "Objekttyp wechseln"
},
"edit-marker-dialog": {
"save-error": "Fehler beim Speichern des Markers",
"title": "Marker bearbeiten",
"name": "Name",
"colour": "Farbe",
"size": "Größe",
"icon": "Symbol",
"shape": "Form",
"change-type": "Objekttyp wechseln"
},
"edit-type-dialog": {
"delete-field-title": "Feld löschen",
"delete-field-message": "Wollen Sie das Feld „{{fieldName}}“ wirklich löschen?",
@ -189,6 +240,145 @@
"option-reorder": "Umordnen",
"option-add": "Erstellen"
},
"elevation-stats": {
"ascent-alt": "Aufstieg",
"descent-alt": "Abstieg",
"show-tooltip": "Höhenstatistik einblenden",
"show-alt": "Höhenstatistik",
"total-ascent": "Gesamtaufstieg",
"total-descent": "Gesamtabstieg"
},
"export-dialog": {
"format-option-gpx": "GPX",
"format-option-geojson": "GeoJSON",
"format-option-html": "HTML",
"format-option-csv": "CSV",
"column-name": "Name",
"column-position": "Position",
"column-distance": "Länge",
"column-time": "Reisedauer",
"route-type-track": "Track",
"route-type-track-zip": "Track, eine Datei pro Linie (ZIP-Datei)",
"route-type-route": "Route",
"open-file": "Datei öffnen",
"download-file": "Datei herunterladen",
"generate-link": "Link generieren",
"copy-to-clipboard": "In die Zwischenablage kopieren",
"select-type-error": "Bitte wählen Sie einen Objekttyp aus.",
"export-ok": "Exportieren",
"copy-ok": "Kopieren",
"export-copied-title": "{{format}}-Export kopiert",
"export-copied-message": "Der {{format}}-Export wurde in die Zwischenablage kopiert.",
"title": "Kollaborative Karte exportieren",
"introduction": "Exportieren Sie Ihre Karte hier, um sie in eine andere Anwendung, auf ein anderes Gerät oder in eine andere kollaborative Karte zu übertragen.",
"format": "Format",
"gpx-explanation": "{{gpx}}-Dateien können benutzt werden, um Ihre Karte in Navigations- und Routenplanungsprogramme und -Geräte zu übertragen, zum Beispiel OsmAnd oder Garmin. Sie enthalten alle Marker und Linien mit deren Namen und Beschreibung, aber nicht deren Stilattributen (mit Ausnahme einiger Basisattribute für OsmAnd).",
"gpx-explanation-interpolation-gpx": "GPX",
"geojson-explanation": "{{geojson}}-Dateien können benutzt werden, um vollständige Backups oder Kopien Ihrer Karte anzufertigen. Sie enthalten die vollständigen Daten der Karte, inklusive der Karteneinstellungen, Ansichten, Objekttypen, Marker und Linien mit all deren Attributen. Um ein GeoJSON-Backup wiederherzustellen oder um eine Kopie Ihrer Karte anzulegen, importieren Sie die Datei einfach wieder in FacilMap.",
"geojson-explanation-interpolation-geojson": "GeoJSON",
"html-explanation": "{{html}}-Dateien können in jedem Webbrowser geöffnet werden. Der HTML-Export enthält eine Tabelle mit den Datenattributen aller Marker und Linien. Die Tabelle kann über die Zwischenablage auch in eine Tabellenkalkulation kopiert werden.",
"html-explanation-interpolation-html": "HTML",
"csv-explanation": "{{csv}}-Dateien können in den meisten Tabellenkalkulationen geöffnet werden und enhalten die Datenattribute der Marker/Linien eines einzelnen Objekttyps.",
"csv-explanation-interpolation-csv": "CSV",
"export-method": "Export-Methode",
"route-type": "Routenformat",
"track-explanation": "{{track}} exportiert den Verlauf Ihrer Linien, wie diese auf der Karte gespeichert sind.",
"track-explanation-interpolation-track": "Track",
"track-zip-explanation": "{{trackZip}} generiert eine ZIP-Datei, die eine GPX-Datei für alle Marker und eine GPX-Datei pro Linie enthält. Das funktioniert besser mit Apps wie OsmAnd zusammen, die nur eine Linie pro Datei unterstützen.",
"track-zip-explanation-interpolation-trackZip": "Track, eine Datei pro Linie (ZIP-Datei)",
"route-explanation": "{{route}} exportiert nur die Wegpunkte Ihrer Linien. Das importierende Navigationsprogramm/-gerät muss die Route basierend auf seinem eigenen Kartenmaterial berechnen.",
"route-explanation-interpolation-route": "Route",
"type": "Objekttyp",
"include-columns": "Spalten importieren",
"apply-filter": "Filter anwenden",
"apply-filter-label": "Nur Objekte exportieren, die unter dem aktuellen Filter sichtbar sind"
},
"export-dropdown": {
"export-error": "Fehler beim Exportieren der Route",
"button-label": "Exportieren",
"gpx-track-tooltip": "GPX-Dateien können mit den meisten Navigationsprogrammen geöffnet werden. Im Track-Modus wird die berechnete Route in der exportierten Datei gespeichert.",
"gpx-track-label": "Als GPX-Track exportieren",
"gpx-route-tooltip": "GPX-Dateien können mit den meisten Navigationsprogrammen geöffnet werden. Im Route-Modus werden nur die Wegpunkte der Route in der exportierten Datei gespeichert; die importierende Software muss die Route selbst berechnen.",
"gpx-route-label": "Als GPX-Route exportieren"
},
"file-results": {
"import-view-error": "Fehler beim Importieren der Ansicht",
"import-type-error": "Fehler beim Importieren des Objekttyps",
"views": "Ansichten",
"view": "Ansicht",
"add-view-tooltip": "Diese Ansicht zur Karte hinzufügen",
"add-view-alt": "Hinzufügen",
"markers-lines": "Marker/Linien",
"types": "Objekttypen",
"type": "Objekttyp",
"add-type-tooltip": "Diesen Objekttyp zur Karte hinzufügen",
"add-type-alt": "Hinzufügen"
},
"files": {
"invalid-xml-error": "Ungültiges XML: {{textContent}}"
},
"heightgraph": {
"distance": "Länge",
"elevation": "Höhe über NN",
"segment-length": "Abschnittslänge",
"type": "Typ",
"legend": "Legende",
"steepness": "Steilheit",
"waytypes": "Straßenart",
"surface": "Oberfläche",
"suitablity": "Nutzbarkeit",
"green": "Grün",
"noise": "Lärm",
"tollways": "Maut",
"avgspeed": "Durchschnittsgeschwindigkeit",
"traildifficulty": "Schwierigkeitsgrad",
"roadaccessrestrictions": "Zufahrtsbeschränkungen",
"label-with-total": "{{label}} ({{total}})",
"unknown": "Unbekannt",
"waytype-other": "Andere",
"waytype-state-road": "Bundesstraße",
"waytype-road": "Landstraße",
"waytype-street": "Wohnstraße",
"waytype-path": "Pfad",
"waytype-track": "Feldweg",
"waytype-cycleway": "Radweg",
"waytype-footway": "Fußweg",
"waytype-steps": "Treppe",
"waytype-ferry": "Fähre",
"waytype-construction": "Baustelle",
"surface-other": "Andere",
"surface-paved": "Asphaltiert",
"surface-unpaved": "Unasphaltiert",
"surface-asphalt": "Asphalt",
"surface-contrete": "Beton",
"surface-cobblestone": "Kopfsteinpflaster",
"surface-metal": "Metall",
"surface-wood": "Holz",
"surface-compacted-gravel": "Schotter (verdichtet)",
"surface-fine-gravel": "Schotter (fein)",
"surface-gravel": "Schotter",
"surface-dirt": "Staub",
"surface-ground": "Erde",
"surface-ice": "Eis",
"surface-paving-stones": "Pflastersteine",
"surface-sand": "Sand",
"surface-woodchips": "Holzspäne",
"surface-grass": "Gras",
"surface-grass-paver": "Rasengitterstein",
"tollway-no": "Keine Mautstraße",
"tollway-yes": "Mautstraße",
"traildifficulty-unknown": "Keine SAC-Klassifikation",
"access-yes": "Erlaubt",
"access-no": "Verboten",
"access-customers": "Kunden",
"access-destination": "Anlieger",
"access-delivery": "Lieferdienste",
"access-private": "Privat",
"access-permissive": "Gestattet"
},
"help-popover": {
"show-alt": "Erläuterung einblenden"
},
"history-dialog": {
"loading-error": "Fehler beim Laden der Versionsgeschichte",
"revert-error": "Fehler beim Rückgängigmachen der Änderung",
@ -262,6 +452,21 @@
"revert-delete-type-message-unnamed": "Wollen Sie die alte Version dieses Objekttyps wirklich wiederherstellen?",
"revert-delete-button": "Wiederherstellen"
},
"hybrid-popover": {
"close-label": "Schließen",
"ok-label": "OK"
},
"import-tab": {
"parse-error-title": "Parsing-Fehler",
"parse-error-message_one": "The ausgewählte Datei konnte nicht dekodiert werden.",
"parse-error-message_other": "Die ausgewählten Dateien konnten nicht dekodiert werden.",
"no-geometries-error-title": "Keine Objekte",
"no-geometries-error-message_one": "Die ausgewählte Datei enthält keine geometrischen Objekte.",
"no-geometries-error-message_other": "Die ausgewählten Dateien enthalten keine geometrischen Objekte.",
"partial-parse-error-title": "Parsing-Fehler",
"partial-parse-error-message": "Manche der ausgewählten Dateien konnten nicht dekodiert werden.",
"read-error-title": "Lesefehler"
},
"leaflet-map": {
"open-full-size": "{{appName}} als ganze Seite öffnen",
"loading": "Wird geladen…"
@ -293,9 +498,48 @@
"ascent-descent": "Aufstieg/Abstieg",
"zoom-to-object-label": "Zur Linie zoomen",
"edit-data": "Bearbeiten",
"actions": "Aktionen",
"edit-waypoints": "Bewegen",
"delete": "Löschen"
},
"manage-bookmarks-dialog": {
"title": "Favoriten verwalten",
"introduction": "Kollaborative Karten können als Favoriten abgespeichert werden, um sie schnell öffnen zu können, ohne den Link eingeben zu müssen. Ihre Favoriten werden in Ihrem Browser abgespeichert, andere Benutzer können darauf nicht zugreifen.",
"map-id": "Karten-ID",
"name": "Name",
"delete": "Entfernen",
"reorder-alt": "Umordnen",
"bookmark-current": "Aktuelle Karte hinzufügen"
},
"manage-types-dialog": {
"delete-title": "Objekttyp löschen",
"delete-message": "Wollen Sie den Objekttyp „{{typeName}}“ wirklich löschen?",
"delete-ok": "Löschen",
"delete-error": "Fehler beim Löschen des Objekttyps „{{typeName}}“",
"title": "Objekttypen verwalten",
"name": "Name",
"type": "Art",
"edit": "Bearbeiten",
"type-marker": "Marker",
"type-line": "Linien",
"edit-button": "Bearbeiten",
"delete-button": "Löschen",
"reorder-alt": "Umordnen",
"create": "Erstellen",
"marker-type": "Marker-Typ",
"line-type": "Linien-Typ"
},
"manage-views-dialog": {
"default-view-error": "Fehler beim Setzen der Standardansicht",
"delete-view-title": "Ansicht löschen",
"delete-view-message": "Wollen Sie die Ansicht „{{viewName}}“ wirklich löschen?",
"delete-view-ok": "Löschen",
"delete-view-error": "Fehler beim Löschen der Ansicht „{{viewName}}“",
"title": "Ansichten verwalten",
"make-default": "Zur Standardansicht machen",
"delete": "Löschen",
"reorder-alt": "Umsortieren"
},
"marker-info": {
"delete-marker-title": "Marker löschen",
"delete-marker-message": "Wollen Sie den Marker „{{name}}“ wirklich löschen?",
@ -304,6 +548,7 @@
"coordinates": "Koordinaten",
"zoom-to-object-label": "Zum Marker zoomen",
"edit-data": "Bearbeiten",
"actions": "Aktionen",
"move": "Bewegen",
"delete": "Löschen"
},
@ -326,6 +571,20 @@
"cancel": "Abbrechen",
"save": "Speichern"
},
"open-map-dialog": {
"find-pads-error": "Fehler bei der Suche nach öffentlichen Karten",
"map-id-format-error": "Bitte geben Sie eine gültige Karten-ID oder -URL ein.",
"map-not-found-error": "Es konnte keine Karte mit dieser ID gefunden werden.",
"title": "Kollaborative Karte öffnen",
"introduction": "Geben Sie hier den Link oder die ID einer existierenden kollaborativen Karte ein, um diese Karte zu öffnen.",
"open-map-by-id-button": "Öffnen",
"search-public-maps": "Öffentliche Karten suchen",
"search-alt": "Suchen",
"no-maps-found": "Es konnten keine Karten gefunden werden.",
"name": "Name",
"description": "Beschreibung",
"open-map-by-search-button": "Öffnen"
},
"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.",
@ -388,6 +647,85 @@
"delete-description": "Um die Karte zu löschen, tippen Sie {{code}} in das Feld und klicken Sie „Karte löschen“.",
"delete-code": "LÖSCHEN"
},
"pagination": {
"first-label": "Erste Seite",
"previous-label": "Vorige Seite",
"next-label": "Nächste Seite",
"last-label": "Letzte Seite"
},
"save-view-dialog": {
"save-view-error": "Fehler beim Speichern der Ansicht",
"empty": "—",
"title": "Aktuelle Ansicht speichern",
"name": "Name",
"top-left": "Oben links",
"bottom-right": "Unten rechts",
"base-layer": "Kartenstil (Basis)",
"overlays": "Kartenstil (Überlagerungen)",
"overlays-joiner": ", ",
"pois": "POIs",
"include-pois": "Aktivierte POIs speichern ({{pois}})",
"include-pois-interpolation-pois-joiner": ", ",
"filter": "Filter",
"include-current-filter": "Aktuellen Filter speichern ({{filter}})",
"default-view": "Standardansicht",
"make-default": "Zur Standardansicht machen"
},
"search-result-info": {
"coordinates": "Koordinaten",
"type": "Art",
"address": "Addresse",
"zoom-to-result-label": "Zum Ergebnis zoomen"
},
"shape-picker": {
"unknown-shape-error": "Unbekannte Form",
"no-shapes-error": "Es konnten keine Formen gefunden werden."
},
"share-dialog": {
"type-admin": "Admin-Link",
"type-write": "Schreib-Link",
"type-read": "Lese-Link",
"title": "Teilen",
"settings": "Einstellungen",
"include-view": "Aktuelle Ansicht einschließen (Mittelpunkt: {{centre}}; Zoomstufe: {{zoom}}; Kartenstil: {{layers}}{{conditionalPois}}{{conditionalSelection}}{{conditionalFilter}})",
"include-view-interpolation-layers-joiner": ", ",
"include-view-interpolation-conditionalPois": ", POIs: {{pois}}",
"include-view-interpolation-conditionalPois-interpolation-pois-joiner": ", ",
"include-view-interpolation-conditionalSelection": "; aktive(s) Objekt(e): {{description}}",
"include-view-interpolation-conditionalFilter": "; Filter: {{filter}}",
"show-controls": "Schaltflächen anzeigen",
"show-toolbox": "Menü",
"show-search-box": "Suchbereich",
"show-route": "Routen-Tab",
"show-pois": "POI-Tab",
"show-legend": "Legende",
"show-locate": "Standort",
"link-type": "Berechtigungen",
"share-link": "Link teilen",
"embed": "Karte einbetten",
"link-copied-title": "Link kopiert",
"link-copied-message": "Der Link zur Karte wurde in die Zwischenablage kopiert.",
"embed-copied-title": "Code kopiert",
"embed-copied-message": "Der Code zum Einbetten von {{appName}} wurde in die Zwischenablage kopiert.",
"embed-explanation": "Fügen Sie diesen HTML-Code auf einer Webseite ein, um {{appName}} einzubetten. {{learnMore}}",
"embed-explanation-interpolation-learnMore": "Mehr erfahren"
},
"stroke-picker": {
"solid": "Durchgezogen",
"dashed": "Gestrichelt",
"dotted": "Gepunktet"
},
"symbol-picker": {
"unknown-icon-error": "Unbekanntes Symbol",
"filter-placeholder": "Filtern",
"no-icons-found-error": "Es konnten keine Symbole gefunden werden."
},
"use-as-dropdown": {
"label": "Route",
"from": "Als Start verwenden",
"via": "Zwischenstopp hinzufügen",
"to": "Als Ziel verwenden"
},
"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.",
@ -398,6 +736,9 @@
"units-metric": "Metrisch",
"units-us": "US customary (Meilen und Füße)"
},
"utils": {
"required-error": "Dieses Feld muss ausgefüllt werden."
},
"route-form": {
"route-description-outer": "Route von {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
@ -490,7 +831,7 @@
},
"toolbox-collab-maps-dropdown": {
"label": "Kollaborative Karten",
"bookmark": "Karte „{{padName}}“ als Favoriten hinzufügen",
"bookmark": "Karte „{{padName}}“ zu Favoriten hinzufügen",
"manage-bookmarks": "Favoriten verwalten",
"create-map": "Neue Karte erstellen",
"open-map": "Existierende Karte öffnen",

Wyświetl plik

@ -21,6 +21,19 @@
"programs-libraries": "Programs/libraries",
"icons": "Icons"
},
"add": {
"hidden-markers-added-title_one": "Marker added",
"hidden-markers-added-title_other": "{{count}} markers added",
"hidden-markers-added-message_one": "The marker has been added successfully, but the active filter is preventing it from being shown.",
"hidden-markers-added-message_other": "{{count}} markers have been added successfully, but the active filter is preventing them from being shown.",
"hidden-lines-added-title_one": "Line added",
"hidden-lines-added-title_other": "{{count}} lines added",
"hidden-lines-added-message_one": "The line has been added successfully, but the active filter is preventing it from being shown.",
"hidden-lines-added-message_other": "{{count}} lines have been added successfully, but the active filter is preventing them from being shown.",
"hidden-objects-added-title": "{{count}} objects added",
"hidden-objects-added-message": "{{count}} objects have been added successfully, but the active filter is preventing them from being shown.",
"hidden-objects-added-message-some": "{{count}} objects have been added successfully, but the active filter is preventing some of them from being shown."
},
"add-to-map-dropdown": {
"fallback-label": "Add to map",
"add-error": "Error adding to map",
@ -80,6 +93,24 @@
"untyped-lines_one": "Untyped line/polygon ({{count}})",
"untyped-lines_other": "Untyped lines/polygons ({{count}})"
},
"draw": {
"add-marker-error": "Error adding marker",
"add-marker-title": "Add {{typeName}}",
"add-marker-message": "Please click on the map to add a marker.",
"add-marker-current": "Add at current location",
"add-marker-cancel": "Cancel",
"move-marker-error": "Error moving marker",
"move-marker-title": "Drag marker",
"move-marker-message": "Drag the marker to reposition it.",
"move-marker-save": "Save",
"move-marker-current": "Move to current location",
"move-marker-cancel": "Cancel",
"add-line-title": "Add {{typeName}}",
"add-line-message": "Click on the map multiple times to draw a line. Then click “Finish” to save it.",
"add-line-finish": "Finish",
"add-line-cancel": "Cancel",
"add-line-error": "Error adding line"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Apply",
@ -122,6 +153,26 @@
"functions-description": "Mathematical functions",
"min-max-description": "Smallest/highest value"
},
"edit-line-dialog": {
"save-error": "Error saving line",
"title": "Edit Line",
"name": "Name",
"routing-mode": "Routing mode",
"colour": "Colour",
"width": "Width",
"stroke": "Stroke",
"change-type": "Change type"
},
"edit-marker-dialog": {
"save-error": "Error saving marker",
"title": "Edit Marker",
"name": "Name",
"colour": "Colour",
"size": "Size",
"icon": "Icon",
"shape": "Shape",
"change-type": "Change type"
},
"edit-type-dialog": {
"delete-field-title": "Delete field",
"delete-field-message": "Do you really want to delete the field “{{fieldName}}”?",
@ -191,6 +242,145 @@
"option-reorder": "Reorder",
"option-add": "Add"
},
"elevation-stats": {
"ascent-alt": "Ascent",
"descent-alt": "Descent",
"show-tooltip": "Show elevation statistics",
"show-alt": "Show stats",
"total-ascent": "Total ascent",
"total-descent": "Total descent"
},
"export-dialog": {
"format-option-gpx": "GPX",
"format-option-geojson": "GeoJSON",
"format-option-html": "HTML",
"format-option-csv": "CSV",
"column-name": "Name",
"column-position": "Position",
"column-distance": "Distance",
"column-time": "Line time",
"route-type-track": "Track points",
"route-type-track-zip": "Track points, one file per line (ZIP file)",
"route-type-route": "Route points",
"open-file": "Open file",
"download-file": "Download file",
"generate-link": "Generate link",
"copy-to-clipboard": "Copy to clipboard",
"select-type-error": "Please select a type.",
"export-ok": "Export",
"copy-ok": "Copy",
"export-copied-title": "{{format}} export copied",
"export-copied-message": "The {{format}} export was copied to the clipboard.",
"title": "Export collaborative map",
"introduction": "Export your map here to transfer it to another application, another device or another collaborative map.",
"format": "Format",
"gpx-explanation": "{{gpx}} files can be used to transfer your map data into navigation and route planning software and devices, such as OsmAnd or Garmin. They contain your markers and lines with their names and descriptions, but not their style attributes (with the exception of some basic attributes supported by OsmAnd).",
"gpx-explanation-interpolation-gpx": "GPX",
"geojson-explanation": "{{geojson}} files can be used to create complete backups or copies of your map. They contain the complete data of your map, including the map settings, views, types, markers and lines along with all their data attributes. To restore a GeoJSON backup or to create a copy of your map, simply import the file into FacilMap again.",
"geojson-explanation-interpolation-geojson": "GeoJSON",
"html-explanation": "{{html}} files can be opened by any web browser. Exporting a map to HTML will render a table with only the data attributes of all markers and lines. This table can also be copy&pasted into a spreadsheet application for further processing.",
"html-explanation-interpolation-html": "HTML",
"csv-explanation": "{{csv}} files can be imported into most spreadsheet applications and only contain the data attributes of the objects of one type of marker or line.",
"csv-explanation-interpolation-csv": "CSV",
"export-method": "Export method",
"route-type": "Route type",
"track-explanation": "{{track}} will export your lines exactly as they are on your map.",
"track-explanation-interpolation-track": "Track points",
"track-zip-explanation": "{{trackZip}} will create a ZIP file with one GPX file for all markers and one GPX file for each line. This works better with apps such as OsmAnd that only support one line style per file.",
"track-zip-explanation-interpolation-trackZip": "Track points, one file per line (ZIP file)",
"route-explanation": "{{route}} will export only the from/via/to route points of your lines, and your navigation software/device will have to calculate the route using its own map data and algorithm.",
"route-explanation-interpolation-route": "Route points",
"type": "Type",
"include-columns": "Include columns",
"apply-filter": "Apply filter",
"apply-filter-label": "Only include objects visible under current filter"
},
"export-dropdown": {
"export-error": "Error exporting route",
"button-label": "Export",
"gpx-track-tooltip": "GPX files can be opened with most navigation software. In track mode, the calculated route is saved in the file.",
"gpx-track-label": "Export as GPX track",
"gpx-route-tooltip": "GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to calculate the route.",
"gpx-route-label": "Export as GPX route"
},
"file-results": {
"import-view-error": "Error importing view",
"import-type-error": "Error importing type",
"views": "Views",
"view": "View",
"add-view-tooltip": "Add this view to the map",
"add-view-alt": "Add",
"markers-lines": "Markers/Lines",
"types": "Types",
"type": "Type",
"add-type-tooltip": "Add this type to the map",
"add-type-alt": "Add"
},
"files": {
"invalid-xml-error": "Invalid XML: {{textContent}}"
},
"heightgraph": {
"distance": "Distance",
"elevation": "Elevation",
"segment-length": "Segment length",
"type": "Type",
"legend": "Legend",
"steepness": "Steepness",
"waytypes": "Road types",
"surface": "Surface",
"suitablity": "Suitability",
"green": "Green",
"noise": "Noise",
"tollways": "Tollways",
"avgspeed": "Average speed",
"traildifficulty": "Trail difficulty",
"roadaccessrestrictions": "Access restrictions",
"label-with-total": "{{label}} ({{total}})",
"unknown": "Unknown",
"waytype-other": "Other",
"waytype-state-road": "State road",
"waytype-road": "Road",
"waytype-street": "Street",
"waytype-path": "Path",
"waytype-track": "Track",
"waytype-cycleway": "Cycleway",
"waytype-footway": "Footway",
"waytype-steps": "Steps",
"waytype-ferry": "Ferry",
"waytype-construction": "Construction",
"surface-other": "Other",
"surface-paved": "Paved",
"surface-unpaved": "Unpaved",
"surface-asphalt": "Asphalt",
"surface-contrete": "Concrete",
"surface-cobblestone": "Cobblestone",
"surface-metal": "Metal",
"surface-wood": "Wood",
"surface-compacted-gravel": "Compacted gravel",
"surface-fine-gravel": "Fine gravel",
"surface-gravel": "Gravel",
"surface-dirt": "Dirt",
"surface-ground": "Ground",
"surface-ice": "Ice",
"surface-paving-stones": "Paving stones",
"surface-sand": "Sand",
"surface-woodchips": "Woodchips",
"surface-grass": "Grass",
"surface-grass-paver": "Grass paver",
"tollway-no": "No tollway",
"tollway-yes": "Tollway",
"traildifficulty-unknown": "Missing SAC tag",
"access-yes": "Yes",
"access-no": "No",
"access-customers": "Customers",
"access-destination": "Destination",
"access-delivery": "Delivery",
"access-private": "Private",
"access-permissive": "Permissive"
},
"help-popover": {
"show-alt": "Show explanation"
},
"history-dialog": {
"loading-error": "Error loading history",
"revert-error": "Error reverting history entry",
@ -264,6 +454,21 @@
"revert-delete-type-message-unnamed": "Do you really want to restore this type?",
"revert-delete-button": "Restore"
},
"hybrid-popover": {
"close-label": "Close",
"ok-label": "OK"
},
"import-tab": {
"parse-error-title": "Parsing error",
"parse-error-message_one": "The selected file could not be parsed.",
"parse-error-message_other": "The selected files could not be parsed.",
"no-geometries-error-title": "No geometries",
"no-geometries-error-message_one": "The selected file did not contain any geometries.",
"no-geometries-error-message_other": "The selected files did not contain any geometries.",
"partial-parse-error-title": "Parsing error",
"partial-parse-error-message": "Some of the selected files could not be parsed.",
"read-error-title": "Error reading files"
},
"leaflet-map": {
"open-full-size": "Open {{appName}} in full size",
"loading": "Loading…"
@ -295,9 +500,48 @@
"ascent-descent": "Climb/drop",
"zoom-to-object-label": "Zoom to line",
"edit-data": "Edit data",
"actions": "Actions",
"edit-waypoints": "Edit waypoints",
"delete": "Delete"
},
"manage-bookmarks-dialog": {
"title": "Manage Bookmarks",
"introduction": "Bookmarks are a quick way to remember and access collaborative maps. They are saved in your browser, other users will not be able to see them.",
"map-id": "Map ID",
"name": "Name",
"delete": "Delete",
"reorder-alt": "Reorder",
"bookmark-current": "Bookmark current map"
},
"manage-types-dialog": {
"delete-title": "Delete type",
"delete-message": "Do you really want to delete the type “{{typeName}}”?",
"delete-ok": "Delete",
"delete-error": "Error deleting type “{{typeName}}”",
"title": "Manage Types",
"name": "Name",
"type": "Type",
"edit": "Edit",
"type-marker": "Markers",
"type-line": "Lines",
"edit-button": "Edit",
"delete-button": "Delete",
"reorder-alt": "Reorder",
"create": "Create",
"marker-type": "Marker type",
"line-type": "Line type"
},
"manage-views-dialog": {
"default-view-error": "Error setting default view",
"delete-view-title": "Delete view",
"delete-view-message": "Do you really want to delete the view “{{viewName}}”?",
"delete-view-ok": "Delete",
"delete-view-error": "Error deleting view “{{viewName}}”",
"title": "Manage Views",
"make-default": "Make default",
"delete": "Delete",
"reorder-alt": "Reorder"
},
"marker-info": {
"delete-marker-title": "Delete marker",
"delete-marker-message": "Do you really want to delete the marker “{{name}}”?",
@ -306,6 +550,7 @@
"coordinates": "Coordinates",
"zoom-to-object-label": "Zoom to marker",
"edit-data": "Edit data",
"actions": "Actions",
"move": "Move",
"delete": "Delete"
},
@ -328,6 +573,20 @@
"cancel": "Cancel",
"save": "Save"
},
"open-map-dialog": {
"find-pads-error": "Error searching for public maps",
"map-id-format-error": "Please enter a valid map ID or URL.",
"map-not-found-error": "No map with this ID could be found.",
"title": "Open collaborative map",
"introduction": "Enter the link or ID of an existing collaborative map here to open that map.",
"open-map-by-id-button": "Open",
"search-public-maps": "Search public maps",
"search-alt": "Search",
"no-maps-found": "No maps could be found.",
"name": "Name",
"description": "Description",
"open-map-by-search-button": "Open"
},
"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.",
@ -390,6 +649,85 @@
"delete-description": "To delete this map, type {{code}} into the field and click the “Delete map” button.",
"delete-code": "DELETE"
},
"pagination": {
"first-label": "First",
"previous-label": "Previous",
"next-label": "Next",
"last-label": "Last"
},
"save-view-dialog": {
"save-view-error": "Error saving view",
"empty": "—",
"title": "Save current view",
"name": "Name",
"top-left": "Top left",
"bottom-right": "Bottom right",
"base-layer": "Base layer",
"overlays": "Overlays",
"overlays-joiner": ", ",
"pois": "POIs",
"include-pois": "Include POIs ({{pois}})",
"include-pois-interpolation-pois-joiner": ", ",
"filter": "Filter",
"include-current-filter": "Include current filter ({{filter}})",
"default-view": "Default view",
"make-default": "Make default view"
},
"search-result-info": {
"coordinates": "Coordinates",
"type": "Type",
"address": "Address",
"zoom-to-result-label": "Zoom to search result"
},
"shape-picker": {
"unknown-shape-error": "Unknown shape",
"no-shapes-error": "No shapes could be found."
},
"share-dialog": {
"type-admin": "Admin",
"type-write": "Writable",
"type-read": "Read-only",
"title": "Share",
"settings": "Settings",
"include-view": "Include current map view (centre: {{centre}}; zoom level: {{zoom}}; layer(s): {{layers}}{{conditionalPois}}{{conditionalSelection}}{{conditionalFilter}})",
"include-view-interpolation-layers-joiner": ", ",
"include-view-interpolation-conditionalPois": ", POIs: {{pois}}",
"include-view-interpolation-conditionalPois-interpolation-pois-joiner": ", ",
"include-view-interpolation-conditionalSelection": "; active object(s): {{description}}",
"include-view-interpolation-conditionalFilter": "; filter: {{filter}}",
"show-controls": "Show controls",
"show-toolbox": "Toolbox",
"show-search-box": "Search box",
"show-route": "Route tab",
"show-pois": "POI tab",
"show-legend": "Legend",
"show-locate": "Location",
"link-type": "Link type",
"share-link": "Share link",
"embed": "Embed",
"link-copied-title": "Map link copied",
"link-copied-message": "The map link was copied to the clipboard.",
"embed-copied-title": "Embed code copied",
"embed-copied-message": "The code to embed {{appName}} was copied to the clipboard.",
"embed-explanation": "Add this HTML code to a web page to embed {{appName}}. {{learnMore}}",
"embed-explanation-interpolation-learnMore": "Learn more"
},
"stroke-picker": {
"solid": "Solid",
"dashed": "Dashed",
"dotted": "Dotted"
},
"symbol-picker": {
"unknown-icon-error": "Unknown icon",
"filter-placeholder": "Filter",
"no-icons-found-error": "No icons could be found."
},
"use-as-dropdown": {
"label": "Use as",
"from": "Route start",
"via": "Route via",
"to": "Route destination"
},
"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.",
@ -400,6 +738,9 @@
"units-metric": "Metric",
"units-us": "US customary (miles, feet)"
},
"utils": {
"required-error": "Must not be empty."
},
"route-form": {
"route-description-outer": "Route from {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
@ -521,7 +862,7 @@
"open-file": "Open file",
"export": "Export",
"filter": "Filter",
"settings": "Settings",
"settings": "Map settings",
"history": "History",
"user-preferences": "User preferences"
},

Wyświetl plik

@ -1,12 +1,12 @@
<script setup lang="ts">
import { getLayers } from "facilmap-leaflet";
import { type Layer, Util } from "leaflet";
import { Util } from "leaflet";
import { computed } from "vue";
import ModalDialog from "./ui/modal-dialog.vue";
import { injectContextRequired, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import { T, useI18n } from "../utils/i18n";
const { t } = useI18n();
const i18n = useI18n();
const context = injectContextRequired();
const mapContext = requireMapContext(context);
@ -15,9 +15,20 @@
hidden: [];
}>();
const layers = computed((): Layer[] => {
const layers = computed(() => {
const { baseLayers, overlays } = getLayers(mapContext.value.components.map);
return [...Object.values(baseLayers), ...Object.values(overlays)];
return [...Object.values(baseLayers), ...Object.values(overlays)].flatMap((layer) => {
const attributionHtml = layer.getAttribution?.();
if (attributionHtml) {
return [{
id: Util.stamp(layer),
name: layer.options.fmGetName?.() ?? layer.options.fmName,
attributionHtml
}];
} else {
return [];
}
});
});
const fmVersion = __FM_VERSION__;
@ -25,7 +36,7 @@
<template>
<ModalDialog
:title="t('about-dialog.header', { version: fmVersion })"
:title="i18n.t('about-dialog.header', { version: fmVersion })"
class="fm-about"
size="lg"
@hidden="emit('hidden')"
@ -33,17 +44,17 @@
<p>
<T k="about-dialog.license-text">
<template #facilmap>
<a href="https://github.com/facilmap/facilmap" target="_blank"><strong>{{t('about-dialog.license-text-facilmap')}}</strong></a>
<a href="https://github.com/facilmap/facilmap" target="_blank"><strong>{{i18n.t('about-dialog.license-text-facilmap')}}</strong></a>
</template>
<template #license>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">{{t('about-dialog.license-text-license')}}</a>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">{{i18n.t('about-dialog.license-text-license')}}</a>
</template>
</T>
</p>
<p>
<T k="about-dialog.issues-text">
<template #tracker>
<a href="https://github.com/FacilMap/facilmap/issues" target="_blank">{{t('about-dialog.issues-text-tracker')}}</a>
<a href="https://github.com/FacilMap/facilmap/issues" target="_blank">{{i18n.t('about-dialog.issues-text-tracker')}}</a>
</template>
</T>
</p>
@ -51,37 +62,35 @@
<p>
<T k="about-dialog.help-text">
<template #documentation>
<a href="https://docs.facilmap.org/users/" target="_blank">{{t('about-dialog.help-text-documentation')}}</a>
<a href="https://docs.facilmap.org/users/" target="_blank">{{i18n.t('about-dialog.help-text-documentation')}}</a>
</template>
<template #discussions>
<a href="https://github.com/FacilMap/facilmap/discussions" target="_blank">{{t('about-dialog.help-text-discussions')}}</a>
<a href="https://github.com/FacilMap/facilmap/discussions" target="_blank">{{i18n.t('about-dialog.help-text-discussions')}}</a>
</template>
<template #chat>
<a href="https://matrix.to/#/#facilmap:rankenste.in" target="_blank">{{t('about-dialog.help-text-chat')}}</a>
<a href="https://matrix.to/#/#facilmap:rankenste.in" target="_blank">{{i18n.t('about-dialog.help-text-chat')}}</a>
</template>
</T>
</p>
<p><a href="https://docs.facilmap.org/users/privacy/" target="_blank">{{t('about-dialog.privacy-information')}}</a></p>
<h4>{{t('about-dialog.map-data')}}</h4>
<p><a href="https://docs.facilmap.org/users/privacy/" target="_blank">{{i18n.t('about-dialog.privacy-information')}}</a></p>
<h4>{{i18n.t('about-dialog.map-data')}}</h4>
<dl class="row">
<template v-for="layer in layers">
<template v-if="layer.options.attribution">
<dt :key="`name-${Util.stamp(layer)}`" class="col-sm-3">{{layer.options.fmName}}</dt>
<dd :key="`attribution-${Util.stamp(layer)}`" class="col-sm-9" v-html="layer.options.attribution"></dd>
</template>
<template v-for="{ id, name, attributionHtml } in layers" :key="id">
<dt class="col-sm-3">{{name}}</dt>
<dd class="col-sm-9" v-html="attributionHtml"></dd>
</template>
<dt class="col-sm-3">{{t('about-dialog.map-data-search')}}</dt>
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{i18n.t('about-dialog.map-data-search')}}</dt>
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{i18n.t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{t('about-dialog.map-data-pois')}}</dt>
<dd class="col-sm-9"><a href="https://overpass-api.de/" target="_blank">Overpass API</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{i18n.t('about-dialog.map-data-pois')}}</dt>
<dd class="col-sm-9"><a href="https://overpass-api.de/" target="_blank">Overpass API</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{i18n.t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{t('about-dialog.map-data-directions')}}</dt>
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{i18n.t('about-dialog.map-data-directions')}}</dt>
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{i18n.t('about-dialog.attribution-osm-contributors')}}</a></dd>
<dt class="col-sm-3">{{t('about-dialog.map-data-geoip')}}</dt>
<dt class="col-sm-3">{{i18n.t('about-dialog.map-data-geoip')}}</dt>
<dd class="col-sm-9">
<T k="about-dialog.map-data-geoip-description">
<template #maxmind>
@ -90,7 +99,7 @@
</T>
</dd>
</dl>
<h4>{{t('about-dialog.programs-libraries')}}</h4>
<h4>{{i18n.t('about-dialog.programs-libraries')}}</h4>
<ul>
<li><a href="https://nodejs.org/" target="_blank">Node.js</a></li>
<li><a href="https://sequelize.org/" target="_blank">Sequelize</a></li>
@ -110,7 +119,7 @@
<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>
<h4>{{i18n.t('about-dialog.icons')}}</h4>
<ul>
<li><a href="https://github.com/twain47/Open-SVG-Map-Icons/" target="_blank">Open SVG Map Icons</a></li>
<li><a href="https://glyphicons.com/" target="_blank">Glyphicons</a></li>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { SearchResult } from "facilmap-types";
import { find, getElevationForPoint, getFallbackLonLatResult, round } from "facilmap-utils";
import { find, formatCoordinates, getCurrentLanguage, getElevationForPoint, getFallbackLonLatResult } from "facilmap-utils";
import { SearchResultsLayer } from "facilmap-leaflet";
import SearchResultInfo from "./search-result-info.vue";
import { Util } from "leaflet";
@ -10,7 +10,7 @@
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";
import { isLanguageExplicit, useI18n } from "../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
@ -63,7 +63,11 @@
searchBoxContext.value.activateTab(`fm${context.id}-click-marker-tab-${tabs.value.length - 1}`, { expand: true });
(async () => {
const results = await mapContext.value.runOperation(async () => await find(`geo:${round(point.lat, 5)},${round(point.lon, 5)}?z=${mapContext.value.zoom}`));
const results = await mapContext.value.runOperation(async () => (
await find(`geo:${formatCoordinates(point)}?z=${mapContext.value.zoom}`, {
lang: isLanguageExplicit() ? getCurrentLanguage() : undefined
})
));
if (results.length > 0) {
tab.result = { ...results[0], elevation: tab.result.elevation };
@ -71,7 +75,7 @@
tab.isLoading = false;
})().catch((err) => {
toasts.showErrorToast(`find-error-${tab.id}`, i18n.t("click-marker-tab.look-up-error"), err);
toasts.showErrorToast(`find-error-${tab.id}`, () => i18n.t("click-marker-tab.look-up-error"), err);
});
(async () => {

Wyświetl plik

@ -8,7 +8,7 @@
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 { isLanguageExplicit, isUnitsExplicit, useI18n } from "../utils/i18n";
import { getCurrentLanguage, getCurrentUnits } from "facilmap-utils";
function isPadNotFoundError(serverError: Client["serverError"]): boolean {
@ -60,8 +60,8 @@
const newClient = new CustomClient(props.serverUrl, props.padId, {
query: {
lang: getCurrentLanguage(),
units: getCurrentUnits()
...isLanguageExplicit() ? { lang: getCurrentLanguage() } : {},
...isUnitsExplicit() ? { units: getCurrentUnits() } : {}
}
});
connectingClient.value = newClient;

Wyświetl plik

@ -131,7 +131,7 @@
<code>data.Description</code>
</template>
<template #example2>
<code>prop(data, &quot;Description&quot;)</code>)
<code>prop(data, &quot;Description&quot;)</code>
</template>
</T>
<br />

Wyświetl plik

@ -14,10 +14,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";
import { useMaxBreakpoint } from "../utils/bootstrap";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
lineId: ID;
@ -41,6 +44,8 @@
const resolvedCanControl = computed(() => canControl(client.value.types[line.value.typeId]));
const isXs = useMaxBreakpoint("xs");
watch(originalLine, (newLine, oldLine) => {
if (!newLine) {
modalRef.value?.modal.hide();
@ -57,14 +62,14 @@
await client.value.editLine(omit(line.value, "trackPoints"));
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-edit-line-error`, "Error saving line", err);
toasts.showErrorToast(`fm${context.id}-edit-line-error`, () => i18n.t("edit-line-dialog.save-error"), err);
}
}
</script>
<template>
<ModalDialog
title="Edit Line"
:title="i18n.t('edit-line-dialog.title')"
class="fm-edit-line"
:isModified="isModified"
@submit="$event.waitUntil(save())"
@ -73,7 +78,7 @@
>
<template #default>
<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-line-dialog.name")}}</label>
<ValidatedField
:value="line.name"
:validators="[getZodValidator(lineValidator.update.shape.name)]"
@ -88,8 +93,8 @@
</ValidatedField>
</div>
<div v-if="resolvedCanControl.includes('mode') && line.mode !== 'track'" class="row mb-3">
<label class="col-sm-3 col-form-label">Routing mode</label>
<div v-if="resolvedCanControl.includes('mode') && line.mode !== 'track' && context.settings.routing" class="row mb-3">
<label class="col-sm-3 col-form-label">{{i18n.t("edit-line-dialog.routing-mode")}}</label>
<div class="col-sm-9">
<RouteMode v-model="line.mode"></RouteMode>
</div>
@ -97,7 +102,7 @@
<template v-if="resolvedCanControl.includes('colour')">
<div class="row mb-3">
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">Colour</label>
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-line-dialog.colour")}}</label>
<div class="col-sm-9">
<ColourPicker
:id="`${id}-colour-input`"
@ -110,7 +115,7 @@
<template v-if="resolvedCanControl.includes('width')">
<div class="row mb-3">
<label :for="`${id}-width-input`" class="col-sm-3 col-form-label">Width</label>
<label :for="`${id}-width-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-line-dialog.width")}}</label>
<div class="col-sm-9">
<WidthPicker
:id="`${id}-width-input`"
@ -123,7 +128,7 @@
<template v-if="resolvedCanControl.includes('stroke')">
<div class="row mb-3">
<label :for="`${id}-stroke-input`" class="col-sm-3 col-form-label">Stroke</label>
<label :for="`${id}-stroke-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-line-dialog.stroke")}}</label>
<div class="col-sm-9">
<StrokePicker
:id="`${id}-stroke-input`"
@ -134,21 +139,31 @@
</template>
<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">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9">
<FieldInput
:id="`${id}-${idx}-input`"
:field="field"
v-model="line.data[field.name]"
></FieldInput>
<template v-if="field.type !== 'checkbox' || !isXs">
<div class="row mb-3">
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9" :class="{ 'fm-form-check-with-label': field.type === 'checkbox' }">
<FieldInput
:id="`${id}-${idx}-input`"
:field="field"
v-model="line.data[field.name]"
></FieldInput>
</div>
</div>
</div>
</template>
<template v-else>
<FieldInput
:id="`${id}-${idx}-input`"
:field="field"
v-model="line.data[field.name]"
showCheckboxLabel
></FieldInput>
</template>
</template>
</template>
<template #footer-left>
<DropdownMenu v-if="types.length > 1" class="dropup" label="Change type">
<DropdownMenu v-if="types.length > 1" class="dropup" :label="i18n.t('edit-line-dialog.change-type')">
<template v-for="type in types" :key="type.id">
<li>
<a

Wyświetl plik

@ -14,10 +14,13 @@
import DropdownMenu from "./ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "./ui/validated-form/validated-field.vue";
import { useI18n } from "../utils/i18n";
import { useMaxBreakpoint } from "../utils/bootstrap";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
markerId: ID;
@ -40,6 +43,8 @@
const resolvedCanControl = computed(() => canControl(client.value.types[marker.value.typeId]));
const isXs = useMaxBreakpoint("xs");
watch(originalMarker, (newMarker, oldMarker) => {
if (!newMarker) {
modalRef.value?.modal.hide();
@ -56,14 +61,14 @@
await client.value.editMarker(marker.value);
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-edit-marker-error`, "Error saving marker", err);
toasts.showErrorToast(`fm${context.id}-edit-marker-error`, () => i18n.t("edit-marker-dialog.save-error"), err);
}
}
</script>
<template>
<ModalDialog
title="Edit Marker"
:title="i18n.t('edit-marker-dialog.title')"
class="fm-edit-marker"
:isModified="isModified"
ref="modalRef"
@ -72,7 +77,7 @@
>
<template #default>
<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-marker-dialog.name")}}</label>
<ValidatedField
:value="marker.name"
:validators="[getZodValidator(markerValidator.update.shape.name)]"
@ -89,7 +94,7 @@
<template v-if="resolvedCanControl.includes('colour')">
<div class="row mb-3">
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">Colour</label>
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.colour")}}</label>
<div class="col-sm-9">
<ColourPicker
:id="`${id}-colour-input`"
@ -102,7 +107,7 @@
<template v-if="resolvedCanControl.includes('size')">
<div class="row mb-3">
<label :for="`${id}-size-input`" class="col-sm-3 col-form-label">Size</label>
<label :for="`${id}-size-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.size")}}</label>
<div class="col-sm-9">
<SizePicker
:id="`${id}-size-input`"
@ -115,7 +120,7 @@
<template v-if="resolvedCanControl.includes('symbol')">
<div class="row mb-3">
<label :for="`${id}-symbol-input`" class="col-sm-3 col-form-label">Icon</label>
<label :for="`${id}-symbol-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.icon")}}</label>
<div class="col-sm-9">
<SymbolPicker :id="`${id}-symbol-input`" v-model="marker.symbol"></SymbolPicker>
</div>
@ -124,7 +129,7 @@
<template v-if="resolvedCanControl.includes('shape')">
<div class="row mb-3">
<label :for="`${id}-shape-input`" class="col-sm-3 col-form-label">Shape</label>
<label :for="`${id}-shape-input`" class="col-sm-3 col-form-label">{{i18n.t("edit-marker-dialog.shape")}}</label>
<div class="col-sm-9">
<ShapePicker :id="`${id}-shape-input`" v-model="marker.shape"></ShapePicker>
</div>
@ -132,21 +137,31 @@
</template>
<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">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9">
<FieldInput
:id="`fm-edit-marker-${idx}-input`"
:field="field"
v-model="marker.data[field.name]"
></FieldInput>
<template v-if="field.type !== 'checkbox' || !isXs">
<div class="row mb-3">
<label :for="`${id}-${idx}-input`" class="col-sm-3 col-form-label text-break">{{formatFieldName(field.name)}}</label>
<div class="col-sm-9" :class="{ 'fm-form-check-with-label': field.type === 'checkbox' }">
<FieldInput
:id="`${id}-${idx}-input`"
:field="field"
v-model="marker.data[field.name]"
></FieldInput>
</div>
</div>
</div>
</template>
<template v-else>
<FieldInput
:id="`${id}-${idx}-input`"
:field="field"
v-model="marker.data[field.name]"
showCheckboxLabel
></FieldInput>
</template>
</template>
</template>
<template #footer-left>
<DropdownMenu v-if="types.length > 1" class="dropup" label="Change type">
<DropdownMenu v-if="types.length > 1" class="dropup" :label="i18n.t('edit-marker-dialog.change-type')">
<template v-for="type in types" :key="type.id">
<li>
<a

Wyświetl plik

@ -120,7 +120,7 @@
} catch (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"),
() => (isCreate.value ? i18n.t("edit-type-dialog.create-type-error") : i18n.t("edit-type-dialog.save-type-error")),
err
);
}
@ -135,8 +135,8 @@
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"))
() => i18n.t("edit-type-dialog.field-update-error"),
() => i18n.t("edit-type-dialog.field-disappeared-error")
);
}
type.value.fields[idx] = field;
@ -361,7 +361,7 @@
</div>
</template>
<template v-if="resolvedCanControl.includes('mode')">
<template v-if="resolvedCanControl.includes('mode') && context.settings.routing">
<div class="row mb-3">
<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">
@ -459,7 +459,7 @@
</template>
</div>
</td>
<td class="text-center">
<td class="text-center align-middle">
<FieldInput :field="field" v-model="field.default" ignore-default></FieldInput>
</td>
<td class="td-buttons">

Wyświetl plik

@ -282,7 +282,7 @@
>
<template #item="{ element: option, index: idx }">
<tr>
<td v-if="fieldValue.type == 'checkbox'">
<td v-if="fieldValue.type == 'checkbox'" class="align-middle">
<strong>{{formatCheckboxValue(idx === 0 ? "0" : "1")}}</strong>
</td>
<ValidatedField

Wyświetl plik

@ -12,8 +12,10 @@
import copyToClipboard from "copy-to-clipboard";
import type { CustomSubmitEvent } from "./ui/validated-form/validated-form.vue";
import { formatFieldName, formatTypeName, getOrderedTypes } from "facilmap-utils";
import { T, useI18n } from "../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -31,39 +33,39 @@
const copyRef = ref<InstanceType<typeof CopyToClipboardInput>>();
const formatOptions = {
gpx: "GPX",
geojson: "GeoJSON",
table: "HTML",
csv: "CSV"
};
const formatOptions = computed(() => ({
gpx: i18n.t("export-dialog.format-option-gpx"),
geojson: i18n.t("export-dialog.format-option-geojson"),
table: i18n.t("export-dialog.format-option-html"),
csv: i18n.t("export-dialog.format-option-csv")
}));
const hideOptions = computed(() => new Set([
"Name",
"Position",
"Distance",
"Line time",
const hideOptions = computed(() => ({
Name: i18n.t("export-dialog.column-name"),
Position: i18n.t("export-dialog.column-position"),
Distance: i18n.t("export-dialog.column-distance"),
Time: i18n.t("export-dialog.column-time"),
// TODO: Include only types not currently filtered
...orderedTypes.value.flatMap((type) => type.fields.map((field) => field.name))
]));
...Object.fromEntries(orderedTypes.value.flatMap((type) => type.fields.map((field) => [field.name, formatFieldName(field.name)])))
}));
const routeTypeOptions = {
"tracks": "Track points",
"zip": "Track points, one file per line (ZIP file)",
"routes": "Route points"
};
const routeTypeOptions = computed(() => ({
"tracks": i18n.t("export-dialog.route-type-track"),
"zip": i18n.t("export-dialog.route-type-track-zip"),
"routes": i18n.t("export-dialog.route-type-route")
}));
const format = ref<keyof typeof formatOptions>("gpx");
const routeType = ref<keyof typeof routeTypeOptions>("tracks");
const format = ref<keyof typeof formatOptions["value"]>("gpx");
const routeType = ref<keyof typeof routeTypeOptions["value"]>("tracks");
const filter = ref(true);
const hide = ref(new Set<string>());
const typeId = ref<ID>();
const methodOptions = computed(() => ({
download: format.value === "table" ? "Open file" : "Download file",
link: "Generate link",
download: format.value === "table" ? i18n.t("export-dialog.open-file") : i18n.t("export-dialog.download-file"),
link: i18n.t("export-dialog.generate-link"),
...(format.value === "table" ? {
copy: "Copy to clipboard"
copy: i18n.t("export-dialog.copy-to-clipboard")
} : {})
}));
@ -86,7 +88,7 @@
function validateTypeId(typeId: ID | undefined) {
if (mustSelectType.value && resolveTypeId(typeId) == null) {
return "Please select a type.";
return i18n.t("export-dialog.select-type-error");
}
}
@ -166,12 +168,12 @@
action: url.value,
target: format.value === "table" ? "_blank" : undefined,
isCreate: true,
okLabel: "Export"
okLabel: i18n.t("export-dialog.export-ok")
};
} else if (method.value === "copy") {
return {
isCreate: true,
okLabel: "Copy"
okLabel: i18n.t("export-dialog.copy-ok")
};
} else {
return {
@ -191,7 +193,7 @@
const res = await fetch(fetchUrl);
const html = await res.text();
copyToClipboard(html, { format: "text/html" });
toasts.showToast(undefined, `${formatOptions[format.value]} export copied`, `The ${formatOptions[format.value]} export was copied to the clipboard.`, { variant: "success", autoHide: true });
toasts.showToast(undefined, () => i18n.t("export-dialog.export-copied-title", { format: formatOptions.value[format.value] }), () => i18n.t("export-dialog.export-copied-message", { format: formatOptions.value[format.value] }), { variant: "success", autoHide: true });
})());
}
}
@ -200,7 +202,7 @@
<template>
<ModalDialog
title="Export collaborative map"
:title="i18n.t('export-dialog.title')"
size="lg"
class="fm-export-dialog"
ref="modalRef"
@ -208,30 +210,39 @@
@submit="handleSubmit"
@hidden="emit('hidden')"
>
<p>Export your map here to transfer it to another application, another device or another collaborative map.</p>
<p>{{i18n.t("export-dialog.introduction")}}</p>
<div class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-format-select`">
Format
{{i18n.t("export-dialog.format")}}
<HelpPopover>
<p>
<strong>GPX</strong> files can be used to transfer your map data into navigation and route planning software and devices, such as
OsmAnd or Garmin. They contain your markers and lines with their names and descriptions, but not their style
attributes (with the exception of some basic attributes supported by OsmAnd).
<T k="export-dialog.gpx-explanation">
<template #gpx>
<strong>{{i18n.t("export-dialog.gpx-explanation-interpolation-gpx")}}</strong>
</template>
</T>
</p>
<p>
<strong>GeoJSON</strong> files can be used to create complete backups or copies of your map. They contain the complete data of your
map, including the map settings, views, types, markers and lines along with all their data attributes. To restore
a GeoJSON backup or to create a copy of your map, simply import the file into FacilMap again.
<T k="export-dialog.geojson-explanation">
<template #geojson>
<strong>{{i18n.t("export-dialog.geojson-explanation-interpolation-geojson")}}</strong>
</template>
</T>
</p>
<p>
<strong>HTML</strong> files can be opened by any web browser. Exporting a map to HTML will render a table with only the data
attributes of all markers and lines. This table can also be copy&pasted into a spreadsheet application for
further processing.
<T k="export-dialog.html-explanation">
<template #html>
<strong>{{i18n.t("export-dialog.html-explanation-interpolation-html")}}</strong>
</template>
</T>
</p>
<p>
<strong>CSV</strong> files can be imported into most spreadsheet applications and only contain the data attributes of the objects
one type of marker or line.
<T k="export-dialog.csv-explanation">
<template #csv>
<strong>{{i18n.t("export-dialog.csv-explanation-interpolation-csv")}}</strong>
</template>
</T>
</p>
</HelpPopover>
</label>
@ -243,7 +254,7 @@
</div>
<div class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-method`">Export method</label>
<label class="col-sm-3 col-form-label" :for="`${id}-method`">{{i18n.t("export-dialog.export-method")}}</label>
<div class="col-sm-9">
<select class="form-select" v-model="method" :id="`${id}-method`">
<option v-for="(label, value) in methodOptions" :value="value" :key="value">{{label}}</option>
@ -253,19 +264,28 @@
<div v-if="canSelectRouteType" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-route-type-select`">
Route type
{{i18n.t("export-dialog.route-type")}}
<HelpPopover>
<p>
<strong>Track points</strong> will export your lines exactly as they are on your map.
<T k="export-dialog.track-explanation">
<template #track>
<strong>{{i18n.t("export-dialog.track-explanation-interpolation-track")}}</strong>
</template>
</T>
</p>
<p>
<strong>Track points, one file per line (ZIP file)</strong> will create a ZIP file with one GPX file
for all markers and one GPX file for each line. This works better with apps such as OsmAnd that only
support one line style per file.
<T k="export-dialog.track-zip-explanation">
<template #trackZip>
<strong>{{i18n.t("export-dialog.track-zip-explanation-interpolation-trackZip")}}</strong>
</template>
</T>
</p>
<p>
<strong>Route points</strong> will export only the from/via/to route points of your lines, and your
navigation software/device will have to calculate the route using its own map data and algorithm.
<T k="export-dialog.route-explanation">
<template #route>
<strong>{{i18n.t("export-dialog.route-explanation-interpolation-route")}}</strong>
</template>
</T>
</p>
</HelpPopover>
</label>
@ -278,7 +298,7 @@
<div v-if="canSelectType" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-type-select`">
Type
{{i18n.t("export-dialog.type")}}
</label>
<validatedField
:value="typeId"
@ -300,25 +320,25 @@
</div>
<div v-if="canSelectHide" class="row mb-3">
<label class="col-sm-3 col-form-label">Include columns</label>
<label class="col-sm-3 col-form-label">{{i18n.t("export-dialog.include-columns")}}</label>
<div class="col-sm-9 fm-export-dialog-hide-options">
<template v-for="key in hideOptions" :key="key">
<template v-for="(label, value) in hideOptions" :key="value">
<div class="form-check fm-form-check-with-label">
<input
class="form-check-input"
type="checkbox"
:id="`${id}-show-${key}-checkbox`"
:checked="!hide.has(key)"
@change="hide.has(key) ? hide.delete(key) : hide.add(key)"
:id="`${id}-show-${value}-checkbox`"
:checked="!hide.has(value)"
@change="hide.has(value) ? hide.delete(value) : hide.add(value)"
>
<label class="form-check-label" :for="`${id}-show-${key}-checkbox`">{{formatFieldName(key)}}</label>
<label class="form-check-label" :for="`${id}-show-${value}-checkbox`">{{label}}</label>
</div>
</template>
</div>
</div>
<div v-if="mapContext.filter" class="row mb-3">
<label class="col-sm-3 col-form-label" :for="`${id}-filter-checkbox`">Apply filter</label>
<label class="col-sm-3 col-form-label" :for="`${id}-filter-checkbox`">{{i18n.t("export-dialog.apply-filter")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -327,7 +347,7 @@
:id="`${id}-filter-checkbox`"
v-model="filter"
>
<label class="form-check-label" :for="`${id}-filter-checkbox`">Only include objects visible under current filter</label>
<label class="form-check-label" :for="`${id}-filter-checkbox`">{{i18n.t("export-dialog.apply-filter-label")}}</label>
</div>
</div>
</div>

Wyświetl plik

@ -72,11 +72,16 @@
settings: readonly(toRef(() => ({
toolbox: true,
search: true,
route: true,
pois: true,
locate: true,
autofocus: false,
legend: true,
interactive: true,
linkLogo: false,
updateHash: false,
routing: true,
advancedRouting: true,
...props.settings
}))),
components: shallowReadonly(components),

Wyświetl plik

@ -10,11 +10,16 @@ import type { ImportTabContext } from "./import-tab-context";
export interface FacilMapSettings {
toolbox: boolean;
search: boolean;
route: boolean;
pois: boolean;
locate: boolean;
autofocus: boolean;
legend: boolean;
interactive: boolean;
linkLogo: boolean;
updateHash: boolean;
routing: boolean;
advancedRouting: boolean;
}
export interface FacilMapComponents {

Wyświetl plik

@ -5,18 +5,21 @@ import type { Emitter } from "mitt";
import type { DeepReadonly } from "vue";
import type { SelectedItem } from "../../utils/selection";
import type SelectionHandler from "../../utils/selection";
import type { AttributionControl } from "../leaflet-map/attribution";
import type { Point } from "facilmap-types";
export type MapContextEvents = {
"open-selection": { selection: DeepReadonly<SelectedItem[]> };
};
export interface MapComponents {
attribution: AttributionControl;
bboxHandler: BboxHandler;
container: HTMLElement;
graphicScale: any;
hashHandler: HashHandler;
linesLayer: LinesLayer;
locateControl: L.Control.Locate;
locateControl?: L.Control.Locate;
map: Map;
markersLayer: MarkersLayer;
mousePosition: L.Control.MousePosition;
@ -44,6 +47,7 @@ export type MapContextData = {
overpassPresets: OverpassPreset[];
overpassCustom: string;
overpassMessage: string | undefined;
location: Point | undefined;
components: MapComponents;
loaded: boolean;
fatalError: string | undefined;

Wyświetl plik

@ -80,8 +80,8 @@
<template v-if="context.components.searchBox">
<SearchFormTab v-if="context.settings.search"></SearchFormTab>
<RouteFormTab v-if="context.settings.search"></RouteFormTab>
<OverpassFormTab v-if="context.settings.search"></OverpassFormTab>
<RouteFormTab v-if="context.settings.search && context.settings.routing && context.settings.route"></RouteFormTab>
<OverpassFormTab v-if="context.settings.search && context.settings.pois"></OverpassFormTab>
<MarkerInfoTab></MarkerInfoTab>
<LineInfoTab></LineInfoTab>
<MultipleInfoTab></MultipleInfoTab>

Wyświetl plik

@ -8,6 +8,7 @@
import { computed, ref } from "vue";
import { useToasts } from "./ui/toasts/toasts.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../utils/i18n";
type ViewImport = FileResultObject["views"][0];
type TypeImport = FileResultObject["types"][0];
@ -16,6 +17,7 @@
const mapContext = requireMapContext(context);
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
layerId: number;
@ -51,7 +53,7 @@
try {
await client.value.addView(view);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-file-result-import-error`, "Error importing view", err);
toasts.showErrorToast(`fm${context.id}-file-result-import-error`, () => i18n.t("file-results.import-view-error"), err);
} finally {
isAddingView.value.delete(view);
}
@ -68,7 +70,7 @@
try {
await client.value.addType(type);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-file-result-import-error`, "Error importing type", err);
toasts.showErrorToast(`fm${context.id}-file-result-import-error`, () => i18n.t("file-results.import-type-error"), err);
} finally {
isAddingType.value.delete(type);
}
@ -86,14 +88,14 @@
>
<template #before>
<template v-if="hasViews">
<h3>Views</h3>
<h3>{{i18n.t("file-results.views")}}</h3>
<ul class="list-group">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<li v-for="view in file.views" class="list-group-item">
<span class="text-break">
<a href="javascript:" @click="showView(view)">{{view.name}}</a>
{{" "}}
<span class="result-type">(View)</span>
<span class="result-type">({{i18n.t("file-results.view")}})</span>
</span>
<template v-if="isAddingView.has(view)">
<div class="spinner-border spinner-border-sm"></div>
@ -102,27 +104,27 @@
<a
href="javascript:"
@click="addView(view)"
v-tooltip.right="'Add this view to the map'"
v-tooltip.right="i18n.t('file-results.add-view-tooltip')"
>
<Icon icon="plus" alt="Add"></Icon>
<Icon icon="plus" :alt="i18n.t('file-results.add-view-alt')"></Icon>
</a>
</template>
</li>
</ul>
</template>
<h3 v-if="hasViews || hasTypes">Markers/Lines</h3>
<h3 v-if="hasViews || hasTypes">{{i18n.t("file-results.markers-lines")}}</h3>
</template>
<template #after>
<template v-if="hasTypes">
<h3>Types</h3>
<h3>{{i18n.t("file-results.types")}}</h3>
<ul class="list-group">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<li v-for="type in file.types" class="list-group-item">
<span class="text-break">
{{type.name}}
{{" "}}
<span class="result-type">(Type)</span>
<span class="result-type">({{i18n.t("file-results.type")}})</span>
</span>
<template v-if="isAddingType.has(type)">
<div class="spinner-border spinner-border-sm"></div>
@ -131,9 +133,9 @@
<a
href="javascript:"
@click="addType(type)"
v-tooltip.right="'Add this type to the map'"
v-tooltip.right="i18n.t('file-results.add-type-tooltip')"
>
<Icon icon="plus" alt="Add"></Icon>
<Icon icon="plus" :alt="i18n.t('file-results.add-type-alt')"></Icon>
</a>
</template>
</li>

Wyświetl plik

@ -39,7 +39,7 @@
try {
await client.value.listenToHistory();
} catch (err) {
toasts.showErrorToast(`${id}-listen-error`, i18n.t("history-dialog.loading-error"), err);
toasts.showErrorToast(`${id}-listen-error`, () => i18n.t("history-dialog.loading-error"), err);
} finally {
isLoading.value = false;
}
@ -69,7 +69,7 @@
try {
await client.value.revertHistoryEntry({ id: entry.id });
} catch (err) {
toasts.showErrorToast(`${id}-revert-error`, i18n.t("history-dialog.revert-error"), err);
toasts.showErrorToast(`${id}-revert-error`, () => i18n.t("history-dialog.revert-error"), err);
} finally {
isReverting.value = undefined;
}

Wyświetl plik

@ -10,11 +10,13 @@
import { useToasts } from "./ui/toasts/toasts.vue";
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import type { WritableImportTabContext } from "./facil-map-context-provider/import-tab-context";
import { useI18n } from "../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const searchBoxContext = requireSearchBoxContext(context);
const toasts = useToasts();
const i18n = useI18n();
const fileInputRef = ref<HTMLInputElement>();
@ -84,12 +86,12 @@
};
const hasAnyItems = result.features.length > 0 || Object.keys(result.types).length > 0 || Object.keys(result.views).length > 0;
if (!hasAnyItems && result.errors)
toasts.showErrorToast(`fm${context.id}-import-error`, "Parsing error", `The selected ${pluralize("file", fileList.length)} could not be parsed.`);
toasts.showErrorToast(`fm${context.id}-import-error`, () => i18n.t("import-tab.parse-error-title"), () => i18n.t("import-tab.parse-error-message", { count: fileList.length }));
else if (!hasAnyItems)
toasts.showErrorToast(`fm${context.id}-import-error`, "No geometries", `The selected ${pluralize("file", fileList.length)} did not contain any geometries.`);
toasts.showErrorToast(`fm${context.id}-import-error`, () => i18n.t("import-tab.no-geometries-error-title"), () => i18n.t("import-tab.no-geometries-error-message", { count: fileList.length }));
else {
if (result.errors)
toasts.showErrorToast(`fm${context.id}-import-error`, "Parsing error", "Some of the selected files could not be parsed.", { variant: "warning" });
toasts.showErrorToast(`fm${context.id}-import-error`, () => i18n.t("import-tab.partial-parse-error-title"), () => i18n.t("import-tab.partial-parse-error-message"), { variant: "warning" });
const layer = markRaw(new SearchResultsLayer(result.features, { pathOptions: { weight: 7 } }).addTo(mapContext.value.components.map));
if (result.features.length > 0) {
@ -105,7 +107,7 @@
}, 0);
}
} catch (err) {
toasts.showErrorToast(`fm${context.id}-import-error`, "Error reading files", err);
toasts.showErrorToast(`fm${context.id}-import-error`, () => i18n.t("import-tab.read-error-title"), err);
}
}

Wyświetl plik

@ -0,0 +1,49 @@
import { Control, DomEvent, DomUtil, Map, type ControlOptions } from "leaflet";
export interface AttributionControlOptions extends ControlOptions {
prefix?: string;
}
// Like the attribution control from Leaflet, but has a simple update() method that can be called in reaction to language changes
export class AttributionControl extends Control {
declare options: AttributionControlOptions;
protected _map?: Map;
constructor(options?: AttributionControlOptions) {
super({
position: 'bottomright',
prefix: `<a href="https://leafletjs.com" target="_blank">Leaflet</a>`,
...options
});
}
onAdd(map: Map): HTMLElement {
this._container = DomUtil.create('div', 'leaflet-control-attribution');
DomEvent.disableClickPropagation(this._container);
this.update();
map.on("layeradd", this.update, this);
map.on("layerremove", this.update, this);
return this._container;
}
onRemove(map: Map): void {
map.off("layeradd", this.update, this);
map.off("layerremove", this.update, this);
delete this._map;
}
update(): void {
if (this._map) {
this._container.innerHTML = [
...(this.options.prefix ? [this.options.prefix] : []),
...Object.values(this._map._layers).flatMap((layer) => {
const attr = layer.getAttribution?.();
return attr ? [attr] : [];
})
].join(" <span aria-hidden=\"true\">|</span> ");
}
}
}

Wyświetl plik

@ -16,7 +16,9 @@ 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";
import { getI18n, i18nResourceChangeCounter } from "../../utils/i18n";
import { AttributionControl } from "./attribution";
import { fixOnCleanup } from "../../utils/vue";
type MapContextWithoutComponents = Optional<WritableMapContext, 'components'>;
type OnCleanup = (cleanupFn: () => void) => void;
@ -26,7 +28,7 @@ function useMap(element: Ref<HTMLElement>, mapContext: MapContextWithoutComponen
const interaction = ref(0);
watchEffect((onCleanup) => {
const map = mapRef.value = markRaw(leafletMap(element.value, { boxZoom: false }));
const map = mapRef.value = markRaw(leafletMap(element.value, { boxZoom: false, attributionControl: false }));
map._controlCorners.bottomcenter = DomUtil.create("div", "leaflet-bottom fm-leaflet-center", map._controlContainer);
@ -53,6 +55,16 @@ function useMap(element: Ref<HTMLElement>, mapContext: MapContextWithoutComponen
interaction.value--;
});
map.on("locationfound", (data) => {
mapContext.location = { lat: data.latlng.lat, lon: data.latlng.lng };
});
const stopLocate = map.stopLocate;
map.stopLocate = function() {
mapContext.location = undefined;
return stopLocate.call(this);
};
onCleanup(() => {
map.remove();
});
@ -78,13 +90,31 @@ function useMapComponent<T>(
watch([
componentRef,
map
], ([component], prev, onCleanup) => {
], ([component], prev, onCleanup_) => {
const onCleanup = fixOnCleanup(onCleanup_);
activate(component as T, onCleanup);
}, { immediate: true });
return componentRef;
}
function useAttribution(map: Ref<Map>): Ref<Raw<AttributionControl>> {
return useMapComponent(
map,
() => markRaw(new AttributionControl()),
(attribution, onCleanup) => {
map.value.addControl(attribution);
const i18nWatcher = watch(i18nResourceChangeCounter, () => {
attribution.update();
});
onCleanup(() => {
i18nWatcher();
});
}
);
}
function useBboxHandler(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<BboxHandler>> {
return useMapComponent(
map,
@ -124,36 +154,45 @@ function useLinesLayer(map: Ref<Map>, client: Ref<ClientContext>): Ref<Raw<Lines
);
}
function useLocateControl(map: Ref<Map>): Ref<Raw<Control.Locate>> {
function useLocateControl(map: Ref<Map>, context: FacilMapContext): Ref<Raw<Control.Locate> | undefined> {
return useMapComponent(
map,
() => (
markRaw(control.locate({
flyTo: true,
icon: "a",
iconLoading: "a",
markerStyle: { pane: "fm-raised-marker", zIndexOffset: 10000 },
locateOptions: {
enableHighAccuracy: true
}
}))
),
(locateControl, onCleanup) => {
locateControl.addTo(map.value);
if (!coreSymbolList.includes("screenshot")) {
console.warn(`Icon "screenshot" is not in core icons.`);
() => {
if (context.settings.locate) {
return markRaw(control.locate({
flyTo: true,
icon: "a",
iconLoading: "a",
markerStyle: { pane: "fm-raised-marker", zIndexOffset: 10000 },
locateOptions: {
enableHighAccuracy: true
},
clickBehavior: {
inView: "stop",
outOfView: "setView",
inViewNotFollowing: "outOfView"
}
}));
}
},
(locateControl, onCleanup) => {
if (locateControl) {
locateControl.addTo(map.value);
getSymbolHtml("currentColor", "1.5em", "screenshot").then((html) => {
locateControl._container.querySelector("a")?.insertAdjacentHTML("beforeend", html);
}).catch((err) => {
console.error("Error loading locate control icon", err);
});
if (!coreSymbolList.includes("screenshot")) {
console.warn(`Icon "screenshot" is not in core icons.`);
}
onCleanup(() => {
locateControl.remove();
});
getSymbolHtml("currentColor", "1.5em", "screenshot").then((html) => {
locateControl._container.querySelector("a")?.insertAdjacentHTML("beforeend", html);
}).catch((err) => {
console.error("Error loading locate control icon", err);
});
onCleanup(() => {
locateControl.remove();
});
}
}
);
}
@ -306,10 +345,11 @@ function useHashHandler(map: Ref<Map>, client: Ref<ClientContext>, context: Faci
function useMapComponents(context: FacilMapContext, mapContext: MapContextWithoutComponents, mapRef: Ref<HTMLElement>, innerContainerRef: Ref<HTMLElement>): MapComponents {
const client = requireClientContext(context);
const map = useMap(mapRef, mapContext);
const attribution = useAttribution(map);
const bboxHandler = useBboxHandler(map, client);
const graphicScale = useGraphicScale(map);
const linesLayer = useLinesLayer(map, client);
const locateControl = useLocateControl(map);
const locateControl = useLocateControl(map, context);
const markersLayer = useMarkersLayer(map, client);
const mousePosition = useMousePosition(map);
const overpassLayer = useOverpassLayer(map, mapContext);
@ -318,17 +358,18 @@ function useMapComponents(context: FacilMapContext, mapContext: MapContextWithou
const hashHandler = useHashHandler(map, client, context, mapContext, overpassLayer);
const components: MapComponents = reactive({
map: map,
bboxHandler: bboxHandler,
graphicScale: graphicScale,
linesLayer: linesLayer,
locateControl: locateControl,
markersLayer: markersLayer,
mousePosition: mousePosition,
overpassLayer: overpassLayer,
searchResultsLayer: searchResultsLayer,
selectionHandler: selectionHandler,
hashHandler: hashHandler,
map,
attribution,
bboxHandler,
graphicScale,
linesLayer,
locateControl,
markersLayer,
mousePosition,
overpassLayer,
searchResultsLayer,
selectionHandler,
hashHandler,
container: innerContainerRef
});
@ -357,6 +398,7 @@ export async function useMapContext(context: FacilMapContext, mapRef: Ref<HTMLEl
overpassPresets: [],
overpassCustom: "",
overpassMessage: undefined,
location: undefined,
loaded: false,
fatalError: undefined,
runOperation: async (operation) => {

Wyświetl plik

@ -8,13 +8,14 @@
import RouteForm from "../route-form/route-form.vue";
import vTooltip from "../../utils/tooltip";
import { formatDistance, formatFieldName, formatFieldValue, formatRouteTime, formatTypeName, normalizeLineName } from "facilmap-utils";
import { computed, ref } from "vue";
import { computed, reactive, ref, toRef } 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";
import DropdownMenu from "../ui/dropdown-menu.vue";
const context = injectContextRequired();
const client = requireClientContext(context);
@ -62,7 +63,7 @@
try {
await client.value.deleteLine({ id: props.lineId });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-delete`, i18n.t("line-info.delete-line-error"), err);
toasts.showErrorToast(`fm${context.id}-line-info-delete`, () => i18n.t("line-info.delete-line-error"), err);
} finally {
isDeleting.value = false;
}
@ -73,7 +74,7 @@
}
async function moveLine(): Promise<void> {
toasts.hideToast(`fm${context.id}-line-info-move-error`);
toasts.hideToast(`fm${context.id}-line-info-move`);
mapContext.value.components.map.fire('fmInteractionStart');
const routeId = `l${line.value.id}`;
@ -83,18 +84,22 @@
mapContext.value.components.linesLayer.hideLine(line.value.id);
const isSaving = ref(false);
const done = async (save: boolean) => {
const route = client.value.routes[routeId];
if (save && !route)
return;
toasts.hideToast(`fm${context.id}-line-info-move`);
try {
if(save)
if(save) {
isSaving.value = true;
await client.value.editLine({ id: line.value.id, routePoints: route.routePoints, mode: route.mode });
}
toasts.hideToast(`fm${context.id}-line-info-move`);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, i18n.t("line-info.save-line-error"), err);
toasts.showErrorToast(`fm${context.id}-line-info-move`, () => i18n.t("line-info.save-line-error"), err);
} finally {
mapContext.value.components.map.fire('fmInteractionEnd');
isMoving.value = false;
@ -108,17 +113,27 @@
}
};
toasts.showToast(`fm${context.id}-line-info-move`, i18n.t("line-info.move-line-title"), i18n.t("line-info.move-line-message"), {
toasts.showToast(`fm${context.id}-line-info-move`, () => i18n.t("line-info.move-line-title"), () => i18n.t("line-info.move-line-message"), reactive({
noCloseButton: true,
actions: [
{ 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); } }
]
});
actions: toRef(() => [
{
label: i18n.t("line-info.move-line-finish"),
variant: "primary" as const,
onClick: () => { void done(true); },
isPending: isSaving.value,
isDisabled: isSaving.value
},
{
label: i18n.t("line-info.move-line-cancel"),
onClick: () => { void done(false); },
isDisabled: isSaving.value
}
])
}));
isMoving.value = true;
} catch (err) {
toasts.showErrorToast(`fm${context.id}-line-info-move-error`, i18n.t("line-info.save-line-error"), 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');
@ -203,24 +218,30 @@
:disabled="isDeleting || mapContext.interaction"
>{{i18n.t("line-info.edit-data")}}</button>
<button
v-if="!client.readonly && line.mode != 'track'"
type="button"
class="btn btn-secondary btn-sm"
@click="moveLine()"
:disabled="isDeleting || mapContext.interaction"
>{{i18n.t("line-info.edit-waypoints")}}</button>
<button
<DropdownMenu
v-if="!client.readonly"
type="button"
class="btn btn-secondary btn-sm"
@click="deleteLine()"
:disabled="isDeleting || mapContext.interaction"
size="sm"
:label="i18n.t('line-info.actions')"
:isBusy="isDeleting"
:isDisabled="mapContext.interaction"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
{{i18n.t("line-info.delete")}}
</button>
<li>
<a
v-if="line.mode != 'track'"
href="javascript:"
class="dropdown-item"
@click="moveLine()"
>{{i18n.t("line-info.edit-waypoints")}}</a>
</li>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="deleteLine()"
>{{i18n.t("line-info.delete")}}</a>
</li>
</DropdownMenu>
</div>
<RouteForm

Wyświetl plik

@ -5,9 +5,11 @@
import { computed } from "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";
const context = injectContextRequired();
const client = requireClientContext(context);
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -30,17 +32,17 @@
<template>
<ModalDialog
title="Manage Bookmarks"
:title="i18n.t('manage-bookmarks-dialog.title')"
size="lg"
class="fm-manage-bookmarks"
@hidden="emit('hidden')"
>
<p>Bookmarks are a quick way to remember and access collaborative maps. They are saved in your browser, other users will not be able to see them.</p>
<p>{{i18n.t("manage-bookmarks-dialog.introduction")}}</p>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Map ID</th>
<th>Name</th>
<th>{{i18n.t("manage-bookmarks-dialog.map-id")}}</th>
<th>{{i18n.t("manage-bookmarks-dialog.name")}}</th>
<th></th>
</tr>
</thead>
@ -52,15 +54,15 @@
>
<template #item="{ element: bookmark }">
<tr>
<td class="align-middle" :class="{ 'font-weight-bold': bookmark.id == client.padId }">
<td class="align-middle text-break" :class="{ 'font-weight-bold': bookmark.id == client.padId }">
{{bookmark.id}}
</td>
<td>
<td class="align-middle">
<input class="form-control" v-model="bookmark.customName" :placeholder="bookmark.name" />
</td>
<td class="td-buttons text-right">
<button type="button" class="btn btn-secondary" @click="deleteBookmark(bookmark)">Delete</button>
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" alt="Reorder"></Icon></button>
<td class="align-middle td-buttons text-right">
<button type="button" class="btn btn-secondary" @click="deleteBookmark(bookmark)">{{i18n.t("manage-bookmarks-dialog.delete")}}</button>
<button type="button" class="btn btn-secondary fm-drag-handle"><Icon icon="resize-vertical" :alt="i18n.t('manage-bookmarks-dialog.reorder-alt')"></Icon></button>
</td>
</tr>
</template>
@ -68,7 +70,7 @@
<tfoot v-if="client.padData && !isBookmarked">
<tr>
<td colspan="3">
<button type="button" class="btn btn-secondary" @click="addBookmark()">Bookmark current map</button>
<button type="button" class="btn btn-secondary" @click="addBookmark()">{{i18n.t("manage-bookmarks-dialog.bookmark-current")}}</button>
</td>
</tr>
</tfoot>

Wyświetl plik

@ -10,10 +10,12 @@
import { formatTypeName, getOrderedTypes } from "facilmap-utils";
import Draggable from "vuedraggable";
import Icon from "./ui/icon.vue";
import { useI18n } from "../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -31,17 +33,17 @@
try {
if (!await showConfirm({
title: "Delete type",
message: `Do you really want to delete the type “${formatTypeName(type.name)}”?`,
title: i18n.t("manage-types-dialog.delete-title"),
message: i18n.t("manage-types-dialog.delete-message", { typeName: formatTypeName(type.name) }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("manage-types-dialog.delete-ok")
})) {
return;
}
await client.value.deleteType({ id: type.id });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-manage-types-delete-${type.id}`, `Error deleting type “${formatTypeName(type.name)}`, err);
toasts.showErrorToast(`fm${context.id}-manage-types-delete-${type.id}`, () => i18n.t("manage-types-dialog.delete-error", { typeName: formatTypeName(type.name) }), err);
} finally {
delete isDeleting.value[type.id];
}
@ -74,7 +76,7 @@
<template>
<ModalDialog
title="Manage Types"
:title="i18n.t('manage-types-dialog.title')"
:isBusy="isBusy"
size="lg"
class="fm-manage-types"
@ -83,9 +85,9 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Edit</th>
<th>{{i18n.t("manage-types-dialog.name")}}</th>
<th>{{i18n.t("manage-types-dialog.type")}}</th>
<th>{{i18n.t("manage-types-dialog.edit")}}</th>
</tr>
</thead>
<Draggable
@ -98,14 +100,16 @@
<template #item="{ element: type }">
<tr>
<td class="text-break">{{formatTypeName(type.name)}}</td>
<td>{{type.type}}</td>
<td>
{{type.type === "marker" ? i18n.t("manage-types-dialog.type-marker") : i18n.t("manage-types-dialog.type-line")}}
</td>
<td class="td-buttons">
<button
type="button"
class="btn btn-secondary"
:disabled="isDeleting[type.id]"
@click="editDialogTypeId = type.id"
>Edit</button>
>{{i18n.t("manage-types-dialog.edit-button")}}</button>
<button
type="button"
@click="deleteType(type)"
@ -113,7 +117,7 @@
:disabled="isDeleting[type.id] || isMoving != null"
>
<div v-if="isDeleting[type.id]" class="spinner-border spinner-border-sm"></div>
Delete
{{i18n.t("manage-types-dialog.delete-button")}}
</button>
<button
type="button"
@ -121,7 +125,7 @@
:disabled="isDeleting[type.id] || isMoving != null"
>
<div v-if="isMoving === type.id" class="spinner-border spinner-border-sm"></div>
<Icon v-else icon="resize-vertical" alt="Reorder"></Icon>
<Icon v-else icon="resize-vertical" :alt="i18n.t('manage-types-dialog.reorder-alt')"></Icon>
</button>
</td>
</tr>
@ -130,13 +134,13 @@
<tfoot>
<tr>
<td colspan="3">
<DropdownMenu label="Create">
<DropdownMenu :label="i18n.t('manage-types-dialog.create')">
<li>
<a
href="javascript:"
class="dropdown-item"
@click="editDialogTypeId = 'createMarkerType'"
>Marker type</a>
>{{i18n.t("manage-types-dialog.marker-type")}}</a>
</li>
<li>
@ -144,7 +148,7 @@
href="javascript:"
class="dropdown-item"
@click="editDialogTypeId = 'createLineType'"
>Line type</a>
>{{i18n.t("manage-types-dialog.line-type")}}</a>
</li>
</DropdownMenu>
</td>

Wyświetl plik

@ -9,11 +9,13 @@
import { getOrderedViews } from "facilmap-utils";
import Draggable from "vuedraggable";
import Icon from "./ui/icon.vue";
import { useI18n } from "../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -38,7 +40,7 @@
try {
await client.value.editPad({ defaultViewId: view.id });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-save-view-error-default`, "Error setting default view", err);
toasts.showErrorToast(`fm${context.id}-save-view-error-default`, () => i18n.t("manage-views-dialog.default-view-error"), err);
} finally {
isSavingDefaultView.value = undefined;
}
@ -49,10 +51,10 @@
try {
if (!await showConfirm({
title: "Delete view",
message: `Do you really want to delete the view “${view.name}”?`,
title: i18n.t("manage-views-dialog.delete-view-title"),
message: i18n.t("manage-views-dialog.delete-view-message", { viewName: view.name }),
variant: "danger",
okLabel: "Delete"
okLabel: i18n.t("manage-views-dialog.delete-view-ok")
}))
return;
@ -60,7 +62,7 @@
await client.value.deleteView({ id: view.id });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-save-view-error-${view.id}`, `Error deleting view “${view.name}`, err);
toasts.showErrorToast(`fm${context.id}-save-view-error-${view.id}`, () => i18n.t("manage-views-dialog.delete-view-error", { viewName: view.name }), err);
} finally {
isDeleting.value.delete(view.id);
}
@ -93,7 +95,7 @@
<template>
<ModalDialog
title="Manage Views"
:title="i18n.t('manage-views-dialog.title')"
:isBusy="isBusy"
size="lg"
class="fm-manage-views"
@ -110,14 +112,14 @@
<template #item="{ element: view }">
<tr>
<td
class="text-break"
class="text-break align-middle"
:class="{
'font-weight-bold': client.padData?.defaultView && view.id == client.padData.defaultView.id
}"
>
<a href="javascript:" @click="display(view)">{{view.name}}</a>
</td>
<td class="td-buttons text-right">
<td class="td-buttons text-right align-middle">
<button
type="button"
class="btn btn-secondary"
@ -126,7 +128,7 @@
:disabled="!!isSavingDefaultView || isDeleting.has(view.id)"
>
<div v-if="isSavingDefaultView == view.id" class="spinner-border spinner-border-sm"></div>
Make default
{{i18n.t("manage-views-dialog.make-default")}}
</button>
<button
type="button"
@ -135,7 +137,7 @@
:disabled="isDeleting.has(view.id) || isSavingDefaultView == view.id || isMoving != null"
>
<div v-if="isDeleting.has(view.id)" class="spinner-border spinner-border-sm"></div>
Delete
{{i18n.t("manage-views-dialog.delete")}}
</button>
<button
type="button"
@ -143,7 +145,7 @@
:disabled="isDeleting.has(view.id) || isMoving != null"
>
<div v-if="isMoving === view.id" class="spinner-border spinner-border-sm"></div>
<Icon v-else icon="resize-vertical" alt="Reorder"></Icon>
<Icon v-else icon="resize-vertical" :alt="i18n.t('manage-views-dialog.reorder-alt')"></Icon>
</button>
</td>
</tr>

Wyświetl plik

@ -14,6 +14,7 @@
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";
import DropdownMenu from "../ui/dropdown-menu.vue";
const context = injectContextRequired();
const client = requireClientContext(context);
@ -32,7 +33,7 @@
back: [];
}>();
const isDeleting = ref(false);
const isBusy = ref(false);
const showEditDialog = ref(false);
const marker = computed(() => client.value.markers[props.markerId]);
@ -55,14 +56,14 @@
}))
return;
isDeleting.value = true;
isBusy.value = true;
try {
await client.value.deleteMarker({ id: props.markerId });
} catch (err) {
toasts.showErrorToast(`fm${context.id}-marker-info-delete`, i18n.t("marker-info.delete-marker-error"), err);
toasts.showErrorToast(`fm${context.id}-marker-info-delete`, () => i18n.t("marker-info.delete-marker-error"), err);
} finally {
isDeleting.value = false;
isBusy.value = false;
}
}
@ -117,25 +118,32 @@
type="button"
class="btn btn-secondary btn-sm"
@click="showEditDialog = true"
:disabled="isDeleting || mapContext.interaction"
:disabled="isBusy || mapContext.interaction"
>{{i18n.t("marker-info.edit-data")}}</button>
<button
<DropdownMenu
v-if="!client.readonly"
type="button"
class="btn btn-secondary btn-sm"
@click="move()"
:disabled="isDeleting || mapContext.interaction"
>{{i18n.t("marker-info.move")}}</button>
<button
v-if="!client.readonly"
type="button"
class="btn btn-secondary btn-sm"
@click="deleteMarker()"
:disabled="isDeleting || mapContext.interaction"
size="sm"
:label="i18n.t('marker-info.actions')"
:isBusy="isBusy"
:isDisabled="mapContext.interaction"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
{{i18n.t("marker-info.delete")}}
</button>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="move()"
>{{i18n.t("marker-info.move")}}</a>
</li>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="deleteMarker()"
>{{i18n.t("marker-info.delete")}}</a>
</li>
</DropdownMenu>
</div>
<EditMarkerDialog

Wyświetl plik

@ -85,7 +85,7 @@
await client.value.deleteLine({ id: object.id });
}
} catch (err) {
toasts.showErrorToast(`fm${context.id}-multiple-info-delete`, i18n.t("multiple-info.delete-objects-error"), err);
toasts.showErrorToast(`fm${context.id}-multiple-info-delete`, () => i18n.t("multiple-info.delete-objects-error"), err);
} finally {
isDeleting.value = false;
}

Wyświetl plik

@ -11,8 +11,10 @@
import type { FacilMapContext } from "./facil-map-context-provider/facil-map-context";
import ValidatedField from "./ui/validated-form/validated-field.vue";
import { parsePadUrl } from "facilmap-utils";
import { useI18n } from "../utils/i18n";
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -95,7 +97,7 @@
results.value = newResults.results;
pages.value = Math.ceil(newResults.totalLength / ITEMS_PER_PAGE);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-open-map-search-error`, "Error searching for public maps", err);
toasts.showErrorToast(`fm${context.id}-open-map-search-error`, () => i18n.t("open-map-dialog.find-pads-error"), err);
} finally {
isSearching.value = false;
}
@ -105,7 +107,7 @@
const parsed = parsePadId(padId, context);
if (!parsed) {
return "Please enter a valid map ID or URL.";
return i18n.t("open-map-dialog.map-id-format-error");
}
}
@ -115,7 +117,7 @@
if (parsed) {
const padInfo = await client.value.getPad({ padId: parsed.padId });
if (!padInfo) {
return "No map with this ID could be found.";
return i18n.t("open-map-dialog.map-not-found-error");
}
}
}
@ -123,13 +125,13 @@
<template>
<ModalDialog
title="Open collaborative map"
:title="i18n.t('open-map-dialog.title')"
size="lg"
class="fm-open-map"
ref="modalRef"
@hidden="emit('hidden')"
>
<p>Enter the link or ID of an existing collaborative map here to open that map.</p>
<p>{{i18n.t("open-map-dialog.introduction")}}</p>
<ValidatedField
:value="padId"
:validators="padId ? [
@ -138,7 +140,7 @@
] : []"
:reportValid="!!padId"
:debounceMs="300"
class="input-group has-validation"
class="input-group has-validation position-relative"
>
<template #default="slotProps">
<input
@ -154,9 +156,9 @@
:form="`${id}-open-form`"
>
<div v-if="openFormRef?.formData.isValidating" class="spinner-border spinner-border-sm"></div>
Open
{{i18n.t("open-map-dialog.open-map-by-id-button")}}
</button>
<div class="invalid-feedback">
<div class="invalid-tooltip">
{{slotProps.validationError}}
</div>
</template>
@ -164,7 +166,7 @@
<hr/>
<h4>Search public maps</h4>
<h4>{{i18n.t("open-map-dialog.search-public-maps")}}</h4>
<div class="input-group">
<input
@ -181,12 +183,12 @@
:form="`${id}-search-form`"
>
<div v-if="isSearching" class="spinner-border spinner-border-sm"></div>
<Icon v-else icon="search" alt="Search"></Icon>
<Icon v-else icon="search" :alt="i18n.t('open-map-dialog.search-alt')"></Icon>
</button>
</div>
<div v-if="submittedSearchQuery && results.length == 0" class="alert alert-danger">
No maps could be found.
<div v-if="submittedSearchQuery && results.length == 0" class="alert alert-danger mt-2">
{{i18n.t("open-map-dialog.no-maps-found")}}
</div>
<template v-if="submittedSearchQuery && results.length > 0">
@ -194,8 +196,8 @@
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>{{i18n.t("open-map-dialog.name")}}</th>
<th>{{i18n.t("open-map-dialog.description")}}</th>
<th></th>
</tr>
</thead>
@ -208,7 +210,7 @@
class="btn btn-secondary"
:href="context.baseUrl + encodeURIComponent(result.id)"
@click.exact.prevent="openResult(result)"
>Open</a>
>{{i18n.t("open-map-dialog.open-map-by-search-button")}}</a>
</td>
</tr>
</tbody>

Wyświetl plik

@ -70,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 ? i18n.t("pad-settings-dialog.create-map-error") : i18n.t("pad-settings-dialog.save-map-error"), 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);
}
};
@ -92,7 +92,7 @@
await client.value.deletePad();
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, i18n.t("pad-settings-dialog.delete-map-error"), err);
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, () => i18n.t("pad-settings-dialog.delete-map-error"), err);
} finally {
isDeleting.value = false;
}

Wyświetl plik

@ -353,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}`, i18n.t("route-form.find-destination-error", { query }), err);
toasts.showErrorToast(`fm${context.id}-route-form-suggestion-error-${idx}`, () => i18n.t("route-form.find-destination-error", { query }), err);
} finally {
resolveLoadingPromise();
}
@ -441,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`, i18n.t("route-form.route-calculation-error"), err);
toasts.showErrorToast(`fm${context.id}-route-form-error`, () => i18n.t("route-form.route-calculation-error"), err);
}
}
@ -633,7 +633,7 @@
<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>
<RouteMode v-if="context.settings.routing" v-model="routeMode" :tabindex="destinations.length+2" tooltip-placement="bottom"></RouteMode>
<button
type="submit"

Wyświetl plik

@ -8,11 +8,13 @@
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import ValidatedField from "./ui/validated-form/validated-field.vue";
import { viewValidator } from "facilmap-types";
import { T, useI18n } from "../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -34,7 +36,7 @@
const overlays = computed(() => {
const { overlays } = getLayers(mapContext.value.components.map);
return mapContext.value.layers.overlays.map((key) => overlays[key].options.fmName || key).join(", ") || "—";
return mapContext.value.layers.overlays.map((key) => overlays[key].options.fmName || key).join(i18n.t("save-view-dialog.overlays-joiner")) || i18n.t("save-view-dialog.empty");
});
async function save(): Promise<void> {
@ -55,14 +57,14 @@
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-save-view-error`, "Error saving view", err);
toasts.showErrorToast(`fm${context.id}-save-view-error`, () => i18n.t("save-view-dialog.save-view-error"), err);
}
};
</script>
<template>
<ModalDialog
title="Save current view"
:title="i18n.t('save-view-dialog.title')"
class="fm-save-view"
:isCreate="true"
ref="modalRef"
@ -70,7 +72,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("save-view-dialog.name")}}</label>
<ValidatedField
:value="name"
:validators="[
@ -94,7 +96,7 @@
</div>
<div class="row mb-3">
<label :for="`${id}-topleft-input`" class="col-sm-3 col-form-label">Top left</label>
<label :for="`${id}-topleft-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.top-left")}}</label>
<div class="col-sm-9">
<input
class="form-control-plaintext"
@ -106,7 +108,7 @@
</div>
<div class="row mb-3">
<label :for="`${id}-bottomright-input`" class="col-sm-3 col-form-label">Bottom right</label>
<label :for="`${id}-bottomright-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.bottom-right")}}</label>
<div class="col-sm-9">
<input
class="form-control-plaintext"
@ -118,7 +120,7 @@
</div>
<div class="row mb-3">
<label :for="`${id}-base-layer-input`" class="col-sm-3 col-form-label">Base layer</label>
<label :for="`${id}-base-layer-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.base-layer")}}</label>
<div class="col-sm-9">
<input
class="form-control-plaintext"
@ -130,7 +132,7 @@
</div>
<div class="row mb-3">
<label :for="`${id}-overlays-input`" class="col-sm-3 col-form-label">Overlays</label>
<label :for="`${id}-overlays-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.overlays")}}</label>
<div class="col-sm-9">
<input
class="form-control-plaintext"
@ -143,13 +145,13 @@
<template v-if="mapContext.overpassIsCustom ? !mapContext.overpassCustom : mapContext.overpassPresets.length == 0">
<div class="row mb-3">
<label :for="`${id}-overpass-input`" class="col-sm-3 col-form-label">POIs</label>
<label :for="`${id}-overpass-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.pois")}}</label>
<div class="col-sm-9">
<input
class="form-control-plaintext"
readonly
:id="`${id}-overpass-input`"
value="—"
:value="i18n.t('save-view-dialog.empty')"
/>
</div>
</div>
@ -157,7 +159,7 @@
<template v-else>
<div class="row mb-3">
<label :for="`${id}-overpass-input`" class="col-sm-3 col-form-label">POIs</label>
<label :for="`${id}-overpass-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.pois")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -167,7 +169,11 @@
v-model="includeOverpass"
/>
<label class="form-check-label" :for="`${id}-overpass-input`">
Include POIs (<code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code><template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(', ')}}</template>)
<T k="save-view-dialog.include-pois">
<template #pois>
<code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code><template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(i18n.t("save-view-dialog.include-pois-interpolation-pois-joiner"))}}</template>
</template>
</T>
</label>
</div>
</div>
@ -176,16 +182,16 @@
<template v-if="!mapContext.filter">
<div class="row mb-3">
<label :for="`${id}-filter-input`" class="col-sm-3 col-form-label">Filter</label>
<label :for="`${id}-filter-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.filter")}}</label>
<div class="col-sm-9">
<input class="form-control-plaintext" :id="`${id}-filter-input`" value="—" />
<input class="form-control-plaintext" :id="`${id}-filter-input`" :value="i18n.t('save-view-dialog.empty')" />
</div>
</div>
</template>
<template v-else>
<div class="row mb-3">
<label :for="`${id}-filter-checkbox`" class="col-sm-3 col-form-label">Filter</label>
<label :for="`${id}-filter-checkbox`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.filter")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -195,7 +201,11 @@
v-model="includeFilter"
/>
<label :for="`${id}-filter-checkbox`" class="form-check-label">
Include current filter (<code>{{mapContext.filter}}</code>)
<T k="save-view-dialog.include-current-filter">
<template #filter>
<code>{{mapContext.filter}}</code>
</template>
</T>
</label>
</div>
</div>
@ -203,7 +213,7 @@
</template>
<div class="row mb-3">
<label :for="`${id}-make-default-input`" class="col-sm-3 col-form-label">Default view</label>
<label :for="`${id}-make-default-input`" class="col-sm-3 col-form-label">{{i18n.t("save-view-dialog.default-view")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -212,7 +222,7 @@
:id="`${id}-make-default-input`"
v-model="makeDefault"
/>
<label :for="`${id}-make-default-input`" class="form-check-label">Make default view</label>
<label :for="`${id}-make-default-input`" class="form-check-label">{{i18n.t("save-view-dialog.make-default")}}</label>
</div>
</div>
</div>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import Icon from "../ui/icon.vue";
import { find, getElevationForPoint, isSearchId, parseUrlQuery } from "facilmap-utils";
import { find, getCurrentLanguage, getElevationForPoint, isSearchId, parseUrlQuery } from "facilmap-utils";
import { useToasts } from "../ui/toasts/toasts.vue";
import type { FindOnMapResult, SearchResult } from "facilmap-types";
import SearchResults from "../search-results/search-results.vue";
@ -14,7 +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";
import { isLanguageExplicit, useI18n } from "../../utils/i18n";
const emit = defineEmits<{
"hash-query-change": [query: HashQuery | undefined];
@ -92,7 +92,13 @@
const url = parseUrlQuery(query);
const [newSearchResults, newMapResults] = await Promise.all([
url ? client.value.find({ query, loadUrls: true }) : mapContext.value.runOperation(async () => await find(query)),
url ? (
client.value.find({ query, loadUrls: true })
) : (
mapContext.value.runOperation(async () => await find(query, {
lang: isLanguageExplicit() ? getCurrentLanguage() : undefined
}))
),
client.value.padData ? client.value.findOnMap({ query }) : undefined
]);
@ -134,7 +140,7 @@
}
}
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-form-error`, i18n.t("search-form.search-error"), err);
toasts.showErrorToast(`fm${context.id}-search-form-error`, () => i18n.t("search-form.search-error"), err);
return;
}
}

Wyświetl plik

@ -1,5 +1,5 @@
<script setup lang="ts">
import { renderOsmTag } from "facilmap-utils";
import { formatCoordinates, renderOsmTag } from "facilmap-utils";
import type { FindOnMapResult, Point, SearchResult, Type } from "facilmap-types";
import Icon from "./ui/icon.vue";
import type { FileResult } from "../utils/files";
@ -12,6 +12,9 @@
import ZoomToObjectButton from "./ui/zoom-to-object-button.vue";
import type { RouteDestination } from "./facil-map-context-provider/route-form-tab-context";
import AddToMapDropdown from "./ui/add-to-map-dropdown.vue";
import { useI18n } from "../utils/i18n";
const i18n = useI18n();
const props = withDefaults(defineProps<{
result: SearchResult | FileResult;
@ -37,9 +40,9 @@
const routeDestination = computed<RouteDestination | undefined>(() => {
if (isFileResult(props.result)) {
if (props.result.lat != null && props.result.lon != null) {
return { query: `${props.result.lat},${props.result.lon}` };
return { query: formatCoordinates({ lat: props.result.lat, lon: props.result.lon }) };
} else if (props.result.geojson?.type === "Point") {
return { query: `${props.result.geojson.coordinates[1]},${props.result.geojson.coordinates[0]}` };
return { query: formatCoordinates({ lat: props.result.geojson.coordinates[1], lon: props.result.geojson.coordinates[0] }) };
} else {
return undefined;
}
@ -65,17 +68,17 @@
</h2>
<dl class="fm-search-box-collapse-point fm-search-box-dl">
<template v-if="result.lat != null && result.lon != null">
<dt class="pos">Coordinates</dt>
<dt class="pos">{{i18n.t("search-result-info.coordinates")}}</dt>
<dd class="pos"><Coordinates :point="result as Point" :ele="result.elevation"></Coordinates></dd>
</template>
<template v-if="result.type">
<dt>Type</dt>
<dt>{{i18n.t("search-result-info.type")}}</dt>
<dd class="text-break">{{result.type}}</dd>
</template>
<template v-if="result.address">
<dt>Address</dt>
<dt>{{i18n.t("search-result-info.address")}}</dt>
<dd class="text-break">{{result.address}}</dd>
</template>
@ -94,7 +97,7 @@
<div class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="Zoom to search result"
:label="i18n.t('search-result-info.zoom-to-result-label')"
size="sm"
:destination="zoomDestination"
></ZoomToObjectButton>

Wyświetl plik

@ -157,7 +157,7 @@
modalRef.value?.modal.hide();
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, i18n.t("custom-import-dialog.import-error"), err);
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, () => i18n.t("custom-import-dialog.import-error"), err);
}
}
</script>

Wyświetl plik

@ -2,16 +2,18 @@
import { getLayers } from "facilmap-leaflet";
import { getLegendItems } from "./legend/legend-utils";
import type { Writable } from "facilmap-types";
import { quoteHtml, round } from "facilmap-utils";
import { formatCoordinates, quoteHtml } from "facilmap-utils";
import { computed, ref } from "vue";
import ModalDialog from "./ui/modal-dialog.vue";
import { getUniqueId } from "../utils/utils";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import CopyToClipboardInput from "./ui/copy-to-clipboard-input.vue";
import { T, useI18n } from "../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const i18n = useI18n();
const emit = defineEmits<{
hidden: [];
@ -22,7 +24,10 @@
const includeMapView = ref(true);
const showToolbox = ref(true);
const showSearch = ref(true);
const showRoute = ref(true);
const showPois = ref(true);
const showLegend = ref(true);
const showLocate = ref(true);
const padIdType = ref<Writable>(2);
const activeShareTab = ref(0);
@ -31,7 +36,7 @@
return [
baseLayers[mapContext.value.layers.baseLayer]?.options.fmName || mapContext.value.layers.baseLayer,
...mapContext.value.layers.overlays.map((key) => overlays[key].options.fmName || key)
].join(", ");
].join(i18n.t("share-dialog.include-view-interpolation-layers-joiner"));
});
const hasLegend = computed(() => {
@ -40,20 +45,33 @@
const padIdTypes = computed(() => {
return [
{ value: 2, text: 'Admin' },
{ value: 1, text: 'Writable' },
{ value: 0, text: 'Read-only' }
{ value: 2, text: i18n.t("share-dialog.type-admin") },
{ value: 1, text: i18n.t("share-dialog.type-write") },
{ value: 0, text: i18n.t("share-dialog.type-read") }
].filter((option) => client.value.writable != null && option.value <= client.value.writable);
});
const url = computed(() => {
const params = new URLSearchParams();
if (!showToolbox.value)
if (!showToolbox.value) {
params.set("toolbox", "false");
if (!showSearch.value)
}
if (!showSearch.value) {
params.set("search", "false");
if (!showLegend.value)
} else {
if (!showRoute.value) {
params.set("route", "false");
}
if (!showPois.value) {
params.set("pois", "false");
}
}
if (!showLegend.value) {
params.set("legend", "false");
}
if (!showLocate.value) {
params.set("locate", "false");
}
const paramsStr = params.toString();
return context.baseUrl
@ -69,13 +87,13 @@
<template>
<ModalDialog
title="Share"
:title="i18n.t('share-dialog.title')"
size="lg"
class="fm-share-dialog"
@hidden="emit('hidden')"
>
<div class="row mb-3">
<label class="col-sm-3 col-form-label">Settings</label>
<label class="col-sm-3 col-form-label">{{i18n.t("share-dialog.settings")}}</label>
<div class="col-sm-9">
<div class="form-check fm-form-check-with-label">
<input
@ -86,11 +104,55 @@
:disabled="!client.padData"
/>
<label :for="`${id}-include-map-view-input`" class="form-check-label">
Include current map view (centre: <code>{{round(mapContext.center.lat, 5)}},{{round(mapContext.center.lng, 5)}}</code>; zoom level: <code>{{mapContext.zoom}}</code>; layer(s): {{layers}}<template v-if="mapContext.overpassIsCustom ? !!mapContext.overpassCustom : mapContext.overpassPresets.length > 0">; POIs: <code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code><template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(', ')}}</template></template><template v-if="mapContext.activeQuery">; active object(s): <template v-if="mapContext.activeQuery.description">{{mapContext.activeQuery.description}}</template><code v-else>{{mapContext.activeQuery.query}}</code></template><template v-if="mapContext.filter">; filter: <code>{{mapContext.filter}}</code></template>)
<T k="share-dialog.include-view">
<template #centre>
<code>{{formatCoordinates({ lat: mapContext.center.lat, lon: mapContext.center.lng })}}</code>
</template>
<template #zoom>
<code>{{mapContext.zoom}}</code>
</template>
<template #layers>
{{layers}}
</template>
<template #conditionalPois>
<template v-if="mapContext.overpassIsCustom ? !!mapContext.overpassCustom : mapContext.overpassPresets.length > 0">
<T k="share-dialog.include-view-interpolation-conditionalPois">
<template #pois>
<code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code>
<template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(i18n.t("share-dialog.include-view-interpolation-conditionalPois-interpolation-pois-joiner"))}}</template>
</template>
</T>
</template>
</template>
<template #conditionalSelection>
<template v-if="mapContext.activeQuery">
<T k="share-dialog.include-view-interpolation-conditionalSelection">
<template #description>
<template v-if="mapContext.activeQuery.description">{{mapContext.activeQuery.description}}</template>
<code v-else>{{mapContext.activeQuery.query}}</code>
</template>
</T>
</template>
</template>
<template #conditionalFilter>
<template v-if="mapContext.filter">
<T k="share-dialog.include-view-interpolation-conditionalFilter">
<template #filter>
<code>{{mapContext.filter}}</code>
</template>
</T>
</template>
</template>
</T>
</label>
</div>
</div>
</div>
<div class="form-check">
<div class="row mb-3">
<label class="col-sm-3 col-form-label">{{i18n.t("share-dialog.show-controls")}}</label>
<div class="col-sm-9 checkbox-grid">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
@ -98,7 +160,7 @@
v-model="showToolbox"
/>
<label :for="`${id}-show-toolbox-input`" class="form-check-label">
Show toolbox
{{i18n.t("share-dialog.show-toolbox")}}
</label>
</div>
@ -110,7 +172,35 @@
v-model="showSearch"
/>
<label :for="`${id}-show-search-input`" class="form-check-label">
Show search box
{{i18n.t("share-dialog.show-search-box")}}
</label>
</div>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-route-input`"
:disabled="!showSearch"
:checked="showSearch && showRoute"
@change="showRoute = ($event.target as HTMLInputElement).checked"
/>
<label :for="`${id}-show-route-input`" class="form-check-label">
{{i18n.t("share-dialog.show-route")}}
</label>
</div>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-pois-input`"
:disabled="!showSearch"
:checked="showSearch && showPois"
@change="showPois = ($event.target as HTMLInputElement).checked"
/>
<label :for="`${id}-show-pois-input`" class="form-check-label">
{{i18n.t("share-dialog.show-pois")}}
</label>
</div>
@ -122,7 +212,19 @@
v-model="showLegend"
/>
<label :for="`${id}-show-legend-input`" class="form-check-label">
Show legend
{{i18n.t("share-dialog.show-legend")}}
</label>
</div>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-locate-input`"
v-model="showLocate"
/>
<label :for="`${id}-show-locate-input`" class="form-check-label">
{{i18n.t("share-dialog.show-locate")}}
</label>
</div>
</div>
@ -130,7 +232,7 @@
<template v-if="client.padData">
<div class="row mb-3">
<label :for="`${id}-padIdType-input`" class="col-sm-3 col-form-label">Link type</label>
<label :for="`${id}-padIdType-input`" class="col-sm-3 col-form-label">{{i18n.t("share-dialog.link-type")}}</label>
<div class="col-sm-9">
<select :id="`${id}-padIdType-input`" class="form-select" v-model="padIdType">
<option v-for="type in padIdTypes" :key="type.value" :value="type.value">{{type.text}}</option>
@ -146,7 +248,7 @@
href="javascript:"
:class="{ active: activeShareTab === 0 }"
@click="activeShareTab = 0"
>Share link</a>
>{{i18n.t("share-dialog.share-link")}}</a>
</li>
<li class="nav-item">
@ -155,7 +257,7 @@
href="javascript:"
:class="{ active: activeShareTab === 1 }"
@click="activeShareTab = 1"
>Embed</a>
>{{i18n.t("share-dialog.embed")}}</a>
</li>
</ul>
@ -164,8 +266,8 @@
class="mt-2"
:modelValue="url"
readonly
successTitle="Map link copied"
successMessage="The map link was copied to the clipboard."
:successTitle="i18n.t('share-dialog.link-copied-title')"
:successMessage="i18n.t('share-dialog.link-copied-message')"
></CopyToClipboardInput>
</template>
@ -174,13 +276,30 @@
class="mt-2"
:modelValue="embedCode"
readonly
successTitle="Embed code copied"
:successMessage="`The code to embed ${context.appName} was copied to the clipboard.`"
:successTitle="i18n.t('share-dialog.embed-copied-title')"
:successMessage="i18n.t('share-dialog.embed-copied-message', { appName: context.appName })"
:rows="2"
noQr
></CopyToClipboardInput>
<p class="mt-2">Add this HTML code to a web page to embed {{context.appName}}. <a href="https://docs.facilmap.org/developers/embed.html" target="_blank">Learn more</a></p>
<p class="mt-2">
<T k="share-dialog.embed-explanation">
<template #appName>
{{context.appName}}
</template>
<template #learnMore>
<a href="https://docs.facilmap.org/developers/embed.html" target="_blank">{{i18n.t("share-dialog.embed-explanation-interpolation-learnMore")}}</a>
</template>
</T>
</p>
</template>
</ModalDialog>
</template>
</template>
<style lang="scss">
.fm-share-dialog {
.checkbox-grid {
column-width: 160px;
}
}
</style>

Wyświetl plik

@ -108,7 +108,7 @@
:href="`${context.baseUrl}#${hash}`"
@click.exact.prevent="client.openPad(undefined)"
draggable="false"
>{{i18n.t("toolbox-collab-maps-dropdown.close-map", { padName: client.padData.name })}}</a>
>{{i18n.t("toolbox-collab-maps-dropdown.close-map", { padName: normalizePadName(client.padData.name) })}}</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -24,7 +24,7 @@
const { baseLayers } = getLayers(mapContext.value.components.map);
return Object.keys(baseLayers).map((key) => ({
key,
name: baseLayers[key].options.fmName!,
name: baseLayers[key].options.fmGetName!(),
active: mapContext.value.layers.baseLayer === key
}));
});
@ -33,7 +33,7 @@
const { overlays } = getLayers(mapContext.value.components.map);
return Object.keys(overlays).map((key) => ({
key,
name: overlays[key].options.fmName!,
name: overlays[key].options.fmGetName?.() ?? overlays[key].options.fmName!,
active: mapContext.value.layers.overlays.includes(key)
}));
});

Wyświetl plik

@ -52,7 +52,7 @@
mapContext.value?.components.selectionHandler.setSelectedItems(selection, true);
} catch (err) {
toasts.showErrorToast(`fm${context.id}-add-to-map-error`, i18n.t("add-to-map-dropdown.add-error"), err);
toasts.showErrorToast(`fm${context.id}-add-to-map-error`, () => i18n.t("add-to-map-dropdown.add-error"), err);
} finally {
isAdding.value = false;
}

Wyświetl plik

@ -20,7 +20,7 @@
function copy(): void {
copyToClipboard(formattedCoordinates.value);
toasts.showToast(undefined, i18n.t("coordinates.copied-title"), i18n.t("coordinates.copied-message"), { variant: "success", autoHide: true });
toasts.showToast(undefined, () => i18n.t("coordinates.copied-title"), () => i18n.t("coordinates.copied-message"), { variant: "success", autoHide: true });
}
</script>

Wyświetl plik

@ -36,7 +36,7 @@
function copy(): void {
copyToClipboard(fullUrl.value);
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 });
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>

Wyświetl plik

@ -1,9 +1,10 @@
<script setup lang="ts">
import FmHeightgraph from "../../utils/heightgraph";
import type { LineWithTrackPoints, RouteWithTrackPoints } from "facilmap-client";
import { onBeforeUnmount, onMounted, ref, toRef, watch } from "vue";
import { computed, markRaw, ref, toRef, watch } from "vue";
import { useDomEventListener, useEventListener } from "../../utils/utils";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { fixOnCleanup } from "../../utils/vue";
const props = defineProps<{
route: RouteWithTrackPoints | LineWithTrackPoints;
@ -20,33 +21,40 @@
const containerRef = ref<HTMLElement>();
let elevationPlot: FmHeightgraph | undefined;
onMounted(() => {
elevationPlot = new FmHeightgraph();
elevationPlot._map = mapContext.value.components.map;
handleTrackPointsChange();
containerRef.value!.append(elevationPlot.onAdd(mapContext.value.components.map));
handleResize();
const elevationPlot = computed(() => {
if (containerRef.value) {
// Construct in computed value so that it is reconstructed on language change
return markRaw(new FmHeightgraph({ mapMarkerPane: "lhl-raised" }));
}
});
onBeforeUnmount(() => {
elevationPlot!.onRemove(mapContext.value.components.map);
watch(elevationPlot, (value, oldValue, onCleanup_) => {
const onCleanup = fixOnCleanup(onCleanup_);
if (elevationPlot.value) {
elevationPlot.value._map = mapContext.value.components.map;
handleTrackPointsChange();
containerRef.value!.append(elevationPlot.value.onAdd(mapContext.value.components.map));
handleResize();
onCleanup(() => {
value!.onRemove(mapContext.value.components.map);
containerRef.value!.replaceChildren();
});
}
});
function handleTrackPointsChange() {
if (elevationPlot && props.route.trackPoints) {
elevationPlot.addData(props.route.extraInfo ?? {}, props.route.trackPoints);
if (elevationPlot.value && props.route.trackPoints) {
elevationPlot.value.addData(props.route.extraInfo ?? {}, props.route.trackPoints);
}
}
watch(() => props.route.trackPoints, handleTrackPointsChange);
function handleResize(): void {
if (elevationPlot && containerRef.value) {
elevationPlot.resize({ width: containerRef.value.offsetWidth, height: containerRef.value.offsetHeight });
if (elevationPlot.value && containerRef.value) {
elevationPlot.value.resize({ width: containerRef.value.offsetWidth, height: containerRef.value.offsetHeight });
}
}
</script>

Wyświetl plik

@ -7,6 +7,9 @@
import { computed, ref } from "vue";
import Popover from "./popover.vue";
import vTooltip from "../../utils/tooltip";
import { useI18n } from "../../utils/i18n";
const i18n = useI18n();
const props = defineProps<{
route: LineWithTrackPoints | RouteWithTrackPoints;
@ -24,17 +27,17 @@
<template>
<span class="fm-elevation-stats" v-if="route.ascent != null && route.descent != null">
<span>
<Icon icon="triangle-top" alt="Ascent"></Icon> {{formatAscentDescent(route.ascent)}} / <Icon icon="triangle-bottom" alt="Descent"></Icon> {{formatAscentDescent(route.descent)}}
<Icon icon="triangle-top" :alt="i18n.t('elevation-stats.ascent-alt')"></Icon> {{formatAscentDescent(route.ascent)}} / <Icon icon="triangle-bottom" :alt="i18n.t('elevation-status.descent-alt')"></Icon> {{formatAscentDescent(route.descent)}}
</span>
<span ref="statsButtonContainerRef">
<button
type="button"
class="btn btn-secondary"
v-tooltip="'Show elevation statistics'"
v-tooltip="i18n.t('elevation-stats.show-tooltip')"
@click="showStatsPopover = !showStatsPopover"
>
<Icon icon="circle-info" alt="Show stats"></Icon>
<Icon icon="circle-info" :alt="i18n.t('elevation-stats.show-alt')"></Icon>
</button>
</span>
@ -45,10 +48,10 @@
class="fm-elevation-stats-popover"
>
<dl class="row">
<dt class="col-6">Total ascent</dt>
<dt class="col-6">{{i18n.t("elevation-stats.total-ascent")}}</dt>
<dd class="col-6">{{formatAscentDescent(route.ascent)}}</dd>
<dt class="col-6">Total descent</dt>
<dt class="col-6">{{i18n.t("elevation-stats.total-descent")}}</dt>
<dd class="col-6">{{formatAscentDescent(route.descent)}}</dd>
<template v-for="stat in statsArr" :key="stat.i">

Wyświetl plik

@ -8,9 +8,11 @@
import { saveAs } from "file-saver";
import vTooltip from "../../utils/tooltip";
import { getSafeFilename } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const toasts = useToasts();
const i18n = useI18n();
const props = defineProps<{
filename: string;
@ -29,7 +31,7 @@
const exported = await props.getExport(format);
saveAs(new Blob([exported], { type: "application/gpx+xml" }), `${getSafeFilename(props.filename)}.gpx`);
} catch(err: any) {
toasts.showErrorToast(`fm${context.id}-export-dropdown-error`, "Error exporting route", err);
toasts.showErrorToast(`fm${context.id}-export-dropdown-error`, () => i18n.t("export-dropdown.export-error"), err);
} finally {
isExporting.value = false;
}
@ -40,24 +42,24 @@
<DropdownMenu
:size="props.size"
:isDisabled="props.isDisabled"
label="Export"
:label="i18n.t('export-dropdown.button-label')"
:isBusy="isExporting"
>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="exportRoute('gpx-trk')"
v-tooltip.right="'GPX files can be opened with most navigation software. In track mode, the calculated route is saved in the file.'"
>Export as GPX track</a>
</li>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="exportRoute('gpx-rte')"
v-tooltip.right="'GPX files can be opened with most navigation software. In route mode, only the start/end/via points are saved in the file, and the navigation software needs to calculate the route.'"
>Export as GPX route</a>
</li>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="exportRoute('gpx-trk')"
v-tooltip.right="i18n.t('export-dropdown.gpx-track-tooltip')"
>{{i18n.t("export-dropdown.gpx-track-label")}}</a>
</li>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="exportRoute('gpx-rte')"
v-tooltip.right="i18n.t('export-dropdown.gpx-route-tooltip')"
>{{i18n.t("export-dropdown.gpx-route-label")}}</a>
</li>
</DropdownMenu>
</template>

Wyświetl plik

@ -1,13 +1,14 @@
<script setup lang="ts">
import type { Field } from "facilmap-types";
import { computed } from "vue";
import { normalizeFieldValue } from "facilmap-utils";
import { formatFieldName, normalizeFieldValue } from "facilmap-utils";
const props = withDefaults(defineProps<{
field: Field;
ignoreDefault?: boolean;
modelValue?: string;
id?: string;
showCheckboxLabel?: boolean;
}>(), {
ignoreDefault: false
});
@ -37,13 +38,29 @@
</select>
</template>
<template v-else-if="field.type === 'checkbox'">
<input
type="checkbox"
class="form-check-input"
:id="id"
:checked="value === '1'"
@input="value = $event ? '1' : '0'"
/>
<template v-if="props.showCheckboxLabel">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="id"
:checked="value === '1'"
@input="value = ($event.target as HTMLInputElement).checked ? '1' : '0'"
/>
<label v-if="props.showCheckboxLabel" :for="id" class="form-check-label">
{{formatFieldName(field.name)}}
</label>
</div>
</template>
<template v-else>
<input
type="checkbox"
class="form-check-input fm-large-checkbox"
:id="id"
:checked="value === '1'"
@input="value = ($event.target as HTMLInputElement).checked ? '1' : '0'"
/>
</template>
</template>
<template v-else>
<input type="text" class="form-control" :id="id" v-model="value"/>
@ -54,12 +71,10 @@
<style lang="scss">
.fm-field-input {
.custom-checkbox {
height: calc(1.5em + 0.75rem + 2px);
}
.custom-checkbox label::before, .custom-checkbox label::after {
top: calc(0.75em - 0.125rem + 1px);
.fm-large-checkbox {
margin-top: 0;
height: 1.5rem;
width: 1.5rem;
}
}

Wyświetl plik

@ -2,6 +2,9 @@
import Icon from "./icon.vue";
import { ref } from "vue";
import Popover from "./popover.vue";
import { useI18n } from "../../utils/i18n";
const i18n = useI18n();
const helpLinkRef = ref<HTMLElement>();
const showPopover = ref(false);
@ -9,7 +12,7 @@
<template>
<a href="javascript:" ref="helpLinkRef" @click.prevent="showPopover = !showPopover">
<Icon icon="question-sign" alt="Show explanation"></Icon>
<Icon icon="question-sign" :alt="i18n.t('help-popover.show-alt')"></Icon>
</a>
<Popover :element="helpLinkRef" v-model:show="showPopover" hideOnOutsideClick>
<slot></slot>

Wyświetl plik

@ -5,6 +5,7 @@
import Popover from "./popover.vue";
import { useRefWithOverride } from "../../utils/vue";
import AttributePreservingElement from "./attribute-preserving-element.vue";
import { useI18n } from "../../utils/i18n";
export const hybridPopoverShouldUseModal = useMaxBreakpoint("xs");
@ -15,6 +16,8 @@
</script>
<script setup lang="ts">
const i18n = useI18n();
const props = withDefaults(defineProps<{
show?: boolean;
title?: string;
@ -126,13 +129,13 @@
<div class="modal-content">
<div v-if="props.title" class="modal-header">
<h1 class="modal-title fs-5">{{props.title}}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="i18n.t('hybrid-popover.close-label')"></button>
</div>
<div class="modal-body">
<slot :is-modal="false" :close="close"></slot>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">{{i18n.t("hybrid-popover.ok-label")}}</button>
</div>
</div>
</div>

Wyświetl plik

@ -7,7 +7,7 @@
import AttributePreservingElement from "./attribute-preserving-element.vue";
import { useI18n } from "../../utils/i18n";
const { t } = useI18n();
const i18n = useI18n();
const props = withDefaults(defineProps<{
title?: string;
@ -111,7 +111,7 @@
@click="modal.hide()"
type="button"
class="btn-close"
:aria-label="t('modal-dialog.close')"
:aria-label="i18n.t('modal-dialog.close')"
></button>
</div>
<div class="modal-body">
@ -128,7 +128,7 @@
class="btn btn-secondary"
@click="modal.hide()"
:disabled="isSubmitting || props.isBusy"
>{{t('modal-dialog.cancel')}}</button>
>{{i18n.t('modal-dialog.cancel')}}</button>
<button
type="submit"
@ -137,7 +137,7 @@
:disabled="isSubmitting || props.isBusy"
>
<div v-if="isSubmitting" class="spinner-border spinner-border-sm"></div>
{{props.okLabel ?? (isCloseButton ? t('modal-dialog.close') : t('modal-dialog.save'))}}
{{props.okLabel ?? (isCloseButton ? i18n.t('modal-dialog.close') : i18n.t('modal-dialog.save'))}}
</button>
</div>
</ValidatedForm>

Wyświetl plik

@ -1,6 +1,9 @@
<script setup lang="ts">
import { range } from "lodash-es";
import { computed } from "vue";
import { useI18n } from "../../utils/i18n";
const i18n = useI18n();
const props = defineProps<{
pages: number;
@ -30,7 +33,7 @@
<a
:href="isPrevDisabled ? undefined : 'javascript:'"
class="page-link"
aria-label="First"
:aria-label="i18n.t('pagination.first-label')"
@click="handleClick(0)"
>
<span aria-hidden="true">«</span>
@ -41,7 +44,7 @@
<a
:href="isPrevDisabled ? undefined : 'javascript:'"
class="page-link"
aria-label="Previous"
:aria-label="i18n.t('pagination.previous-label')"
@click="handleClick(props.modelValue - 1)"
>
<span aria-hidden="true"></span>
@ -66,7 +69,7 @@
<a
:href="isNextDisabled ? undefined : 'javascript:'"
class="page-link"
aria-label="Next"
:aria-label="i18n.t('pagination.next-label')"
@click="handleClick(props.modelValue + 1)"
>
<span aria-hidden="true"></span>
@ -77,7 +80,7 @@
<a
:href="isNextDisabled ? undefined : 'javascript:'"
class="page-link"
aria-label="Last"
:aria-label="i18n.t('pagination.last-label')"
@click="handleClick(pages - 1)"
>
<span aria-hidden="true">»</span>

Wyświetl plik

@ -7,10 +7,12 @@
import vTooltip, { type TooltipPlacement } from "../../utils/tooltip";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { useI18n } from "../../utils/i18n";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
type Mode = Exclude<DecodedRouteMode['mode'], 'track'>;
type Type = DecodedRouteMode['type'];
const context = injectContextRequired();
const i18n = useI18n();
const props = withDefaults(defineProps<{
@ -184,7 +186,7 @@
</label>
</template>
<div class="btn-group">
<div v-if="context.settings.advancedRouting" class="btn-group">
<DropdownMenu
:tabindex="tabindex != null ? tabindex + constants.modes.length : undefined"
tooltip="Customize"

Wyświetl plik

@ -8,6 +8,7 @@
import { computed, nextTick, ref } from "vue";
import type { Validator } from "./validated-form/validated-field.vue";
import { computedAsync } from "../../utils/vue";
import { useI18n } from "../../utils/i18n";
const items = computedAsync(async () => {
const list = await Promise.all(shapeList.map(async (s) => (
@ -18,6 +19,8 @@
</script>
<script setup lang="ts">
const i18n = useI18n();
const gridRef = ref<InstanceType<typeof PrerenderedList>>();
const props = defineProps<{
@ -41,7 +44,7 @@
function validateShape(shape: string) {
if (shape && !shapeList.includes(shape)) {
return "Unknown shape";
return i18n.t("shape-picker.unknown-shape-error");
}
}
@ -79,7 +82,7 @@
</div>
<template v-else>
<div v-if="Object.keys(items).length == 0" class="alert alert-danger mt-2 mb-1">No shapes could be found.</div>
<div v-if="Object.keys(items).length == 0" class="alert alert-danger mt-2 mb-1">{{i18n.t("shape-picker.no-shapes-error")}}</div>
<PrerenderedList
:items="items"

Wyświetl plik

@ -2,6 +2,9 @@
import type { Stroke } from 'facilmap-types';
import type { Validator } from './validated-form/validated-field.vue';
import { computed } from 'vue';
import { useI18n } from '../../utils/i18n';
const i18n = useI18n();
const props = defineProps<{
modelValue: Stroke;
@ -25,8 +28,8 @@
v-model="value"
class="form-select"
>
<option value="">Solid</option>
<option value="dashed">Dashed</option>
<option value="dotted">Dotted</option>
<option value="">{{i18n.t("stroke-picker.solid")}}</option>
<option value="dashed">{{i18n.t("stroke-picker.dashed")}}</option>
<option value="dotted">{{i18n.t("stroke-picker.dotted")}}</option>
</select>
</template>

Wyświetl plik

@ -7,6 +7,7 @@
import { computed, nextTick, ref } from "vue";
import type { Validator } from "./validated-form/validated-field.vue";
import { computedAsync } from "../../utils/vue";
import { useI18n } from "../../utils/i18n";
let allItemsP: Promise<Record<string, string>>;
async function getAllItems(): Promise<Record<string, string>> {
@ -20,6 +21,8 @@
</script>
<script setup lang="ts">
const i18n = useI18n();
const gridRef = ref<InstanceType<typeof PrerenderedList>>();
const props = defineProps<{
@ -73,7 +76,7 @@
function validateSymbol(symbol: string) {
if (symbol && symbol.length !== 1 && !symbolList.includes(symbol)) {
return "Unknown icon";
return i18n.t("symbol-picker.unknown-icon-error");
}
}
@ -118,7 +121,7 @@
type="search"
class="form-control fm-keyboard-navigation-exception"
v-model="filter"
placeholder="Filter"
:placeholder="i18n.t('symbol-picker.filter-placeholder')"
autocomplete="off"
ref="filterRef"
tabindex="-1"
@ -129,7 +132,7 @@
</div>
<template v-else>
<div v-if="Object.keys(items).length == 0" class="alert alert-danger mt-2 mb-1">No icons could be found.</div>
<div v-if="Object.keys(items).length == 0" class="alert alert-danger mt-2 mb-1">{{i18n.t("symbol-picker.no-icons-found-error")}}</div>
<PrerenderedList
:items="items"

Wyświetl plik

@ -1,34 +1,44 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from "vue";
import { computed, onBeforeUnmount, onMounted } from "vue";
import { useToasts } from "./toasts.vue";
import type { ToastOptions } from "./toasts.vue";
import type { ToastAction } from "./toasts.vue";
import type { ThemeColour } from "../../../utils/bootstrap";
/* eslint-disable vue/valid-template-root */
const toasts = useToasts();
const props = defineProps<Omit<ToastOptions, "onHidden"> & {
const props = defineProps<{
id: string;
title: string;
message: string | Error;
actions?: ToastAction[];
spinner?: boolean;
variant?: ThemeColour;
noCloseButton?: boolean;
autoHide?: boolean;
}>();
const emit = defineEmits<{
hidden: [];
}>();
const resolvedOptions = computed(() => ({
actions: props.actions,
spinner: props.spinner,
variant: props.variant,
noCloseButton: props.noCloseButton,
autoHide: props.autoHide,
onHidden: () => {
emit("hidden");
}
}));
onMounted(() => {
const { id, title, message, ...options } = props;
const resolvedOptions: ToastOptions = {
...options,
onHidden: () => {
emit("hidden");
}
};
if (message instanceof Error) {
toasts.showErrorToast(id, title, message, resolvedOptions);
if (props.message instanceof Error) {
toasts.showErrorToast(props.id, () => props.title, props.message, () => resolvedOptions.value);
} else {
toasts.showToast(id, title, message, resolvedOptions);
toasts.showToast(props.id, () => props.title, () => props.message as string, () => resolvedOptions.value);
}
});

Wyświetl plik

@ -1,18 +1,19 @@
<script lang="ts">
/// <reference types="vite/client" />
import { createApp, nextTick, onScopeDispose, reactive, ref, type App } from "vue";
import { createApp, nextTick, onScopeDispose, reactive, ref, toRef, type App } from "vue";
import Toast from "bootstrap/js/dist/toast";
import Toasts from "./toasts.vue";
import { mapRef } from "../../../utils/vue";
import { getUniqueId } from "../../../utils/utils";
import type { ThemeColour } from "../../../utils/bootstrap";
import { getI18n, useI18n } from "../../../utils/i18n";
import vLinkDisabled from "../../../utils/link-disabled";
export interface ToastContext {
showErrorToast(id: string | undefined, title: string, err: any, options?: ToastOptions): Promise<void>;
showErrorToast(id: string | undefined, title: string | (() => string), err: any, options?: ToastOptions | (() => ToastOptions)): Promise<void>;
toastErrors<C extends (...args: any[]) => any>(callback: C): C;
showToast(id: string | undefined, title: string, message: string, options?: ToastOptions): Promise<void>;
showToast(id: string | undefined, title: string | (() => string), message: string | (() => string), options?: ToastOptions | (() => ToastOptions)): Promise<void>;
hideToast(id: string): Promise<void>;
dispose(): void;
}
@ -32,14 +33,17 @@
label: string;
href?: string;
variant?: ThemeColour;
isDisabled?: boolean;
isPending?: boolean;
}
interface ToastInstance extends ToastOptions {
interface ToastInstance {
key: string;
id: string | undefined;
title: string;
message: string;
contextId: string;
options: ToastOptions;
}
export const toastContainer = document.createElement("div");
@ -96,7 +100,14 @@
void result.hideToast(id);
}
const toast: ToastInstance = { ...options, key: getUniqueId("fm-toast"), id, title, message, contextId };
const toast: ToastInstance = reactive({
key: getUniqueId("fm-toast"),
id,
title: toRef(title),
message: toRef(message),
contextId,
options: toRef(options)
});
toasts.value.push(toast);
await nextTick();
@ -132,16 +143,16 @@
await new Promise<void>((resolve) => {
const toastRef = toastRefs.get(toast)!;
toastRef.addEventListener("shown.bs.toast", () => resolve());
Toast.getOrCreateInstance(toastRef, { autohide: !!toast.autoHide }).show();
Toast.getOrCreateInstance(toastRef, { autohide: !!toast.options.autoHide }).show();
toastRef.addEventListener("hide.bs.toast", () => {
toast.onHide?.();
toast.options.onHide?.();
});
toastRef.addEventListener("hidden.bs.toast", () => {
toasts.value = toasts.value.filter((t) => t !== toast);
toastRefs.delete(toast);
toast.onHidden?.();
toast.options.onHidden?.();
});
});
}
@ -168,7 +179,7 @@
v-for="toast in toasts"
:key="toast.key"
class="toast"
:class="{ 'border-0': toast.variant }"
:class="{ 'border-0': toast.options.variant }"
role="alert"
aria-live="assertive"
aria-atomic="true"
@ -176,31 +187,35 @@
>
<div
class="toast-header bg-opacity-25 text-break"
:class="toast.variant && `bg-${toast.variant} bg-opacity-25`"
:class="toast.options.variant && `bg-${toast.options.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="i18n.t('toasts.close-label')"></button>
<button v-if="!toast.options.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"
:class="toast.variant && `bg-${toast.variant} bg-opacity-10`"
:class="toast.options.variant && `bg-${toast.options.variant} bg-opacity-10`"
>
<div>
<div v-if="toast.spinner" class="spinner-border spinner-border-sm" role="status">
<div v-if="toast.options.spinner" class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{i18n.t("toasts.spinner-label")}}</span>
</div>
{{toast.message}}
</div>
<div v-if="(toast.actions?.length ?? 0) > 0" class="btn-toolbar mt-2 pt-2 border-top">
<template v-for="(action, idx) in toast.actions" :key="idx">
<div v-if="(toast.options.actions?.length ?? 0) > 0" class="btn-toolbar mt-2 pt-2 border-top">
<template v-for="(action, idx) in toast.options.actions" :key="idx">
<button
v-if="!action.href"
type="button"
class="btn btn-sm"
:class="`btn-${action.variant ?? 'secondary'}`"
@click="action.onClick"
>{{action.label}}</button>
:disabled="action.isDisabled"
>
<div v-if="action.isPending" class="spinner-border spinner-border-sm"></div>
{{action.label}}
</button>
<a
v-if="action.href"
@ -208,7 +223,11 @@
class="btn btn-sm"
:class="`btn-${action.variant ?? 'secondary'}`"
@click="action.onClick"
>{{action.label}}</a>
v-link-disabled="action.isDisabled ?? false"
>
<div v-if="action.isPending" class="spinner-border spinner-border-sm"></div>
{{action.label}}
</a>
</template>
</div>
</div>

Wyświetl plik

@ -4,10 +4,12 @@
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import type { RouteDestination } from "../facil-map-context-provider/route-form-tab-context";
import DropdownMenu from "./dropdown-menu.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const searchBoxContext = toRef(() => context.components.searchBox);
const routeFormContext = toRef(() => context.components.routeFormTab);
const i18n = useI18n();
const props = defineProps<{
destination: RouteDestination;
@ -33,14 +35,14 @@
v-if="searchBoxContext && routeFormContext && context.settings.search"
:size="props.size"
:isDisabled="props.isDisabled"
label="Use as"
:label="i18n.t('use-as-dropdown.label')"
>
<li>
<a
href="javascript:"
class="dropdown-item"
@click="useAs('from')"
>Route start</a>
>{{i18n.t("use-as-dropdown.from")}}</a>
</li>
<li>
@ -48,7 +50,7 @@
href="javascript:"
class="dropdown-item"
@click="useAs('via')"
>Route via</a>
>{{i18n.t("use-as-dropdown.via")}}</a>
</li>
<li>
@ -56,7 +58,7 @@
href="javascript:"
class="dropdown-item"
@click="useAs('to')"
>Route destination</a>
>{{i18n.t("use-as-dropdown.to")}}</a>
</li>
</DropdownMenu>
</template>

Wyświetl plik

@ -97,6 +97,14 @@
debouncedValidate = pDebounce(validate, props.debounceMs ?? 0);
});
// Memoize validators prop with a shallow equality check, since mostly we specify them using an inline array
const memoizedValidators = ref<Array<Validator<T>>>([]);
watchEffect(() => {
if (props.validators.length !== memoizedValidators.value.length || props.validators.some((v, i) => memoizedValidators.value[i] !== v)) {
memoizedValidators.value = props.validators;
}
});
watchEffect((onCleanup) => {
const abortController = new AbortController();
onCleanup(() => {
@ -106,7 +114,7 @@
try {
toasts.hideToast("fm-validity-error");
const result = (props.debounceMs ? debouncedValidate : validate)(props.value, props.validators, abortController.signal);
const result = (props.debounceMs ? debouncedValidate : validate)(props.value, memoizedValidators.value, abortController.signal);
if (isPromise(result)) {
isValidating.value = true;
const promise = validationErrorPromise.value = result.then((res) => {
@ -116,7 +124,7 @@
}
}).catch((err) => {
if (validationErrorPromise.value === promise) {
toasts.showErrorToast("fm-validity-error", i18n.t("validated-field.validation-error"), err);
toasts.showErrorToast("fm-validity-error", () => i18n.t("validated-field.validation-error"), err);
resolvedValidationError.value = i18n.t("validated-field.validation-error");
isValidating.value = false;
}
@ -127,7 +135,7 @@
isValidating.value = false;
}
} catch (err: any) {
toasts.showErrorToast("fm-validity-error", i18n.t("validated-field.validation-error"), err);
toasts.showErrorToast("fm-validity-error", () => i18n.t("validated-field.validation-error"), err);
validationErrorPromise.value = undefined;
resolvedValidationError.value = i18n.t("validated-field.validation-error");
isValidating.value = false;

Wyświetl plik

@ -11,6 +11,7 @@ import { useToasts } 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 type { Ref } from "vue";
import { getI18n } from "./i18n";
export type MarkerWithTags = Omit<Marker<CRU.CREATE>, "typeId"> & { tags?: Record<string, string> };
export type LineWithTags = Omit<Line<CRU.CREATE>, "typeId"> & { tags?: Record<string, string> };
@ -173,17 +174,18 @@ function showAddConfirmation(context: FacilMapContext, selection: SelectedItem[]
const hiddenObjects = [...markers, ...lines].filter((obj) => !mapContext.value.components.map.fmFilterFunc(obj, client.value.types[obj.typeId])).length;
if (hiddenObjects > 0) {
const title = (
lines.length === 0 ? (markers.length === 1 ? "Marker added" : `${markers.length} markers added`) :
markers.length === 0 ? (lines.length === 1 ? "Line added" : `${lines.length} lines added`) :
`${objects} objects added`
const i18n = getI18n();
const title = () => (
lines.length === 0 ? i18n.t("add.hidden-markers-added-title", { count: markers.length }) :
markers.length === 0 ? i18n.t("add.hidden-lines-added-title", { count: lines.length }) :
i18n.t("add.hidden-objects-added-title", { count: objects })
);
const message = (
lines.length === 0 ? (markers.length === 1 ? "The marker has been added successfully, but the active filter is preventing it from being shown." : `${markers.length} markers have been added successfully, but the active filter is preventing them from being shown.`) :
markers.length === 0 ? (lines.length === 1 ? "The line has been added successfully, but the active filter is preventing it from being shown." : `${lines.length} lines have been added successfully, but the active filter is preventing them from being shown.`) :
objects === hiddenObjects ? `${objects} objects have been added successfully, but the active filter is preventing them from being shown.` :
`${objects} objects have been added successfully, but the active filter is preventing some of them from being shown.`
const message = () => (
lines.length === 0 ? i18n.t("add.hidden-markers-added-message", { count: markers.length }) :
markers.length === 0 ? i18n.t("add.hidden-lines-added-message", { count: lines.length }) :
objects === hiddenObjects ? i18n.t("add.hidden-objects-added-message", { count: objects }) :
i18n.t("add.hidden-objects-added-message-some", { count: objects })
);
const toasts = useToasts(true);

Wyświetl plik

@ -1,40 +1,59 @@
import { addClickListener } from "facilmap-leaflet";
import type { ID, Type } from "facilmap-types";
import type { ID, Point, Type } from "facilmap-types";
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";
import { getI18n } from "./i18n";
import { reactive, ref, toRef } from "vue";
export function drawMarker(type: Type, context: FacilMapContext, toasts: ToastContext): void {
const mapContext = requireMapContext(context);
const clickListener = addClickListener(mapContext.value.components.map, async (point) => {
toasts.hideToast("fm-draw-add-marker");
if (point) {
try {
const isSaving = ref(false);
const create = async (point: Point | undefined) => {
try {
if (point) {
isSaving.value = true;
const selection = await addToMap(context, [
{ marker: { lat: point.lat, lon: point.lon }, type }
]);
mapContext.value.components.selectionHandler.setSelectedItems(selection, true);
} catch (err) {
toasts.showErrorToast("fm-draw-add-marker", "Error adding marker", err);
}
toasts.hideToast("fm-draw-add-marker");
} catch (err) {
toasts.showErrorToast("fm-draw-add-marker", () => getI18n().t("draw.add-marker-error"), err);
}
};
const clickListener = addClickListener(mapContext.value.components.map, async (point) => {
await create(point);
});
toasts.showToast("fm-draw-add-marker", `Add ${formatTypeName(type.name)}`, "Please click on the map to add a marker.", {
toasts.showToast("fm-draw-add-marker", () => getI18n().t("draw.add-marker-title", { typeName: formatTypeName(type.name) }), () => getI18n().t("draw.add-marker-message"), reactive({
noCloseButton: true,
actions: [
{
label: "Cancel",
actions: toRef(() => [
...mapContext.value.location ? [{
label: getI18n().t("draw.add-marker-current"),
onClick: () => {
clickListener.cancel();
}
void create(mapContext.value.location!);
},
isDisabled: isSaving.value
}] : [],
{
label: getI18n().t("draw.add-marker-cancel"),
onClick: () => {
clickListener.cancel();
},
isDisabled: isSaving.value
}
]
});
]),
spinner: isSaving
}));
}
export function moveMarker(markerId: ID, context: FacilMapContext, toasts: ToastContext): void {
@ -50,42 +69,55 @@ export function moveMarker(markerId: ID, context: FacilMapContext, toasts: Toast
mapContext.value.components.map.fire('fmInteractionStart');
mapContext.value.components.markersLayer.lockMarker(markerId);
const finish = async (save: boolean) => {
toasts.hideToast("fm-draw-drag-marker");
const isSaving = ref(false);
const finish = async (pos: Point | undefined) => {
markerLayer.dragging!.disable();
if(save) {
try {
const pos = markerLayer.getLatLng();
await client.value.editMarker({ id: markerId, lat: pos.lat, lon: pos.lng });
} catch (err) {
toasts.showErrorToast("fm-draw-drag-marker", "Error moving marker", err);
try {
if(pos) {
isSaving.value = true;
await client.value.editMarker({ id: markerId, lat: pos.lat, lon: pos.lon });
}
toasts.hideToast("fm-draw-drag-marker");
} catch (err) {
toasts.showErrorToast("fm-draw-drag-marker", () => getI18n().t("draw.move-marker-error"), err);
}
mapContext.value.components.markersLayer.unlockMarker(markerId);
mapContext.value.components.map.fire('fmInteractionEnd');
};
toasts.showToast("fm-draw-drag-marker", "Drag marker", "Drag the marker to reposition it.", {
toasts.showToast("fm-draw-drag-marker", () => getI18n().t("draw.move-marker-title"), getI18n().t("draw.move-marker-message"), reactive({
noCloseButton: true,
actions: [
actions: toRef(() => [
{
label: "Save",
variant: "primary",
label: getI18n().t("draw.move-marker-save"),
variant: "primary" as const,
onClick: () => {
void finish(true);
}
const pos = markerLayer.getLatLng()
void finish({ lat: pos.lat, lon: pos.lng });
},
isPending: isSaving.value,
isDisabled: isSaving.value
},
{
label: "Cancel",
...mapContext.value.location ? [{
label: getI18n().t("draw.move-marker-current"),
onClick: () => {
void finish(false);
}
void finish(mapContext.value.location);
},
isDisabled: isSaving.value
}] : [],
{
label: getI18n().t("draw.move-marker-cancel"),
onClick: () => {
void finish(undefined);
},
isDisabled: isSaving.value
}
]
});
])
}));
markerLayer.dragging!.enable();
}
@ -99,36 +131,47 @@ 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 ${formatTypeName(type.name)}`, "Click on the map to draw a line. Click “Finish” to save it.", {
const isDisabled = ref(true);
const isSaving = ref(false);
toasts.showToast("fm-draw-add-line", () => getI18n().t("draw.add-line-title", { typeName: formatTypeName(type.name) }), () => getI18n().t("draw.add-line-message"), reactive({
noCloseButton: true,
actions: [
actions: toRef(() => [
{
label: "Finish",
variant: "primary",
label: getI18n().t("draw.add-line-finish"),
variant: "primary" as const,
onClick: () => {
mapContext.value.components.linesLayer.endDrawLine(true);
}
},
isDisabled: isDisabled.value || isSaving.value,
isPending: isSaving.value
},
{
label: "Cancel",
label: getI18n().t("draw.add-line-cancel"),
onClick: () => {
mapContext.value.components.linesLayer.endDrawLine(false);
}
},
isDisabled: isSaving.value
}
]
])
}));
const routePoints = await mapContext.value.components.linesLayer.drawLine(lineTemplate, (point, points) => {
isDisabled.value = points.length < 2;
});
const routePoints = await mapContext.value.components.linesLayer.drawLine(lineTemplate);
try {
if (routePoints) {
isSaving.value = true;
toasts.hideToast("fm-draw-add-line");
if (routePoints) {
const selection = await addToMap(context, [
{ line: { routePoints }, type }
]);
mapContext.value.components.selectionHandler.setSelectedItems(selection, true);
const selection = await addToMap(context, [
{ line: { routePoints }, type }
]);
mapContext.value.components.selectionHandler.setSelectedItems(selection, true);
}
} finally {
toasts.hideToast("fm-draw-add-line");
}
} catch (err) {
toasts.showErrorToast("fm-draw-add-line", "Error adding line", err);
toasts.showErrorToast("fm-draw-add-line", getI18n().t("draw.add-line-error"), err);
}
}

Wyświetl plik

@ -1,6 +1,7 @@
import type { Feature, Geometry } from "geojson";
import type { GeoJsonExport, LineFeature, MarkerFeature, SearchResult } from "facilmap-types";
import { flattenObject } from "facilmap-utils";
import { getI18n } from "./i18n";
type FmFeatureProperties = Partial<MarkerFeature["properties"]> | Partial<LineFeature["properties"]>;
type FeatureProperties = FmFeatureProperties & {
@ -27,7 +28,7 @@ async function fileToGeoJSON(file: string): Promise<any> {
const doc = (new window.DOMParser()).parseFromString(file, "text/xml");
const parserErrorElem = doc.getElementsByTagName("parsererror")[0];
if (parserErrorElem) {
throw new Error(`Invalid XML: ${parserErrorElem.textContent}`);
throw new Error(getI18n().t("files.invalid-xml-error", { textContent: parserErrorElem.textContent }));
}
const xml = doc.documentElement;
@ -128,8 +129,5 @@ export async function parseFiles(files: string[]): Promise<FileResultObject> {
}
}
// if(errors)
// return map.messages.showMessage("danger", "Some files could not be parsed.");
return ret;
}

Wyświetl plik

@ -1,11 +1,12 @@
import "leaflet.heightgraph";
import { Control, type Map, Polyline } from "leaflet";
import { Control, Polyline } from "leaflet";
import "leaflet.heightgraph/src/L.Control.Heightgraph.css";
import "./heightgraph.scss";
import type { TrackPoints } from "facilmap-client";
import type { ExtraInfo, TrackPoint } from "facilmap-types";
import { type ExtraInfo, type TrackPoint } from "facilmap-types";
import type { FeatureCollection } from "geojson";
import { calculateDistance, round } from "facilmap-utils";
import { calculateDistance, formatDistance, formatElevation, getCurrentUnits } from "facilmap-utils";
import { getI18n } from "./i18n";
function trackSegment(trackPoints: TrackPoints, fromIdx: number, toIdx: number): TrackPoint[] {
let ret: TrackPoint[] = [];
@ -111,7 +112,26 @@ export function createElevationStats(extraInfo: ExtraInfo | undefined, trackPoin
}
export default class FmHeightgraph extends Control.Heightgraph {
private translatedMapping: Record<"steepness" | "waytypes" | "surface" | "suitablity" | "green" | "noise" | "tollways" | "avgspeed" | "traildifficulty" | "roadaccessrestrictions", string>;
constructor(options?: any) {
// Consume current units in constructor to mark it as a reactive dependency
getCurrentUnits();
const i18n = getI18n();
const translatedMapping: typeof FmHeightgraph["translatedMapping"] = {
steepness: i18n.t("heightgraph.steepness"),
waytypes: i18n.t("heightgraph.waytypes"),
surface: i18n.t("heightgraph.surface"),
suitablity: i18n.t("heightgraph.suitability"),
green: i18n.t("heightgraph.green"),
noise: i18n.t("heightgraph.noise"),
tollways: i18n.t("heightgraph.tollways"),
avgspeed: i18n.t("heightgraph.avgspeed"),
traildifficulty: i18n.t("heightgraph.traildifficulty"),
roadaccessrestrictions: i18n.t("heightgraph.roadaccessrestrictions")
};
super({
margins: {
top: 20,
@ -119,58 +139,65 @@ export default class FmHeightgraph extends Control.Heightgraph {
bottom: 45,
left: 50
},
translation: {
distance: i18n.t("heightgraph.distance"),
elevation: i18n.t("heightgraph.elevation"),
segment_length: i18n.t("heightgraph.segment-length"),
type: i18n.t("heightgraph.type"),
legend: i18n.t("heightgraph.legend")
},
mappings: {
"": {
"": { text: 'unknown', color: '#4682B4' }
"": { text: i18n.t("heightgraph.unknown"), color: '#4682B4' }
},
steepness: {
"-5": { text: "- 16%+", color: "#028306" },
"-4": { text: "- 10-15%", color: "#2AA12E" },
"-3": { text: "- 7-9%", color: "#53BF56" },
"-2": { text: "- 4-6%", color: "#7BDD7E" },
"-1": { text: "- 1-3%", color: "#A4FBA6" },
"0": { text: "0%", color: "#ffcc99" },
"1": { text: "1-3%", color: "#F29898" },
"2": { text: "4-6%", color: "#E07575" },
"3": { text: "7-9%", color: "#CF5352" },
"4": { text: "10-15%", color: "#BE312F" },
"5": { text: "16%+", color: "#AD0F0C" }
[translatedMapping.steepness]: {
"-5": { text: "16% +", color: "#028306" },
"-4": { text: "10–15%", color: "#2AA12E" },
"-3": { text: "7–9%", color: "#53BF56" },
"-2": { text: "4–6%", color: "#7BDD7E" },
"-1": { text: "1–3%", color: "#A4FBA6" },
"0": { text: "0%", color: "#ffcc99" },
"1": { text: "1–3%", color: "#F29898" },
"2": { text: "4–6%", color: "#E07575" },
"3": { text: "7–9%", color: "#CF5352" },
"4": { text: "10–15%", color: "#BE312F" },
"5": { text: "16% +", color: "#AD0F0C" }
},
waytypes: {
"0": { text: "Other", color: "#30959e" },
"1": { text: "StateRoad", color: "#3f9da6" },
"2": { text: "Road", color: "#4ea5ae" },
"3": { text: "Street", color: "#5baeb5" },
"4": { text: "Path", color: "#67b5bd" },
"5": { text: "Track", color: "#73bdc4" },
"6": { text: "Cycleway", color: "#7fc7cd" },
"7": { text: "Footway", color: "#8acfd5" },
"8": { text: "Steps", color: "#96d7dc" },
"9": { text: "Ferry", color: "#a2dfe5" },
"10": { text: "Construction", color: "#ade8ed" }
[translatedMapping.waytypes]: {
"0": { text: i18n.t("heightgraph.waytype-other"), color: "#30959e" },
"1": { text: i18n.t("heightgraph.waytype-state-road"), color: "#3f9da6" },
"2": { text: i18n.t("heightgraph.waytype-road"), color: "#4ea5ae" },
"3": { text: i18n.t("heightgraph.waytype-street"), color: "#5baeb5" },
"4": { text: i18n.t("heightgraph.waytype-path"), color: "#67b5bd" },
"5": { text: i18n.t("heightgraph.waytype-track"), color: "#73bdc4" },
"6": { text: i18n.t("heightgraph.waytype-cycleway"), color: "#7fc7cd" },
"7": { text: i18n.t("heightgraph.waytype-footway"), color: "#8acfd5" },
"8": { text: i18n.t("heightgraph.waytype-steps"), color: "#96d7dc" },
"9": { text: i18n.t("heightgraph.waytype-ferry"), color: "#a2dfe5" },
"10": { text: i18n.t("heightgraph.waytype-construction"), color: "#ade8ed" }
},
surface: {
"0": { text: "Other", color: "#ddcdeb" },
"1": { text: "Paved", color: "#cdb8df" },
"2": { text: "Unpaved", color: "#d2c0e3" },
"3": { text: "Asphalt", color: "#bca4d3" },
"4": { text: "Concrete", color: "#c1abd7" },
"5": { text: "Cobblestone", color: "#c7b2db" },
"6": { text: "Metal", color: "#e8dcf3" },
"7": { text: "Wood", color: "#eee3f7" },
"8": { text: "Compacted Gravel", color: "#d8c6e7" },
"9": { text: "Fine Gravel", color: "#8f9de4" },
"10": { text: "Gravel", color: "#e3d4ef" },
"11": { text: "Dirt", color: "#99a6e7" },
"12": { text: "Ground", color: "#a3aeeb" },
"13": { text: "Ice", color: "#acb6ee" },
"14": { text: "Paving Stones", color: "#b6c0f2" },
"15": { text: "Sand", color: "#c9d1f8" },
"16": { text: "Woodchips", color: "#c0c8f5" },
"17": { text: "Grass", color: "#d2dafc" },
"18": { text: "Grass Paver", color: "#dbe3ff" }
[translatedMapping.surface]: {
"0": { text: i18n.t("heightgraph.surface-other"), color: "#ddcdeb" },
"1": { text: i18n.t("heightgraph.surface-paved"), color: "#cdb8df" },
"2": { text: i18n.t("heightgraph.surface-unpaved"), color: "#d2c0e3" },
"3": { text: i18n.t("heightgraph.surface-asphalt"), color: "#bca4d3" },
"4": { text: i18n.t("heightgraph.surface-contrete"), color: "#c1abd7" },
"5": { text: i18n.t("heightgraph.surface-cobblestone"), color: "#c7b2db" },
"6": { text: i18n.t("heightgraph.surface-metal"), color: "#e8dcf3" },
"7": { text: i18n.t("heightgraph.surface-wood"), color: "#eee3f7" },
"8": { text: i18n.t("heightgraph.surface-compacted-gravel"), color: "#d8c6e7" },
"9": { text: i18n.t("heightgraph.surface-fine-gravel"), color: "#8f9de4" },
"10": { text: i18n.t("heightgraph.surface-gravel"), color: "#e3d4ef" },
"11": { text: i18n.t("heightgraph.surface-dirt"), color: "#99a6e7" },
"12": { text: i18n.t("heightgraph.surface-ground"), color: "#a3aeeb" },
"13": { text: i18n.t("heightgraph.surface-ice"), color: "#acb6ee" },
"14": { text: i18n.t("heightgraph.surface-paving-stones"), color: "#b6c0f2" },
"15": { text: i18n.t("heightgraph.surface-sand"), color: "#c9d1f8" },
"16": { text: i18n.t("heightgraph.surface-woodchips"), color: "#c0c8f5" },
"17": { text: i18n.t("heightgraph.surface-grass"), color: "#d2dafc" },
"18": { text: i18n.t("heightgraph.surface-grass-paver"), color: "#dbe3ff" }
},
suitability: {
[translatedMapping.suitability]: {
"3": { text: "3/10", color: "#3D3D3D" },
"4": { text: "4/10", color: "#4D4D4D" },
"5": { text: "5/10", color: "#5D5D5D" },
@ -180,7 +207,7 @@ export default class FmHeightgraph extends Control.Heightgraph {
"9": { text: "9/10", color: "#9D9D9D" },
"10": { text: "10/10", color: "#ADADAD" }
},
green: {
[translatedMapping.green]: {
"3": { text: "10/10", color: "#8ec639" },
"4": { text: "9/10", color: "#99c93c" },
"5": { text: "8/10", color: "#a4cc40" },
@ -190,39 +217,40 @@ export default class FmHeightgraph extends Control.Heightgraph {
"9": { text: "4/10", color: "#d1d84e" },
"10": { text: "3/10", color: "#dcdc51" }
},
noise: {
[translatedMapping.noise]: {
"7": { text: "7/10", color: "#F8A056" },
"8": { text: "8/10", color: "#EA7F27" },
"9": { text: "9/10", color: "#A04900" },
"10": { text: "10/10", color: "#773600" }
},
tollways: {
"0": { text: "LOCALE_NO_TOLLWAY", color: "#6ca97b" },
"1": { text: "LOCALE_TOLLWAY", color: "#ffb347" }
[translatedMapping.tollways]: {
"0": { text: i18n.t("heightgraph.tollway-no"), color: "#6ca97b" },
"1": { text: i18n.t("heightgraph.tollway-yes"), color: "#ffb347" }
},
avgspeed: {
"3": { text: "3 km/h", color: "#f2fdff" },
"4": { text: "4 km/h", color: "#D8FAFF" },
"5": { text: "5 km/h", color: "bff7ff" },
"6": { text: "6-8 km/h", color: "#f2f7ff" },
"9": { text: "9-12 km/h", color: "#d8e9ff" },
"13": { text: "13-16 km/h", color: "#bedaff" },
"17": { text: "17-20 km/h", color: "#a5cbff" },
"21": { text: "21-24 km/h", color: "#8cbcff" },
"25": { text: "25-29 km/h", color: "#72aeff" },
"30": { text: "30-34 km/h", color: "#599fff" },
"35": { text: "35-39 km/h", color: "#3f91ff" },
"40": { text: "40-44 km/h", color: "#2682ff" },
"45": { text: "45-49 km/h", color: "#0d73ff" },
"50": { text: "50-59 km/h", color: "#0067f2" },
"60": { text: "60-69 km/h", color: "#005cd9" },
"70": { text: "70-79 km/h", color: "#0051c0" },
"80": { text: "80-99 km/h", color: "#0046a6" },
"100": { text: "100-119 km/h", color: "#003c8d" },
"120": { text: "+120 km/h", color: "#003174" }
[translatedMapping.avgspeed]: {
// TODO: Make these available in miles
"3": { text: "3km/h", color: "#f2fdff" },
"4": { text: "4km/h", color: "#D8FAFF" },
"5": { text: "5km/h", color: "bff7ff" },
"6": { text: "6–8km/h", color: "#f2f7ff" },
"9": { text: "9–12km/h", color: "#d8e9ff" },
"13": { text: "13–16km/h", color: "#bedaff" },
"17": { text: "17–20km/h", color: "#a5cbff" },
"21": { text: "21–24km/h", color: "#8cbcff" },
"25": { text: "25–29km/h", color: "#72aeff" },
"30": { text: "30–34km/h", color: "#599fff" },
"35": { text: "35–39km/h", color: "#3f91ff" },
"40": { text: "40–44km/h", color: "#2682ff" },
"45": { text: "45–49km/h", color: "#0d73ff" },
"50": { text: "50–59km/h", color: "#0067f2" },
"60": { text: "60–69km/h", color: "#005cd9" },
"70": { text: "70–79km/h", color: "#0051c0" },
"80": { text: "80–99km/h", color: "#0046a6" },
"100": { text: "100–119km/h", color: "#003c8d" },
"120": { text: "+120km/h", color: "#003174" }
},
traildifficulty: {
"0": { text: "Missing SAC tag", color: "#dfecec" },
[translatedMapping.traildifficulty]: {
"0": { text: i18n.t("heightgraph.traildifficulty-unknown"), color: "#dfecec" },
"1": { text: "S0", color: "#9fc6c6" },
"2": { text: "S1", color: "#80b3b3" },
"3": { text: "S2", color: "#609f9f" },
@ -231,43 +259,26 @@ export default class FmHeightgraph extends Control.Heightgraph {
"6": { text: "S5", color: "#264040" },
"7": { text: ">S5", color: "#132020" }
},
roadaccessrestrictions: {
"0": { text: "None (there are no restrictions)", color: "#fe7f6c" },
"1": { text: "No", color: "#FE7F9C" },
"2": { text: "Customers", color: "#FDAB9F" },
"4": { text: "Destination", color: "#FF66CC" },
"8": { text: "Delivery", color: "#FDB9C8" },
"16": { text: "Private", color: "#F64A8A" },
"32": { text: "Permissive", color: "#E0115F" }
[translatedMapping.roadaccessrestrictions]: {
"0": { text: i18n.t("heightgraph.access-yes"), color: "#fe7f6c" },
"1": { text: i18n.t("heightgraph.access-no"), color: "#FE7F9C" },
"2": { text: i18n.t("heightgraph.access-customers"), color: "#FDAB9F" },
"4": { text: i18n.t("heightgraph.access-destination"), color: "#FF66CC" },
"8": { text: i18n.t("heightgraph.access-delivery"), color: "#FDB9C8" },
"16": { text: i18n.t("heightgraph.access-private"), color: "#F64A8A" },
"32": { text: i18n.t("heightgraph.access-permissive"), color: "#E0115F" }
}
},
...options
});
for (const i of Object.keys(this.options.mappings)) {
for (const j of Object.keys(this.options.mappings[i])) {
this.options.mappings[i][j].originalText = this.options.mappings[i][j].text;
}
}
}
onAdd(map: Map): Element {
// Initialize renderer on overlay pane because Heightgraph renders the hover overlay there (it appends it to .leaflet-overlay-pane svg)
map.getRenderer(new Polyline([]));
return super.onAdd(map);
this.translatedMapping = translatedMapping;
}
addData(extraInfo: ExtraInfo | undefined, trackPoints: TrackPoints): void {
let data = createGeoJsonForHeightGraph(extraInfo, trackPoints);
const translatedExtraInfo = extraInfo && Object.fromEntries(Object.entries(extraInfo).map(([k, v]) => [(this.translatedMapping as any)[k] ?? k, v]));
for (const featureCollection of data) {
for (const i in featureCollection.properties.distances) {
const mapping = this.options.mappings[featureCollection.properties.summary] && this.options.mappings[featureCollection.properties.summary][i];
if (mapping)
mapping.text = mapping.originalText + " (" + round(featureCollection.properties.distances[i], 2) + " km)";
}
}
const data = createGeoJsonForHeightGraph(translatedExtraInfo, trackPoints);
if(this._container)
super.addData(data);
@ -275,4 +286,72 @@ export default class FmHeightgraph extends Control.Heightgraph {
this._data = data;
}
_internalMousemoveHandler(...args: any[]): void {
super._internalMousemoveHandler(...args);
// Hack: Replace distance, elevation, segment length kilometers/meters with configured unit
const dist = this._distTspan.text().match(/(\d+(\.\d+)) km/);
if (dist) {
this._distTspan.text(` ${formatDistance(Number(dist[1]))}`);
}
const alt = this._altTspan.text().match(/(\d+(\.\d+)) m/);
if (alt) {
this._altTspan.text(` ${formatElevation(Number(alt[1]))}`);
}
const area = this._areaTspan.text().match(/(\d+(\.\d+)) km/);
if (area) {
this._areaTspan.text(` ${formatDistance(Number(area[1]))}`);
}
}
_appendScales(): void {
super._appendScales();
// Hack: Replace distance/elevation kilometers/meters in x/y axis labels with configured unit.
// Steps are still according to round numbers of kilometers/meters, but at least the units are right.
this._xAxis.tickFormat((d: number) => formatDistance(d));
this._yAxis.tickFormat((d: number) => formatElevation(d));
}
_prepareData(): void {
super._prepareData();
// Hack: Append the total distance for each result type to the legend
for (let i = 0; i < this._categories.length; i++) {
const category = this._categories[i];
const featureCollection = this._data[i];
category.legend = Object.fromEntries(Object.entries(category.legend).map(([k, v]: [any, any]) => [k, {
...v,
text: getI18n().t("heightgraph.label-with-total", { label: v.text, total: formatDistance(featureCollection.properties.distances[v.type]) })
}]));
}
}
_showMapMarker(...args: any[]): void {
// Heightgraph renders the map marker (when hovering the heightgraph) to the hard-coded element .leaflet-overlay-pane svg
// First, we need to initialize the renderer to make sure that the element is even there
this._map.getRenderer(new Polyline([], { pane: this.options.mapMarkerPane }));
// Then, we temporarily change class names to make the desired pane (our custom option mapMarkerPane) match the hard-coded class name
if (this.options.mapMarkerPane) {
const overlayPane = this._map.getPane("overlayPane");
const overlayPaneClass = overlayPane.className;
const mapMarkerPane = this._map.getPane(this.options.mapMarkerPane);
const mapMarkerPaneClass = mapMarkerPane.className;
overlayPane.classList.remove("leaflet-overlay-pane");
mapMarkerPane.classList.add("leaflet-overlay-pane");
try {
super._showMapMarker(...args);
} finally {
overlayPane.className = overlayPaneClass;
mapMarkerPane.className = mapMarkerPaneClass;
}
} else {
super._showMapMarker(...args);
}
}
}

Wyświetl plik

@ -5,7 +5,7 @@ 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 { LANG_COOKIE, LANG_QUERY, decodeQueryString, getAcceptHotI18n, getRawI18n, onI18nReady, setCurrentUnitsGetter } from "facilmap-utils";
import { cookies } from "./cookies";
import { unitsValidator } from "facilmap-types";
@ -25,7 +25,7 @@ if (import.meta.hot) {
import.meta.hot!.accept(`../../i18n/ru.json`, getAcceptHotI18n("ru", namespace));
}
const i18nResourceChangeCounter = ref(0);
export const i18nResourceChangeCounter = ref(0);
const onI18nResourceChange = () => {
i18nResourceChangeCounter.value++;
};
@ -45,10 +45,13 @@ onI18nReady((i18n) => {
} as any;
});
const UNITS_COOKIE = "units";
const UNITS_QUERY = "units";
setCurrentUnitsGetter(() => {
const queryParams = decodeQueryString(location.search);
const query = queryParams.format ? unitsValidator.safeParse(queryParams.format) : undefined;
return query?.success ? query.data : cookies.units;
const query = queryParams.format ? unitsValidator.safeParse(queryParams[UNITS_QUERY]) : undefined;
return query?.success ? query.data : cookies[UNITS_COOKIE];
});
export function getI18n(): {
@ -93,4 +96,14 @@ export const T = defineComponent({
});
};
}
});
});
export function isLanguageExplicit(): boolean {
const queryParams = decodeQueryString(location.search);
return !!queryParams[LANG_QUERY] || !!queryParams[LANG_COOKIE];
}
export function isUnitsExplicit(): boolean {
const queryParams = decodeQueryString(location.search);
return !!queryParams[UNITS_QUERY] || !!queryParams[UNITS_COOKIE];
}

Wyświetl plik

@ -1,6 +1,7 @@
import type { Emitter } from "mitt";
import { type DeepReadonly, type Ref, watchEffect, toRef, effectScope } from "vue";
import type * as z from "zod";
import { getI18n } from "./i18n";
// https://stackoverflow.com/a/62085569/242365
export type DistributedKeyOf<T> = T extends any ? keyof T : never;
@ -88,7 +89,7 @@ export function validations<V>(val: V, funcs: Array<(val: V) => string | undefin
export function validateRequired(val: any): string | undefined {
if (val == null || val === "") {
return "Must not be empty.";
return getI18n().t("utils.required-error");
}
}

Wyświetl plik

@ -74,11 +74,16 @@ const Root = defineComponent({
settings: {
toolbox: toBoolean(queryParams.toolbox, true),
search: toBoolean(queryParams.search, true),
route: toBoolean(queryParams.route, true),
pois: toBoolean(queryParams.pois, true),
locate: toBoolean(queryParams.locate, true),
autofocus: toBoolean(queryParams.autofocus, parent === window),
legend: toBoolean(queryParams.legend, true),
interactive: toBoolean(queryParams.interactive, parent === window),
linkLogo: parent !== window,
updateHash: true
updateHash: true,
routing: config.supportsRoutes,
advancedRouting: config.supportsAdvancedRoutes
},
"onUpdate:padId": (v) => padId.value = v,
"onUpdate:padName": (v) => padName.value = v

Wyświetl plik

@ -41,7 +41,7 @@
</head>
<body>
<div class="container-fluid">
<h1><%=normalizePadName(padData.name)%> – <%=appName%></h1>
<h1><%=normalizePageTitle(normalizePadName(padData.name), appName)%></h1>
<%
for (const type of types) {
-%>

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-integration-tests",
"version": "4.0.0",
"version": "4.1.0",
"private": true,
"type": "module",
"homepage": "https://github.com/FacilMap/facilmap",
@ -24,14 +24,14 @@
"facilmap-types": "workspace:^",
"facilmap-utils": "workspace:^",
"lodash-es": "^4.17.21",
"socket.io-client": "^4.7.4",
"vitest": "^1.3.1"
"socket.io-client": "^4.7.5",
"vitest": "^1.4.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-plugin-dts": "^3.7.3",
"vite-tsconfig-paths": "^4.3.1"
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-dts": "^3.8.1",
"vite-tsconfig-paths": "^4.3.2"
}
}

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-leaflet",
"version": "4.0.0",
"version": "4.1.0",
"description": "Utilities to show FacilMap objects on a Leaflet map.",
"keywords": [
"maps",
@ -31,7 +31,7 @@
],
"scripts": {
"build": "vite build",
"clean": "rimraf dist",
"clean": "rimraf dist out out.node",
"dev-server": "vite",
"download-icons": "tsx ./download-icons.ts",
"check-types": "tsc -b --emitDeclarationOnly",
@ -52,27 +52,27 @@
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@fortawesome/fontawesome-free": "^6.5.2",
"@types/geojson": "^7946.0.14",
"@types/leaflet": "^1.9.8",
"@types/leaflet": "^1.9.9",
"@types/leaflet.markercluster": "^1.5.4",
"@types/lodash-es": "^4.17.12",
"@types/node-fetch": "^2.6.11",
"@types/yauzl-promise": "^4.0.0",
"cheerio": "^1.0.0-rc.12",
"fast-glob": "^3.3.2",
"happy-dom": "^13.6.2",
"happy-dom": "^14.7.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.5",
"rollup": "^4.12.1",
"rollup": "^4.14.1",
"svgo": "^3.2.0",
"tsx": "^4.7.1",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-plugin-css-injected-by-js": "^3.4.0",
"vite-plugin-dts": "^3.7.3",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.1",
"tsx": "^4.7.2",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-dts": "^3.8.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.4.0",
"yauzl-promise": "^4.0.0"
},
"peerDependencies": {

Wyświetl plik

@ -1,4 +1,34 @@
{
"layers": {
"lima-name": "Lima Labs",
"lima-attribution": "© [Lima Labs](https://maps.lima-labs.com/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"mpnk-name": "Mapnik",
"mpnk-attribution": "© [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"topl-name": "TopPlus",
"topl-attribution": "© [Bundesamt für Kartographie und Geodäsie](https://www.bkg.bund.de/) {{year}}",
"map1-name": "Map1.eu",
"map1-attribution": "© [Map1.eu](http://map1.eu/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"topo-name": "OpenTopoMap",
"topo-attribution": "© [OpenTopoMap](https://opentopomap.org/) ([CC-BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"cyco-name": "CyclOSM",
"cyco-attribution": "© [CyclOSM](https://www.cyclosm.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"ocyc-name": "OpenCycleMap",
"ocyc-attribution": "© [OpenCycleMap](https://opencyclemap.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"hobi-name": "Hike & Bike Map",
"hobi-attribution": "© [Hike & Bike Map](http://hikebikemap.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"mpnw-name": "Mapnik Water",
"mpnw-attribution": "© [FreieTonne](https://www.freietonne.de/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"optm-name": "ÖPNV",
"optm-attribution": "© [OpenPTMap](http://openptmap.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"hike-name": "Wanderrouten",
"hike-attribution": "© [Waymarked Trails](https://hiking.waymarkedtrails.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"bike-name": "Fahrradrouten",
"bike-attribution": "© [Waymarked Trails](https://cycling.waymarkedtrails.org/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)",
"rlie-name": "Relief",
"grid-name": "Gradnetz",
"frto-name": "Schifffahrtszeichen",
"frto-attribution": "© [FreieTonne](https://www.freietonne.de/) / [OSM-Mitwirkende](https://www.openstreetmap.org/copyright/de)"
},
"overpass-presets": {
"category-amenities": "Infrastruktur",
"atm": "Geldautomat",
@ -33,6 +63,7 @@
"hindu": "Hinduistischer Tempel",
"synagogue": "Synagoge",
"cemetery": "Friedhof",
"amenities": "Alle Infrastruktur",
"category-tourism": "Tourismus",
"abandoned": "Verlassen",
"artscentre": "Kunstzentrum",
@ -49,6 +80,8 @@
"museum": "Museum",
"nudism": "FKK",
"picnic": "Picknick",
"sauna": "Sauna",
"spa": "Wellness",
"statue": "Statue",
"themepark": "Freizeitpark",
"viewpoint": "Aussichtspunkt",
@ -56,8 +89,6 @@
"windmill": "Windmühle",
"watermill": "Wassermühle",
"zoo": "Zoo",
"tourism": "Tourism=yes",
"category-hotels": "Hotels",
"alpinehut": "Alpenhütte",
"apartment": "Ferienwohnung",
"campsite": "Campingplatz",
@ -66,8 +97,7 @@
"hostel": "Hostel",
"hotel": "Hotel",
"motel": "Motel",
"spa": "Wellness",
"sauna": "Sauna",
"tourism": "Alle touristischen Orte",
"category-sports": "Sport",
"americanfootball": "American Football",
"baseball": "Baseball",
@ -85,7 +115,22 @@
"tabletennis": "Tischtennis",
"tennis": "Tennis",
"volleyball": "Volleyball",
"sports": "Alle Sportstätten",
"category-shops": "Geschäfte",
"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",
"beautyshop": "Kosmetiksalon",
"bicycleshop": "Fahrrad",
"bookshop": "Bücher",
@ -98,7 +143,7 @@
"diyshop": "Baumarkt",
"florist": "Blumen",
"gardencentre": "Gartencenter",
"generalshop": "Allgemein",
"generalshop": "Gemischtwaren",
"giftshop": "Geschenke",
"hairdresser": "Friseur",
"jewelleryshop": "Juwellier",
@ -114,21 +159,7 @@
"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",
"shops": "Alle Geschäfte",
"category-restaurants": "Restaurants",
"bar": "Bar",
"bbq": "Barbecue",
@ -159,5 +190,8 @@
"village": "Dorf",
"hamlet": "Weiler",
"suburb": "Stadtteil"
},
"search-result-geojson": {
"invalid-geojson-error": "Ungültiges GeoJSON-Objekt."
}
}

Wyświetl plik

@ -1,4 +1,34 @@
{
"layers": {
"lima-name": "Lima Labs",
"lima-attribution": "© [Lima Labs](https://maps.lima-labs.com/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"mpnk-name": "Mapnik",
"mpnk-attribution": "© [OSM Contributors](https://www.openstreetmap.org/copyright)",
"topl-name": "TopPlus",
"topl-attribution": "© [Bundesamt für Kartographie und Geodäsie](https://www.bkg.bund.de/) {{year}}",
"map1-name": "Map1.eu",
"map1-attribution": "© [Map1.eu](http://map1.eu/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"topo-name": "OpenTopoMap",
"topo-attribution": "© [OpenTopoMap](https://opentopomap.org/) ([CC-BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"cyco-name": "CyclOSM",
"cyco-attribution": "© [CyclOSM](https://www.cyclosm.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"ocyc-name": "OpenCycleMap",
"ocyc-attribution": "© [OpenCycleMap](https://opencyclemap.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"hobi-name": "Hike & Bike Map",
"hobi-attribution": "© [Hike & Bike Map](http://hikebikemap.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"mpnw-name": "Mapnik Water",
"mpnw-attribution": "© [FreieTonne](https://www.freietonne.de/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"optm-name": "Public transportation",
"optm-attribution": "© [OpenPTMap](http://openptmap.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"hike-name": "Hiking paths",
"hike-attribution": "© [Waymarked Trails](https://hiking.waymarkedtrails.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"bike-name": "Bicycle routes",
"bike-attribution": "© [Waymarked Trails](https://cycling.waymarkedtrails.org/) / [OSM Contributors](https://www.openstreetmap.org/copyright)",
"rlie-name": "Relief",
"grid-name": "Graticule",
"frto-name": "Sea marks",
"frto-attribution": "© [FreieTonne](https://www.freietonne.de/) / [OSM Contributors](https://www.openstreetmap.org/copyright)"
},
"overpass-presets": {
"category-amenities": "Amenities",
"atm": "ATM",
@ -33,6 +63,7 @@
"hindu": "Hindu Temple",
"synagogue": "Synagogue",
"cemetery": "Cemetery",
"amenities": "All amenities",
"category-tourism": "Tourism",
"abandoned": "Abandoned",
"artscentre": "Arts centre",
@ -49,6 +80,8 @@
"museum": "Museum",
"nudism": "Nudism",
"picnic": "Picnic",
"sauna": "Sauna",
"spa": "Spa",
"statue": "Statue",
"themepark": "Theme park",
"viewpoint": "Viewpoint",
@ -56,8 +89,6 @@
"windmill": "Windmill",
"watermill": "Watermill",
"zoo": "Zoo",
"tourism": "Tourism=yes",
"category-hotels": "Hotels",
"alpinehut": "Alpine hut",
"apartment": "Apartment",
"campsite": "Camp site",
@ -66,8 +97,6 @@
"hostel": "Hostel",
"hotel": "Hotel",
"motel": "Motel",
"spa": "Spa",
"sauna": "Sauna",
"category-sports": "Sports",
"americanfootball": "American football",
"baseball": "Baseball",
@ -85,7 +114,23 @@
"tabletennis": "Table tennis",
"tennis": "Tennis",
"volleyball": "Volleyball",
"tourism": "All touristic places",
"sports": "All sports facilities",
"category-shops": "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",
"beautyshop": "Beauty",
"bicycleshop": "Bicycle",
"bookshop": "Books/Stationary",
@ -114,21 +159,7 @@
"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",
"shops": "All shops",
"category-restaurants": "Restaurants",
"bar": "Bar",
"bbq": "BBQ",
@ -159,5 +190,8 @@
"village": "Village",
"hamlet": "Hamlet",
"suburb": "Suburb"
},
"search-result-geojson": {
"invalid-geojson-error": "Invalid GeoJSON object."
}
}

Wyświetl plik

@ -1,6 +1,8 @@
import { Layer, Map, tileLayer, type TileLayer } from "leaflet";
import AutoGraticule from "leaflet-auto-graticule";
import FreieTonne from "leaflet-freie-tonne";
import { getI18n } from "./utils/i18n";
import { markdownInline } from "facilmap-utils";
export const defaultVisibleLayers: VisibleLayers = {
get baseLayer() {
@ -14,22 +16,33 @@ export interface Layers {
overlays: Record<string, Layer>;
}
function getter<P1 extends keyof any, P2 extends keyof any, R>(prop: P1, getProp: P2, get: () => R): Record<P1, R> & Record<P2, () => R> {
return {
[prop]: get(),
[getProp]: get
} as any;
}
const fmName = (get: () => string) => getter("fmName", "fmGetName", get);
const attribution = (get: () => string) => getter("attribution", "fmGetAttribution", () => markdownInline(get(), true));
const fixAttribution = <T extends Layer>(layer: T): T => Object.assign(layer, { getAttribution(this: any) { return this.options.fmGetAttribution()!; } }) as any;
export function createDefaultLayers(): Layers & { fallbackLayer: string | undefined } {
return {
baseLayers: {
...(layerOptions.limaLabsToken ? {
Lima: tileLayer(`https://cdn.lima-labs.com/{z}/{x}/{y}.png?api=${encodeURIComponent(layerOptions.limaLabsToken)}`, {
fmName: "Lima Labs",
attribution: '© <a href="https://maps.lima-labs.com/" target="_blank">Lima Labs</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Lima: fixAttribution(tileLayer(`https://cdn.lima-labs.com/{z}/{x}/{y}.png?api=${encodeURIComponent(layerOptions.limaLabsToken)}`, {
...fmName(() => getI18n().t("layers.lima-name")),
...attribution(() => getI18n().t("layers.lima-attribution")),
noWrap: true
})
}))
} : {}),
Mpnk: tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
fmName: "Mapnik",
attribution: '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Mpnk: fixAttribution(tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.mpnk-name")),
...attribution(() => getI18n().t("layers.mpnk-attribution")),
noWrap: true
}),
})),
/*MSfR: tileLayer('https://maps.heigit.org/openmapsurfer/tiles/roads/webmercator/{z}/{x}/{y}.png', {
fmName: "MapSurfer Road",
@ -37,88 +50,89 @@ export function createDefaultLayers(): Layers & { fallbackLayer: string | undefi
noWrap: true
})*/
ToPl: tileLayer("https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png", {
fmName: "TopPlus",
attribution: '© <a href="https://www.bkg.bund.de/">Bundesamt für Kartographie und Geodäsie</a> ' + (new Date()).getFullYear(),
ToPl: fixAttribution(tileLayer("https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png", {
...fmName(() => getI18n().t("layers.topl-name")),
...attribution(() => getI18n().t("layers.topl-attribution", { year: new Date().getFullYear() })),
noWrap: true
}),
})),
Map1: tileLayer("http://beta.map1.eu/tiles/{z}/{x}/{y}.jpg", {
fmName: "Map1.eu",
attribution: '© <a href="http://map1.eu/" target="_blank">Map1.eu</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Map1: fixAttribution(tileLayer("http://beta.map1.eu/tiles/{z}/{x}/{y}.jpg", {
...fmName(() => getI18n().t("layers.map1-name")),
...attribution(() => getI18n().t("layers.map1-attribution")),
noWrap: true
}),
})),
Topo: tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
fmName: "OpenTopoMap",
attribution: '© <a href="https://opentopomap.org/" target="_blank">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/" target="_blank">CC-BY-SA</a>) / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Topo: fixAttribution(tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.topo-name")),
...attribution(() => getI18n().t("layers.topo-attribution")),
noWrap: true
}),
})),
CycO: tileLayer("https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png", {
fmName: "CyclOSM",
attribution: '© <a href="https://www.cyclosm.org/" target="_blank">CyclOSM</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
CycO: fixAttribution(tileLayer("https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.cyco-name")),
...attribution(() => getI18n().t("layers.cyco-attribution")),
noWrap: true
}),
})),
OCyc: tileLayer("https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=bc74ceb5f91c448b9615f9b576c61c16", {
fmName: "OpenCycleMap",
attribution: '© <a href="https://opencyclemap.org/" target="_blank">OpenCycleMap</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
OCyc: fixAttribution(tileLayer("https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=bc74ceb5f91c448b9615f9b576c61c16", {
...fmName(() => getI18n().t("layers.ocyc-name")),
...attribution(() => getI18n().t("layers.ocyc-attribution")),
noWrap: true
}),
})),
HiBi: tileLayer("https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png", {
fmName: "Hike & Bike Map",
attribution: '© <a href="http://hikebikemap.org/" target="_blank">Hike &amp; Bike Map</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
HiBi: fixAttribution(tileLayer("https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.hobi-name")),
...attribution(() => getI18n().t("layers.hobi-attribution")),
noWrap: true
}),
})),
MpnW: tileLayer("http://ftdl.de/tile-cache/tiles/{z}/{x}/{y}.png", {
fmName: "Mapnik Water",
attribution: '© <a href="https://www.freietonne.de/" target="_blank">FreieTonne</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
MpnW: fixAttribution(tileLayer("http://ftdl.de/tile-cache/tiles/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.mpnw-name")),
...attribution(() => getI18n().t("layers.mpnw-attribution")),
noWrap: true
}),
})),
},
overlays: {
OPTM: tileLayer("http://openptmap.org/tiles/{z}/{x}/{y}.png", {
fmName: "Public transportation",
attribution: '© <a href="http://openptmap.org/" target="_blank">OpenPTMap</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
OPTM: fixAttribution(tileLayer("http://openptmap.org/tiles/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.optm-name")),
...attribution(() => getI18n().t("layers.optm-attribution")),
zIndex: 300,
noWrap: true
}),
})),
Hike: tileLayer("https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png", {
fmName: "Hiking paths",
attribution: '© <a href="https://hiking.waymarkedtrails.org/" target="_blank">Waymarked Trails</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Hike: fixAttribution(tileLayer("https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.hike-name")),
...attribution(() => getI18n().t("layers.hike-attribution")),
zIndex: 300,
noWrap: true
}),
})),
Bike: tileLayer("https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png", {
fmName: "Bicycle routes",
attribution: '© <a href="https://cycling.waymarkedtrails.org/" target="_blank">Waymarked Trails</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a>',
Bike: fixAttribution(tileLayer("https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png", {
...fmName(() => getI18n().t("layers.bike-name")),
...attribution(() => getI18n().t("layers.bike-attribution")),
zIndex: 300,
noWrap: true
}),
})),
Rlie: tileLayer("https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png", {
Rlie: fixAttribution(tileLayer("https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png", {
maxZoom: 16,
fmName: "Relief",
...fmName(() => getI18n().t("layers.rlie-name")),
zIndex: 300,
noWrap: true
}),
})),
grid: new AutoGraticule({
fmName: "Graticule",
...fmName(() => getI18n().t("layers.grid-name")),
zIndex: 300,
noWrap: true
}),
FrTo: new FreieTonne({
fmName: "Sea marks",
FrTo: fixAttribution(new FreieTonne({
...fmName(() => getI18n().t("layers.frto-name")),
...attribution(() => getI18n().t("layers.frto-attribution")),
zIndex: 300,
noWrap: true
})
}))
},
fallbackLayer: 'Mpnk'
};

Wyświetl plik

@ -185,7 +185,7 @@ export default class LinesLayer extends FeatureGroup {
protected _endDrawLine?: (save: boolean) => void;
drawLine(lineTemplate: LineTemplate): Promise<Point[] | undefined> {
drawLine(lineTemplate: LineTemplate, onAddPoint?: (point: Point, points: Point[]) => void): Promise<Point[] | undefined> {
return new Promise<Point[] | undefined>((resolve) => {
const line: Line & { trackPoints: BasicTrackPoints } = {
id: -1,
@ -215,6 +215,8 @@ export default class LinesLayer extends FeatureGroup {
line.routePoints.push(pos); // Will be updated by handleMouseMove
this._addLine(line);
handler = addClickListener(this._map, handleClick, handleMouseMove);
onAddPoint?.(pos, routePoints);
};
const handleClick = (pos?: Point) => {

Wyświetl plik

@ -138,6 +138,13 @@ export default class MarkersLayer extends MarkerCluster {
*/
lockMarker(id: ID): void {
this.lockedMarkerIds.add(id);
// Remove marker from cluster and add directly to map
const markerLayer = this.getLayerByMarkerId(id);
if (markerLayer) {
this.removeLayer(markerLayer);
this._map.addLayer(markerLayer);
}
}
/**
@ -146,6 +153,13 @@ export default class MarkersLayer extends MarkerCluster {
unlockMarker(id: ID): void {
this.lockedMarkerIds.delete(id);
// Move marker back into cluster
const markerLayer = this.getLayerByMarkerId(id);
if (markerLayer) {
this._map.removeLayer(markerLayer);
this.addLayer(markerLayer);
}
if (this.shouldShowMarker(this.client.markers[id]))
this._addMarker(this.client.markers[id]);
else

Wyświetl plik

@ -55,6 +55,8 @@ export function getAllOverpassPresets(): OverpassPresetCategory[] {
{ 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") }
], [
{ key: "amenities", query: "nwr[amenity]", label: i18n.t("overpass-presets.amenities") }
]
]
},
@ -79,21 +81,16 @@ export function getAllOverpassPresets(): OverpassPresetCategory[] {
{ 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: "sauna", query: "(node[leisure=sauna];way[leisure=sauna];rel[leisure=sauna];)", label: i18n.t("overpass-presets.sauna") },
{ key: "spa", query: "(node[leisure=spa];way[leisure=spa];rel[leisure=spa];)", label: i18n.t("overpass-presets.spa") },
{ 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: i18n.t("overpass-presets.category-hotels"),
presets: [
{ key: "zoo", query: "(node[tourism=zoo];way[tourism=zoo];rel[tourism=zoo];)", label: i18n.t("overpass-presets.zoo") },
],
[
// Places to stay
{ key: "alpinehut", query: "(node[tourism=alpine_hut];way[tourism=alpine_hut];rel[tourism=alpine_hut];)", label: i18n.t("overpass-presets.alpinehut") },
@ -104,10 +101,9 @@ export function getAllOverpassPresets(): OverpassPresetCategory[] {
{ 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") }
],
[
{ key: "tourism", query: "nwr[tourism]", label: i18n.t("overpass-presets.tourism") }
]
]
},
@ -131,12 +127,32 @@ export function getAllOverpassPresets(): OverpassPresetCategory[] {
{ 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") }
],
[
{ key: "sports", query: "nwr[sport]", label: i18n.t("overpass-presets.sports") }
]
]
},
{
label: i18n.t("overpass-presets.category-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") }
],
[
//Various shops (excluding food)
{ key: "beautyshop", query: "(node[shop=beauty];way[shop=beauty];rel[shop=beauty];)", label: i18n.t("overpass-presets.beautyshop") },
@ -168,28 +184,9 @@ export function getAllOverpassPresets(): OverpassPresetCategory[] {
{ 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: 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") }
{ key: "shops", query: "nwr[shop]", label: i18n.t("overpass-presets.shops") }
]
]
},

Wyświetl plik

@ -2,6 +2,7 @@ import type { GeoJSON, Geometry, Feature } from "geojson";
import { FeatureGroup, GeoJSON as GeoJSONLayer, type GeoJSONOptions, Layer, type PathOptions } from "leaflet";
import { HighlightablePolygon, HighlightablePolyline } from "leaflet-highlightable-layers";
import MarkerLayer, { type MarkerLayerOptions } from "../markers/marker-layer";
import { getI18n } from "../utils/i18n";
interface SearchResultGeoJSONOptions extends GeoJSONOptions {
marker?: MarkerLayerOptions['marker'];
@ -98,7 +99,7 @@ export default class SearchResultGeoJSON extends GeoJSONLayer {
)).filter((l) => l) as Layer[]);
default:
throw new Error('Invalid GeoJSON object.');
throw new Error(getI18n().t("search-result-geojson.invalid-geojson-error"));
}
}

Wyświetl plik

@ -18,6 +18,8 @@ declare module "leaflet" {
interface FmLayerOptions {
fmName?: string;
fmGetName?: () => string;
fmGetAttribution?: () => string;
}
interface LayerOptions extends FmLayerOptions {}

Wyświetl plik

@ -24,13 +24,13 @@
"test": "yarn workspaces foreach -v run test"
},
"devDependencies": {
"@types/eslint": "^8.56.5",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@types/eslint": "^8.56.7",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^7.6.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-vue": "^9.22.0"
"eslint-plugin-vue": "^9.24.1"
},
"version": "0.0.0",
"packageManager": "yarn@3.6.3"

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-server",
"version": "4.0.0",
"version": "4.1.0",
"type": "module",
"description": "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.",
"keywords": [
@ -25,7 +25,7 @@
],
"scripts": {
"build": "vite build",
"clean": "rimraf dist",
"clean": "rimraf dist out",
"prod-server": "DOTENV_CONFIG_PATH=../config.env node ./bin/facilmap-server.js",
"server": "DOTENV_CONFIG_PATH=../config.env NODE_OPTIONS='--import tsx' vite-node ./server.ts",
"dev-server": "FM_DEV=true DOTENV_CONFIG_PATH=../config.env NODE_OPTIONS='--import tsx' vite-node ./server.ts",
@ -48,28 +48,28 @@
"csv-stringify": "^6.4.6",
"dotenv": "^16.4.5",
"ejs": "^3.1.9",
"express": "5.0.0-beta.1",
"express": "5.0.0-beta.3",
"express-domain-middleware": "^0.1.0",
"facilmap-frontend": "workspace:^",
"facilmap-leaflet": "workspace:^",
"facilmap-types": "workspace:^",
"facilmap-utils": "workspace:^",
"find-cache-dir": "^5.0.0",
"i18next": "^23.10.1",
"i18next": "^23.11.1",
"i18next-http-middleware": "^3.5.0",
"lodash-es": "^4.17.21",
"maxmind": "^4.3.18",
"md5-file": "^5.0.0",
"mysql2": "^3.9.2",
"mysql2": "^3.9.4",
"p-throttle": "^6.1.0",
"pg": "^8.11.3",
"sequelize": "^6.37.1",
"pg": "^8.11.5",
"sequelize": "^6.37.2",
"serialize-error": "^11.0.3",
"socket.io": "^4.7.4",
"socket.io": "^4.7.5",
"string-similarity": "^4.0.4",
"strip-bom-buf": "^4.0.0",
"unzipper": "^0.10.14",
"zip-stream": "^6.0.0",
"zip-stream": "^6.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
@ -82,17 +82,17 @@
"@types/express-domain-middleware": "^0.0.9",
"@types/geojson": "^7946.0.14",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.25",
"@types/node": "^20.12.6",
"@types/string-similarity": "^4.0.2",
"cpy-cli": "^5.0.0",
"debug": "^4.3.4",
"rimraf": "^5.0.5",
"tsx": "^4.7.1",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-node": "^1.3.1",
"vite-plugin-dts": "^3.7.3",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.1"
"tsx": "^4.7.2",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-node": "^1.4.0",
"vite-plugin-dts": "^3.8.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.4.0"
}
}

Wyświetl plik

@ -5,7 +5,6 @@ import type { ReadableStream } from "stream/web";
import { stringify } from "csv-stringify";
import { Readable, Writable } from "stream";
import { getTabularData } from "./tabular.js";
import { formatFieldName } from "facilmap-utils";
export function exportCsv(
database: Database,
@ -18,7 +17,7 @@ export function exportCsv(
const tabular = await getTabularData(database, padId, typeId, false, filter, hide);
const stringifier = stringify();
stringifier.write(tabular.fields.map((f) => formatFieldName(f)));
stringifier.write(tabular.fieldNames);
void tabular.objects.pipeTo(Writable.toWeb(stringifier));
return Readable.toWeb(stringifier);

Wyświetl plik

@ -1,5 +1,5 @@
import type { ID, PadId } from "facilmap-types";
import { formatFieldName, quoteHtml } from "facilmap-utils";
import { quoteHtml } from "facilmap-utils";
import Database from "../database/database.js";
import { renderTable } from "../frontend.js";
import { ReadableStream } from "stream/web";
@ -44,11 +44,11 @@ export function createSingleTable(
yield `${indent}\t\t<tr>\n`;
let handledFieldNames = new Set<string>();
for (const field of tabular.fields) {
const thAttrs = getThAttrs?.(field, !handledFieldNames.has(field));
handledFieldNames.add(field);
for (let i = 0; i < tabular.fields.length; i++) {
const thAttrs = getThAttrs?.(tabular.fields[i], !handledFieldNames.has(tabular.fields[i]));
handledFieldNames.add(tabular.fields[i]);
yield `${indent}\t\t\t<th${attrs(thAttrs)}>${quoteHtml(formatFieldName(field))}</th>\n`;
yield `${indent}\t\t\t<th${attrs(thAttrs)}>${quoteHtml(tabular.fieldNames[i])}</th>\n`;
}
yield `${indent}\t\t</tr>\n`;

Wyświetl plik

@ -1,5 +1,5 @@
import { flatMapStream, asyncIteratorToStream, mapStream } from "../utils/streams.js";
import { compileExpression, formatDistance, formatFieldValue, formatRouteTime, normalizeLineName, normalizeMarkerName, quoteHtml, round } from "facilmap-utils";
import { compileExpression, formatDistance, formatFieldName, formatFieldValue, formatRouteTime, normalizeLineName, normalizeMarkerName, quoteHtml, round } from "facilmap-utils";
import type { PadId, ID } from "facilmap-types";
import Database from "../database/database.js";
import { ReadableStream } from "stream/web";
@ -7,6 +7,7 @@ import { getI18n } from "../i18n.js";
export type TabularData = {
fields: string[];
fieldNames: string[];
objects: ReadableStream<string[]>;
};
@ -18,9 +19,11 @@ export async function getTabularData(
filter?: string,
hide: string[] = []
): Promise<TabularData> {
const i18n = getI18n();
const padData = await database.pads.getPadData(padId);
if (!padData)
throw new Error(getI18n().t("pad-not-found-error", { padId }));
throw new Error(i18n.t("pad-not-found-error", { padId }));
const type = await database.types.getType(padData.id, typeId);
@ -28,15 +31,15 @@ export async function getTabularData(
const handlePlainText = (str: string) => html ? quoteHtml(str) : str;
const fields = [
"Name",
const fieldsWithNames = [
["Name", i18n.t("tabular.field-name")],
...(type.type === "marker" ? [
"Position"
["Position", i18n.t("tabular.field-position")]
] : type.type === "line" ? [
"Distance",
"Time"
["Distance", i18n.t("tabular.field-distance")],
["Time", i18n.t("tabular.field-time")]
] : []),
...type.fields.map((f) => f.name)
...type.fields.map((f) => [f.name, formatFieldName(f.name)])
];
const objects = type.type === "marker" ? flatMapStream(asyncIteratorToStream(database.markers.getPadMarkersByType(padId, typeId)), (marker): Array<Array<() => string>> => {
@ -63,7 +66,8 @@ export async function getTabularData(
});
return {
fields: fields.filter((f) => !hide.includes(f)),
objects: mapStream(objects, (obj) => obj.flatMap((v, idx) => hide.includes(fields[idx]) ? [] : [v()]))
fields: fieldsWithNames.filter((f) => !hide.includes(f[0])).map((f) => f[0]),
fieldNames: fieldsWithNames.filter((f) => !hide.includes(f[0])).map((f) => f[1]),
objects: mapStream(objects, (obj) => obj.flatMap((v, idx) => hide.includes(fieldsWithNames[idx][0]) ? [] : [v()]))
};
}

Wyświetl plik

@ -69,6 +69,8 @@ function getInjectedConfig(): InjectedConfig {
nominatimUrl: config.nominatimUrl,
limaLabsToken: config.limaLabsToken,
hideCommercialMapLinks: config.hideCommercialMapLinks,
supportsRoutes: !!config.mapboxToken || !!config.orsToken,
supportsAdvancedRoutes: !!config.orsToken
};
}

Wyświetl plik

@ -21,10 +21,16 @@ declare global {
}
}
type FacilMapProcessLang = {
i18n: i18n;
acceptLanguage: string | undefined;
isExplicit: boolean;
};
declare module 'domain' {
interface Domain {
facilmap?: {
i18n?: i18n;
lang?: FacilMapProcessLang;
units?: Units;
}
}
@ -37,11 +43,11 @@ onI18nReady((i18n) => {
i18n.addResourceBundle("ru", namespace, messagesRu);
});
export function getRawDomainI18n(): i18n | undefined {
return process.domain?.facilmap?.i18n;
export function getDomainLang(): FacilMapProcessLang | undefined {
return process.domain?.facilmap?.lang;
}
export function setRawDomainI18n(i18n: i18n): void {
export function setDomainLang(lang: FacilMapProcessLang): void {
if (!process.domain) {
throw new Error("Domain is not initialized");
}
@ -50,14 +56,14 @@ export function setRawDomainI18n(i18n: i18n): void {
process.domain.facilmap = {};
}
process.domain.facilmap.i18n = i18n;
process.domain.facilmap.lang = lang;
}
export function getDomainUnits(): Units | undefined {
return process.domain?.facilmap?.units;
}
export function setDomainUnits(units: Units | undefined): void {
export function setDomainUnits(units: Units): void {
if (!process.domain) {
throw new Error("Domain is not initialized");
}
@ -75,7 +81,11 @@ i18nMiddleware.use((req, res, next) => {
});
i18nMiddleware.use((req, res, next) => {
if ((req as any).i18n) {
setRawDomainI18n(req.i18n);
setDomainLang({
i18n: req.i18n,
isExplicit: !!req.query[LANG_QUERY] || !!req.cookies[LANG_COOKIE],
acceptLanguage: req.headers["accept-language"]
});
}
const queryUnits = req.query.units ? unitsValidator.safeParse(req.query.units) : undefined;
@ -125,7 +135,7 @@ if (!isCustomLanguageDetector) {
if (!isCustomI18nGetter) {
setI18nGetter(() => {
return getRawDomainI18n() ?? defaultI18nGetter();
return getDomainLang()?.i18n ?? defaultI18nGetter();
});
}
@ -138,12 +148,13 @@ export function getI18n(): {
return {
t: getRawI18n().getFixedT(null, namespace),
changeLanguage: async (lang) => {
const i18n = getRawDomainI18n();
if (!i18n) {
const domainLang = getDomainLang();
if (!domainLang) {
throw new Error("Domain not initialized, refusing to change language for main instance.");
}
await i18n.changeLanguage(lang);
await domainLang.i18n.changeLanguage(lang);
domainLang.isExplicit = true;
}
};
}
}

Wyświetl plik

@ -23,7 +23,7 @@
"load-error": "{{appName}} konnte nicht geladen werden!"
},
"routing": {
"ors-token-warning": "Warnung: Kein ORS-Token konfiguriert. Bitten Sie den Administrator, die Umgebungsvariable ORS_TOKEN zu setzen.",
"ors-token-warning": "Kein ORS-Token konfiguriert. Bitten Sie den Administrator, die Umgebungsvariable ORS_TOKEN zu setzen.",
"too-much-distance-error": "Die Distanz zwischen den Routenpunkten ist zu groß. Versuchen Sie, einige Zwischenpunkte hinzuzufügen.",
"invalid-response-error": "Ungültige Antwort vom Routenserver.",
"mapbox-token-warning": "Kein Mapbox-Token konfiguriert. Bitten Sie den Administrator, die Umgebungsvariable MAPBOX_TOKEN zu setzen.",
@ -48,6 +48,12 @@
"url-response-error": "Ungültige Antwort vom Server.",
"url-unknown-format-error": "Unbekanntes Dateiformat."
},
"tabular": {
"field-name": "Name",
"field-position": "Position",
"field-distance": "Länge",
"field-time": "Reisedauer"
},
"webserver": {
"map-not-found-error": "Karte mit der ID {{padId}} konnte nicht gefunden werden."
}

Wyświetl plik

@ -23,7 +23,7 @@
"load-error": "Could not load {{appName}}!"
},
"routing": {
"ors-token-warning": "Warning: No ORS token configured. Please ask the administrator to set ORS_TOKEN in the environment or in config.env.",
"ors-token-warning": "No ORS token configured. Please ask the administrator to set ORS_TOKEN in the environment or in config.env.",
"too-much-distance-error": "Too much distance between route points. Consider adding some via points.",
"invalid-response-error": "Invalid response from routing server.",
"mapbox-token-warning": "No Mapbox token configured. Please ask the administrator to set MAPBOX_TOKEN in the environment or in config.env.",
@ -48,6 +48,12 @@
"url-response-error": "Invalid response from server.",
"url-unknown-format-error": "Unknown file format."
},
"tabular": {
"field-name": "Name",
"field-position": "Position",
"field-distance": "Distance",
"field-time": "Time"
},
"webserver": {
"map-not-found-error": "Map with ID {{padId}} could not be found."
}

Wyświetl plik

@ -5,7 +5,7 @@ import type { RawRouteInfo } from "./routing.js";
import { getI18n } from "../i18n.js";
if (!config.orsToken)
console.error("Warning: No ORS token configured, calculating routes will fail. Please set ORS_TOKEN in the environment or in config.env.");
console.error("Warning: No ORS token configured, calculating routes with advanced settings will not be supported. Please set ORS_TOKEN in the environment or in config.env.");
const ROUTING_URL = `https://api.openrouteservice.org/v2/directions`;

Wyświetl plik

@ -4,7 +4,7 @@ import type { RawRouteInfo } from "./routing.js";
import { getI18n } from "../i18n.js";
if (!config.mapboxToken)
console.error("Warning: No Mapbox token configured, calculating routes will fail. Please set MAPBOX_TOKEN in the environment or in config.env.");
console.error("Warning: No Mapbox token configured, calculating simple routes will fall back to ORS. Please set MAPBOX_TOKEN in the environment or in config.env.");
const ROUTING_URL = "https://api.mapbox.com/directions/v5/mapbox";

Wyświetl plik

@ -1,8 +1,9 @@
import { calculateBbox, isInBbox } from "../utils/geo.js";
import type { Bbox, BboxWithZoom, CRU, Line, Point, Route, RouteInfo, RouteMode, TrackPoint } from "facilmap-types";
import { decodeRouteMode, type DecodedRouteMode, calculateDistance, round } from "facilmap-utils";
import { decodeRouteMode, calculateDistance, round, isSimpleRoute } from "facilmap-utils";
import { calculateOSRMRoute } from "./osrm.js";
import { calculateORSRoute, getMaximumDistanceBetweenRoutePoints } from "./ors.js";
import config from "../config.js";
// The OpenLayers resolution for zoom level 1 is 0.7031249999891753
// and for zoom level 20 0.0000013411044763239684
@ -18,12 +19,13 @@ export type RawRouteInfo = Omit<RouteInfo, "trackPoints" | keyof Bbox> & {
export async function calculateRoute(routePoints: Point[], encodedMode: RouteMode | undefined): Promise<RouteInfo> {
const decodedMode = decodeRouteMode(encodedMode);
const simple = isSimpleRoute(decodedMode);
const simple = (!config.mapboxToken && config.orsToken) ? false : isSimpleRoute(decodedMode);
let route: RawRouteInfo | undefined;
if(simple || _needsOSRM(routePoints, decodedMode))
if (simple) {
route = await calculateOSRMRoute(routePoints, decodedMode.mode);
}
if(!simple) {
if(route) {
@ -98,23 +100,6 @@ export async function calculateRouteForLine(line: Pick<Line<CRU.CREATE_VALIDATED
return result as RouteInfo;
}
export function isSimpleRoute(decodedMode: DecodedRouteMode): boolean {
return !decodedMode.type &&
(!decodedMode.preference || decodedMode.preference == "fastest") &&
(!decodedMode.avoid || decodedMode.avoid.length == 0) &&
!decodedMode.details;
}
function _needsOSRM(routePoints: Point[], decodedMode: DecodedRouteMode) {
const maxDist = getMaximumDistanceBetweenRoutePoints(decodedMode);
for(let i=1; i<routePoints.length; i++) {
if(calculateDistance([ routePoints[i-1], routePoints[i] ]) > maxDist)
return true;
}
return false;
}
function _getTrackPointsFromTrack(trackPoints: Point[], maxDistance: number) {
const result: Point[] = [ trackPoints[0] ];
for(let i=1; i<trackPoints.length; i++) {

Wyświetl plik

@ -6,7 +6,7 @@ import type { SearchResult } from "facilmap-types";
import stripBomBuf from "strip-bom-buf";
import config from "./config.js";
import { find as findSearch, parseUrlQuery } from "facilmap-utils";
import { getI18n } from "./i18n.js";
import { getDomainLang, getI18n } from "./i18n.js";
export async function find(query: string, loadUrls = false): Promise<Array<SearchResult> | string> {
if (loadUrls) {
@ -16,7 +16,11 @@ export async function find(query: string, loadUrls = false): Promise<Array<Searc
}
}
return await findSearch(query);
const lang = getDomainLang();
return await findSearch(query, {
lang: lang?.isExplicit ? lang.i18n.languages.join(",") : lang?.acceptLanguage
});
}
async function _loadUrl(url: string): Promise<string> {

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-types",
"version": "4.0.0",
"version": "4.1.0",
"description": "Typescript typings for the FacilMap communication between client and server.",
"homepage": "https://github.com/FacilMap/facilmap",
"bugs": {
@ -18,7 +18,7 @@
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"clean": "rimraf dist",
"clean": "rimraf dist out out.node",
"check-types": "tsc -b --emitDeclarationOnly"
},
"files": [
@ -32,8 +32,8 @@
},
"devDependencies": {
"rimraf": "^5.0.5",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-plugin-dts": "^3.7.3"
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-dts": "^3.8.1"
}
}

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "facilmap-utils",
"version": "4.0.0",
"version": "4.1.0",
"description": "FacilMap helper functions used in both the frontend and backend.",
"keywords": [
"facilmap"
@ -21,7 +21,7 @@
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"clean": "rimraf dist",
"clean": "rimraf dist out out.node",
"check-types": "tsc -b --emitDeclarationOnly",
"test": "vitest run",
"test-watch": "vitest"
@ -35,10 +35,10 @@
"cheerio": "^1.0.0-rc.12",
"decode-uri-component": "^0.4.1",
"domhandler": "^5.0.3",
"dompurify": "^3.0.9",
"dompurify": "^3.1.0",
"facilmap-types": "workspace:^",
"filtrex": "^2.2.3",
"i18next": "^23.10.1",
"i18next": "^23.11.1",
"i18next-browser-languagedetector": "^7.2.1",
"jquery": "^3.7.1",
"jsdom": "^24.0.0",
@ -54,10 +54,10 @@
"@types/jsdom": "^21.1.6",
"@types/linkifyjs": "^2.1.7",
"rimraf": "^5.0.5",
"typescript": "^5.4.2",
"vite": "^5.1.5",
"vite-plugin-dts": "^3.7.3",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-dts": "^3.8.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.3.1"
"vitest": "^1.4.0"
}
}

Wyświetl plik

@ -46,17 +46,17 @@ test("parseRouteQuery", async () => {
expect(parseRouteQuery("by walk to Berlin from Hamburg")).toEqual({
queries: ["Hamburg", "Berlin"],
mode: "foot"
mode: "pedestrian"
});
expect(parseRouteQuery("from Hamburg by walk to Berlin")).toEqual({
queries: ["Hamburg", "Berlin"],
mode: "foot"
mode: "pedestrian"
});
expect(parseRouteQuery("Hamburg to Berlin walking")).toEqual({
queries: ["Hamburg", "Berlin"],
mode: "foot"
mode: "pedestrian"
});
expect(parseRouteQuery("Hamburg")).toEqual({

Wyświetl plik

@ -158,19 +158,27 @@ export function formatRouteTime(time: number, encodedMode: RouteMode): string {
});
}
export function formatDistance(distance: number): string {
export function kmToMi(km: number): number {
return km / 1.609344;
}
export function mToFt(m: number): number {
return m / 0.3048;
}
export function formatDistance(distance: number, decimals = 2): string {
const units = getCurrentUnits();
if (units === Units.US_CUSTOMARY) {
return getI18n().t("format.distance-mi", { distance: round(distance / 1.609344, 2) });
return getI18n().t("format.distance-mi", { distance: round(kmToMi(distance), decimals) });
} else {
return getI18n().t("format.distance-km", { distance: round(distance, 2) });
return getI18n().t("format.distance-km", { distance: round(distance, decimals) });
}
}
export function formatElevation(elevation: number): string {
const units = getCurrentUnits();
if (units === Units.US_CUSTOMARY) {
return getI18n().t("format.elevation-ft", { elevation: round(elevation / 0.3048, 0) });
return getI18n().t("format.elevation-ft", { elevation: round(mToFt(elevation), 0) });
} else {
return getI18n().t("format.elevation-m", { elevation });
}

Wyświetl plik

@ -192,4 +192,14 @@ export function parseRouteQuery(query: string): RouteQuery {
queries: queryParts.from.concat(queryParts.via, queryParts.to),
mode
};
}
/**
* If true, this route can be handled by OSRM, ORS is not needed.
*/
export function isSimpleRoute(decodedMode: DecodedRouteMode): boolean {
return !decodedMode.type &&
(!decodedMode.preference || decodedMode.preference == "fastest") &&
(!decodedMode.avoid || decodedMode.avoid.length == 0) &&
!decodedMode.details;
}

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