Merge develop
This commit is contained in:
commit
330930d5d4
77 changed files with 2462 additions and 581 deletions
16
.devcontainer/Dockerfile
Normal file
16
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
|
# [Choice] Node.js version: 16, 14, 12
|
||||||
|
ARG VARIANT="16-buster"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||||
|
|
||||||
|
# [Optional] Uncomment this section to install additional OS packages.
|
||||||
|
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||||
|
|
||||||
|
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||||
|
# ARG EXTRA_NODE_VERSION=10
|
||||||
|
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||||
|
|
||||||
|
# [Optional] Uncomment if you want to install more global node packages
|
||||||
|
# RUN su node -c "npm install -g <your-package-list -here>"
|
29
.devcontainer/devcontainer.json
Normal file
29
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node
|
||||||
|
{
|
||||||
|
"name": "MapComplete",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
// Update 'VARIANT' to pick a Node version: 12, 14, 16
|
||||||
|
"args": {
|
||||||
|
"VARIANT": "16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {},
|
||||||
|
|
||||||
|
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
],
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [1234],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "npm run init",
|
||||||
|
|
||||||
|
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"files.eol": "\n"
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import UnitConfigJson from "./UnitConfigJson";
|
||||||
import Translations from "../../UI/i18n/Translations";
|
import Translations from "../../UI/i18n/Translations";
|
||||||
import BaseUIElement from "../../UI/BaseUIElement";
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
import Combine from "../../UI/Base/Combine";
|
import Combine from "../../UI/Base/Combine";
|
||||||
|
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||||
|
|
||||||
export class Unit {
|
export class Unit {
|
||||||
public readonly appliesToKeys: Set<string>;
|
public readonly appliesToKeys: Set<string>;
|
||||||
|
@ -81,7 +82,10 @@ export class Unit {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const [stripped, denom] = this.findDenomination(value)
|
const [stripped, denom] = this.findDenomination(value)
|
||||||
const human = denom.human
|
const human = denom?.human
|
||||||
|
if(human === undefined){
|
||||||
|
return new FixedUiElement(stripped ?? value);
|
||||||
|
}
|
||||||
|
|
||||||
const elems = denom.prefix ? [human, stripped] : [stripped, human];
|
const elems = denom.prefix ? [human, stripped] : [stripped, human];
|
||||||
return new Combine(elems)
|
return new Combine(elems)
|
||||||
|
@ -152,7 +156,7 @@ export class Denomination {
|
||||||
if (stripped === null) {
|
if (stripped === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return stripped + " " + this.canonical.trim()
|
return (stripped + " " + this.canonical.trim()).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -50,9 +50,10 @@ export default class LayerConfig {
|
||||||
public readonly deletion: DeleteConfig | null;
|
public readonly deletion: DeleteConfig | null;
|
||||||
|
|
||||||
presets: {
|
presets: {
|
||||||
title: Translation;
|
title: Translation,
|
||||||
tags: Tag[];
|
tags: Tag[],
|
||||||
description?: Translation;
|
description?: Translation,
|
||||||
|
preciseInput?: { preferredBackground?: string }
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
tagRenderings: TagRenderingConfig[];
|
tagRenderings: TagRenderingConfig[];
|
||||||
|
@ -144,14 +145,19 @@ export default class LayerConfig {
|
||||||
this.minzoom = json.minzoom ?? 0;
|
this.minzoom = json.minzoom ?? 0;
|
||||||
this.maxzoom = json.maxzoom ?? 1000;
|
this.maxzoom = json.maxzoom ?? 1000;
|
||||||
this.wayHandling = json.wayHandling ?? 0;
|
this.wayHandling = json.wayHandling ?? 0;
|
||||||
this.presets = (json.presets ?? []).map((pr, i) => ({
|
this.presets = (json.presets ?? []).map((pr, i) => {
|
||||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
if (pr.preciseInput === true) {
|
||||||
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
pr.preciseInput = {
|
||||||
description: Translations.T(
|
preferredBackground: undefined
|
||||||
pr.description,
|
}
|
||||||
`${context}.presets[${i}].description`
|
}
|
||||||
),
|
return {
|
||||||
}));
|
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||||
|
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
||||||
|
description: Translations.T(pr.description, `${context}.presets[${i}].description`),
|
||||||
|
preciseInput: pr.preciseInput
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** Given a key, gets the corresponding property from the json (or the default if not found
|
/** Given a key, gets the corresponding property from the json (or the default if not found
|
||||||
*
|
*
|
||||||
|
|
|
@ -218,6 +218,16 @@ export interface LayerConfigJson {
|
||||||
* (The first sentence is until the first '.'-character in the description)
|
* (The first sentence is until the first '.'-character in the description)
|
||||||
*/
|
*/
|
||||||
description?: string | any,
|
description?: string | any,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, the user will prompted to confirm the location before actually adding the data.
|
||||||
|
* THis will be with a 'drag crosshair'-method.
|
||||||
|
*
|
||||||
|
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
|
||||||
|
*/
|
||||||
|
preciseInput?: true | {
|
||||||
|
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string
|
||||||
|
}
|
||||||
}[],
|
}[],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default class LayoutConfig {
|
||||||
public readonly enableGeolocation: boolean;
|
public readonly enableGeolocation: boolean;
|
||||||
public readonly enableBackgroundLayerSelection: boolean;
|
public readonly enableBackgroundLayerSelection: boolean;
|
||||||
public readonly enableShowAllQuestions: boolean;
|
public readonly enableShowAllQuestions: boolean;
|
||||||
|
public readonly enableExportButton: boolean;
|
||||||
public readonly customCss?: string;
|
public readonly customCss?: string;
|
||||||
/*
|
/*
|
||||||
How long is the cache valid, in seconds?
|
How long is the cache valid, in seconds?
|
||||||
|
@ -152,6 +153,7 @@ export default class LayoutConfig {
|
||||||
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
|
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
|
||||||
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
|
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
|
||||||
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
|
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
|
||||||
|
this.enableExportButton = json.enableExportButton ?? false;
|
||||||
this.customCss = json.customCss;
|
this.customCss = json.customCss;
|
||||||
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)
|
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson";
|
||||||
* General remark: a type (string | any) indicates either a fixed or a translatable string.
|
* General remark: a type (string | any) indicates either a fixed or a translatable string.
|
||||||
*/
|
*/
|
||||||
export interface LayoutConfigJson {
|
export interface LayoutConfigJson {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of this layout.
|
* The id of this layout.
|
||||||
*
|
*
|
||||||
|
@ -225,6 +226,10 @@ export interface LayoutConfigJson {
|
||||||
*
|
*
|
||||||
* Not only do we want to write consistent data to OSM, we also want to present this consistently to the user.
|
* Not only do we want to write consistent data to OSM, we also want to present this consistently to the user.
|
||||||
* This is handled by defining units.
|
* This is handled by defining units.
|
||||||
|
*
|
||||||
|
* # Rendering
|
||||||
|
*
|
||||||
|
* To render a value with long (human) denomination, use {canonical(key)}
|
||||||
*
|
*
|
||||||
* # Usage
|
* # Usage
|
||||||
*
|
*
|
||||||
|
@ -331,4 +336,5 @@ export interface LayoutConfigJson {
|
||||||
enableGeolocation?: boolean;
|
enableGeolocation?: boolean;
|
||||||
enableBackgroundLayerSelection?: boolean;
|
enableBackgroundLayerSelection?: boolean;
|
||||||
enableShowAllQuestions?: boolean;
|
enableShowAllQuestions?: boolean;
|
||||||
|
enableExportButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@ export default class TagRenderingConfig {
|
||||||
readonly key: string,
|
readonly key: string,
|
||||||
readonly type: string,
|
readonly type: string,
|
||||||
readonly addExtraTags: TagsFilter[];
|
readonly addExtraTags: TagsFilter[];
|
||||||
|
readonly inline: boolean,
|
||||||
|
readonly default?: string,
|
||||||
|
readonly helperArgs?: (string | number | boolean)[]
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly multiAnswer: boolean;
|
readonly multiAnswer: boolean;
|
||||||
|
@ -73,7 +76,9 @@ export default class TagRenderingConfig {
|
||||||
type: json.freeform.type ?? "string",
|
type: json.freeform.type ?? "string",
|
||||||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||||
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
||||||
|
inline: json.freeform.inline ?? false,
|
||||||
|
default: json.freeform.default,
|
||||||
|
helperArgs: json.freeform.helperArgs
|
||||||
|
|
||||||
}
|
}
|
||||||
if (json.freeform["extraTags"] !== undefined) {
|
if (json.freeform["extraTags"] !== undefined) {
|
||||||
|
@ -332,20 +337,20 @@ export default class TagRenderingConfig {
|
||||||
* Note: this might be hidden by conditions
|
* Note: this might be hidden by conditions
|
||||||
*/
|
*/
|
||||||
public hasMinimap(): boolean {
|
public hasMinimap(): boolean {
|
||||||
const translations : Translation[]= Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]);
|
const translations: Translation[] = Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]);
|
||||||
for (const translation of translations) {
|
for (const translation of translations) {
|
||||||
for (const key in translation.translations) {
|
for (const key in translation.translations) {
|
||||||
if(!translation.translations.hasOwnProperty(key)){
|
if (!translation.translations.hasOwnProperty(key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const template = translation.translations[key]
|
const template = translation.translations[key]
|
||||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||||
const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap")
|
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||||
if(hasMiniMap){
|
if (hasMiniMap) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,6 +30,7 @@ export interface TagRenderingConfigJson {
|
||||||
* Allow freeform text input from the user
|
* Allow freeform text input from the user
|
||||||
*/
|
*/
|
||||||
freeform?: {
|
freeform?: {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this key is present, then 'render' is used to display the value.
|
* If this key is present, then 'render' is used to display the value.
|
||||||
* If this is undefined, the rendering is _always_ shown
|
* If this is undefined, the rendering is _always_ shown
|
||||||
|
@ -40,13 +41,30 @@ export interface TagRenderingConfigJson {
|
||||||
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
|
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
|
||||||
*/
|
*/
|
||||||
type?: string,
|
type?: string,
|
||||||
|
/**
|
||||||
|
* Extra parameters to initialize the input helper arguments.
|
||||||
|
* For semantics, see the 'SpecialInputElements.md'
|
||||||
|
*/
|
||||||
|
helperArgs?: (string | number | boolean)[];
|
||||||
/**
|
/**
|
||||||
* If a value is added with the textfield, these extra tag is addded.
|
* If a value is added with the textfield, these extra tag is addded.
|
||||||
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
||||||
**/
|
**/
|
||||||
addExtraTags?: string[];
|
addExtraTags?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set, influences the way a question is asked.
|
||||||
|
* Instead of showing a full-widht text field, the text field will be shown within the rendering of the question.
|
||||||
|
*
|
||||||
|
* This combines badly with special input elements, as it'll distort the layout.
|
||||||
|
*/
|
||||||
|
inline?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default value to enter if no previous tagging is present.
|
||||||
|
* Normally undefined (aka do not enter anything)
|
||||||
|
*/
|
||||||
|
default?: string
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
Development
|
Development
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio, open everything in a 'new WSL Window'.
|
**Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio Code you can use a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the Devcontainer (see more details later).
|
||||||
|
|
||||||
To develop and build MapComplete, yo
|
To develop and build MapComplete, you
|
||||||
|
|
||||||
0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15
|
0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15
|
||||||
0. Make a fork and clone the repository.
|
0. Make a fork and clone the repository.
|
||||||
|
@ -29,6 +29,30 @@
|
||||||
4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||||
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||||
|
|
||||||
|
Development using Windows
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
For Windows you can use the devcontainer, or the WSL subsystem.
|
||||||
|
|
||||||
|
To use the devcontainer in Visual Studio Code:
|
||||||
|
|
||||||
|
0. Make sure you have installed the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and it's dependencies.
|
||||||
|
1. Make a fork and clone the repository.
|
||||||
|
2. After cloning, Visual Studio Code will ask you if you want to use the devcontainer.
|
||||||
|
3. Then you can either clone it again in a volume (for better performance), or open the current folder in a container.
|
||||||
|
4. By now, you should be able to run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||||
|
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||||
|
|
||||||
|
To use the WSL in Visual Studio Code:
|
||||||
|
|
||||||
|
0. Make sure you have installed the [Remote - WSL]() extension and it's dependencies.
|
||||||
|
1. Open a remote WSL window using the button in the bottom left.
|
||||||
|
2. Make a fork and clone the repository.
|
||||||
|
3. Install `npm` using `sudo apt install npm`.
|
||||||
|
4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too
|
||||||
|
5. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
|
||||||
|
6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
|
||||||
|
|
||||||
|
|
||||||
Automatic deployment
|
Automatic deployment
|
||||||
--------------------
|
--------------------
|
||||||
|
|
|
@ -20,126 +20,158 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are
|
||||||
Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
|
||||||
|
|
||||||
|
|
||||||
layer-control-toggle
|
backend
|
||||||
----------------------
|
|
||||||
|
|
||||||
Whether or not the layer control is shown The default value is _false_
|
|
||||||
|
|
||||||
|
|
||||||
tab
|
|
||||||
-----
|
|
||||||
|
|
||||||
The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_
|
|
||||||
|
|
||||||
|
|
||||||
z
|
|
||||||
---
|
|
||||||
|
|
||||||
The initial/current zoom level The default value is _0_
|
|
||||||
|
|
||||||
|
|
||||||
lat
|
|
||||||
-----
|
|
||||||
|
|
||||||
The initial/current latitude The default value is _0_
|
|
||||||
|
|
||||||
|
|
||||||
lon
|
|
||||||
-----
|
|
||||||
|
|
||||||
The initial/current longitude of the app The default value is _0_
|
|
||||||
|
|
||||||
|
|
||||||
fs-userbadge
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-search
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Disables/Enables the search bar The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-layers
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Disables/Enables the layer control The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-add-new
|
|
||||||
------------
|
|
||||||
|
|
||||||
Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-welcome-message
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Disables/enables the help menu or welcome message The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-iframe
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Disables/Enables the iframe-popup The default value is _false_
|
|
||||||
|
|
||||||
|
|
||||||
fs-more-quests
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-share-screen
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-geolocation
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Disables/Enables the geolocation button The default value is _true_
|
|
||||||
|
|
||||||
|
|
||||||
fs-all-questions
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Always show all questions The default value is _false_
|
|
||||||
|
|
||||||
|
|
||||||
test
|
|
||||||
------
|
|
||||||
|
|
||||||
If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_
|
|
||||||
|
|
||||||
|
|
||||||
debug
|
|
||||||
-------
|
|
||||||
|
|
||||||
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
|
|
||||||
|
|
||||||
|
|
||||||
backend
|
|
||||||
---------
|
---------
|
||||||
|
|
||||||
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
|
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
|
||||||
|
|
||||||
|
|
||||||
custom-css
|
test
|
||||||
|
------
|
||||||
|
|
||||||
|
If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
layout
|
||||||
|
--------
|
||||||
|
|
||||||
|
The layout to load into MapComplete The default value is __
|
||||||
|
|
||||||
|
|
||||||
|
userlayout
|
||||||
------------
|
------------
|
||||||
|
|
||||||
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways:
|
||||||
|
|
||||||
|
- The hash of the URL contains a base64-encoded .json-file containing the theme definition
|
||||||
|
- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator
|
||||||
|
- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
background
|
layer-control-toggle
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Whether or not the layer control is shown The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
tab
|
||||||
|
-----
|
||||||
|
|
||||||
|
The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_
|
||||||
|
|
||||||
|
|
||||||
|
z
|
||||||
|
---
|
||||||
|
|
||||||
|
The initial/current zoom level The default value is _14_
|
||||||
|
|
||||||
|
|
||||||
|
lat
|
||||||
|
-----
|
||||||
|
|
||||||
|
The initial/current latitude The default value is _51.2095_
|
||||||
|
|
||||||
|
|
||||||
|
lon
|
||||||
|
-----
|
||||||
|
|
||||||
|
The initial/current longitude of the app The default value is _3.2228_
|
||||||
|
|
||||||
|
|
||||||
|
fs-userbadge
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-search
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Disables/Enables the search bar The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-layers
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Disables/Enables the layer control The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-add-new
|
||||||
------------
|
------------
|
||||||
|
|
||||||
The id of the background layer to start with The default value is _osm_
|
Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-welcome-message
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Disables/enables the help menu or welcome message The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-iframe
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Disables/Enables the iframe-popup The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
fs-more-quests
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-share-screen
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-geolocation
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Disables/Enables the geolocation button The default value is _true_
|
||||||
|
|
||||||
|
|
||||||
|
fs-all-questions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Always show all questions The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
fs-export
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If set, enables the 'download'-button to download everything as geojson The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
fake-user
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
debug
|
||||||
|
-------
|
||||||
|
|
||||||
|
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
|
||||||
|
|
||||||
|
|
||||||
|
custom-css
|
||||||
|
------------
|
||||||
|
|
||||||
|
If specified, the custom css from the given link will be loaded additionaly The default value is __
|
||||||
|
|
||||||
|
|
||||||
|
background
|
||||||
|
------------
|
||||||
|
|
||||||
|
The id of the background layer to start with The default value is _osm_
|
||||||
|
|
||||||
|
|
||||||
|
oauth_token
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Used to complete the login No default value set
|
||||||
layer-<layer-id>
|
layer-<layer-id>
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -435,10 +435,7 @@ export class InitUiElements {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InitBaseMap() {
|
private static InitBaseMap() {
|
||||||
State.state.availableBackgroundLayers = new AvailableBaseLayers(
|
State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl);
|
||||||
State.state.locationControl
|
|
||||||
).availableEditorLayers;
|
|
||||||
|
|
||||||
State.state.backgroundLayer = State.state.backgroundLayerId.map(
|
State.state.backgroundLayer = State.state.backgroundLayerId.map(
|
||||||
(selectedId: string) => {
|
(selectedId: string) => {
|
||||||
if (selectedId === undefined) {
|
if (selectedId === undefined) {
|
||||||
|
@ -545,6 +542,7 @@ export class InitUiElements {
|
||||||
state.selectedElement
|
state.selectedElement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
State.state.featurePipeline = source;
|
||||||
new ShowDataLayer(
|
new ShowDataLayer(
|
||||||
source.features,
|
source.features,
|
||||||
State.state.leafletMap,
|
State.state.leafletMap,
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as editorlayerindex from "../../assets/editor-layer-index.json"
|
import * as editorlayerindex from "../../assets/editor-layer-index.json"
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
|
import {TileLayer} from "leaflet";
|
||||||
import * as X from "leaflet-providers";
|
import * as X from "leaflet-providers";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import {GeoOperations} from "../GeoOperations";
|
import {GeoOperations} from "../GeoOperations";
|
||||||
import {TileLayer} from "leaflet";
|
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import Loc from "../../Models/Loc";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates which layers are available at the current location
|
* Calculates which layers are available at the current location
|
||||||
|
@ -24,45 +25,87 @@ export default class AvailableBaseLayers {
|
||||||
false, false),
|
false, false),
|
||||||
feature: null,
|
feature: null,
|
||||||
max_zoom: 19,
|
max_zoom: 19,
|
||||||
min_zoom: 0
|
min_zoom: 0,
|
||||||
|
isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
|
||||||
|
category: "osmbasedmap"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
|
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
|
||||||
public availableEditorLayers: UIEventSource<BaseLayer[]>;
|
|
||||||
|
|
||||||
constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) {
|
public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||||
const self = this;
|
const source = location.map(
|
||||||
this.availableEditorLayers =
|
(currentLocation) => {
|
||||||
location.map(
|
|
||||||
(currentLocation) => {
|
|
||||||
|
|
||||||
if (currentLocation === undefined) {
|
if (currentLocation === undefined) {
|
||||||
return AvailableBaseLayers.layerOverview;
|
return AvailableBaseLayers.layerOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLayers = self.availableEditorLayers?.data;
|
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||||
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||||
|
|
||||||
if (currentLayers === undefined) {
|
if (currentLayers === undefined) {
|
||||||
|
return newLayers;
|
||||||
|
}
|
||||||
|
if (newLayers.length !== currentLayers.length) {
|
||||||
|
return newLayers;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < newLayers.length; i++) {
|
||||||
|
if (newLayers[i].name !== currentLayers[i].name) {
|
||||||
return newLayers;
|
return newLayers;
|
||||||
}
|
}
|
||||||
if (newLayers.length !== currentLayers.length) {
|
}
|
||||||
return newLayers;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < newLayers.length; i++) {
|
|
||||||
if (newLayers[i].name !== currentLayers[i].name) {
|
|
||||||
return newLayers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentLayers;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
return currentLayers;
|
||||||
|
});
|
||||||
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
|
||||||
|
return AvailableBaseLayers.AvailableLayersAt(location).map(available => {
|
||||||
|
// First float all 'best layers' to the top
|
||||||
|
available.sort((a, b) => {
|
||||||
|
if (a.isBest && b.isBest) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!a.isBest) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (preferedCategory.data === undefined) {
|
||||||
|
return available[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefered: string []
|
||||||
|
if (typeof preferedCategory.data === "string") {
|
||||||
|
prefered = [preferedCategory.data]
|
||||||
|
} else {
|
||||||
|
prefered = preferedCategory.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefered.reverse();
|
||||||
|
for (const category of prefered) {
|
||||||
|
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
|
||||||
|
available.sort((a, b) => {
|
||||||
|
if (a.category === category && b.category === category) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (a.category !== category) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return available[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
|
||||||
const availableLayers = [AvailableBaseLayers.osmCarto]
|
const availableLayers = [AvailableBaseLayers.osmCarto]
|
||||||
const globalLayers = [];
|
const globalLayers = [];
|
||||||
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
|
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
|
||||||
|
@ -140,7 +183,9 @@ export default class AvailableBaseLayers {
|
||||||
min_zoom: props.min_zoom ?? 1,
|
min_zoom: props.min_zoom ?? 1,
|
||||||
name: props.name,
|
name: props.name,
|
||||||
layer: leafletLayer,
|
layer: leafletLayer,
|
||||||
feature: layer
|
feature: layer,
|
||||||
|
isBest: props.best ?? false,
|
||||||
|
category: props.category
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return layers;
|
return layers;
|
||||||
|
@ -152,15 +197,16 @@ export default class AvailableBaseLayers {
|
||||||
function l(id: string, name: string): BaseLayer {
|
function l(id: string, name: string): BaseLayer {
|
||||||
try {
|
try {
|
||||||
const layer: any = () => L.tileLayer.provider(id, undefined);
|
const layer: any = () => L.tileLayer.provider(id, undefined);
|
||||||
const baseLayer: BaseLayer = {
|
return {
|
||||||
feature: null,
|
feature: null,
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
min_zoom: layer.minzoom,
|
min_zoom: layer.minzoom,
|
||||||
max_zoom: layer.maxzoom
|
max_zoom: layer.maxzoom,
|
||||||
|
category: "osmbasedmap",
|
||||||
|
isBest: false
|
||||||
}
|
}
|
||||||
return baseLayer
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not find provided layer", name, e);
|
console.error("Could not find provided layer", name, e);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||||
|
@ -15,11 +14,19 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
*/
|
*/
|
||||||
private readonly _isActive: UIEventSource<boolean>;
|
private readonly _isActive: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private readonly _isLocked: UIEventSource<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The callback over the permission API
|
* The callback over the permission API
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private readonly _permission: UIEventSource<string>;
|
private readonly _permission: UIEventSource<string>;
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* The marker on the map, in order to update it
|
* The marker on the map, in order to update it
|
||||||
* @private
|
* @private
|
||||||
|
@ -39,11 +46,15 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
|
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _lastUserRequest: Date;
|
private _lastUserRequest: Date;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
||||||
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
|
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
|
||||||
|
@ -67,28 +78,32 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
"geolocation-permissions"
|
"geolocation-permissions"
|
||||||
);
|
);
|
||||||
const isActive = new UIEventSource<boolean>(false);
|
const isActive = new UIEventSource<boolean>(false);
|
||||||
|
const isLocked = new UIEventSource<boolean>(false);
|
||||||
super(
|
super(
|
||||||
hasLocation.map(
|
hasLocation.map(
|
||||||
(hasLocation) => {
|
(hasLocationData) => {
|
||||||
if (hasLocation) {
|
let icon: string;
|
||||||
return new CenterFlexedElement(
|
|
||||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
if (isLocked.data) {
|
||||||
); // crosshair_blue_ui()
|
icon = Svg.crosshair_locked;
|
||||||
}
|
} else if (hasLocationData) {
|
||||||
if (isActive.data) {
|
icon = Svg.crosshair_blue;
|
||||||
return new CenterFlexedElement(
|
} else if (isActive.data) {
|
||||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
icon = Svg.crosshair_blue_center;
|
||||||
); // crosshair_blue_center_ui
|
} else {
|
||||||
|
icon = Svg.crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CenterFlexedElement(
|
return new CenterFlexedElement(
|
||||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem")
|
||||||
); //crosshair_ui
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
[isActive]
|
[isActive, isLocked]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this._isActive = isActive;
|
this._isActive = isActive;
|
||||||
|
this._isLocked = isLocked;
|
||||||
this._permission = new UIEventSource<string>("");
|
this._permission = new UIEventSource<string>("");
|
||||||
this._previousLocationGrant = previousLocationGrant;
|
this._previousLocationGrant = previousLocationGrant;
|
||||||
this._currentGPSLocation = currentGPSLocation;
|
this._currentGPSLocation = currentGPSLocation;
|
||||||
|
@ -110,13 +125,14 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
self.SetClass(pointerClass);
|
self.SetClass(pointerClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onClick(() => self.init(true));
|
this.onClick(() => {
|
||||||
|
if (self._hasLocation.data) {
|
||||||
|
self._isLocked.setData(!self._isLocked.data);
|
||||||
|
}
|
||||||
|
self.init(true);
|
||||||
|
});
|
||||||
this.init(false);
|
this.init(false);
|
||||||
}
|
|
||||||
|
|
||||||
private init(askPermission: boolean) {
|
|
||||||
const self = this;
|
|
||||||
const map = this._leafletMap.data;
|
|
||||||
|
|
||||||
this._currentGPSLocation.addCallback((location) => {
|
this._currentGPSLocation.addCallback((location) => {
|
||||||
self._previousLocationGrant.setData("granted");
|
self._previousLocationGrant.setData("granted");
|
||||||
|
@ -125,6 +141,8 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||||
if (timeSinceRequest < 30) {
|
if (timeSinceRequest < 30) {
|
||||||
self.MoveToCurrentLoction(16);
|
self.MoveToCurrentLoction(16);
|
||||||
|
} else if (self._isLocked.data) {
|
||||||
|
self.MoveToCurrentLoction();
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = "#1111cc";
|
let color = "#1111cc";
|
||||||
|
@ -141,6 +159,8 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
|
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const map = self._leafletMap.data;
|
||||||
|
|
||||||
const newMarker = L.marker(location.latlng, {icon: icon});
|
const newMarker = L.marker(location.latlng, {icon: icon});
|
||||||
newMarker.addTo(map);
|
newMarker.addTo(map);
|
||||||
|
|
||||||
|
@ -149,7 +169,14 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
}
|
}
|
||||||
self._marker = newMarker;
|
self._marker = newMarker;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(askPermission: boolean) {
|
||||||
|
const self = this;
|
||||||
|
if (self._isActive.data) {
|
||||||
|
self.MoveToCurrentLoction(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
navigator?.permissions
|
navigator?.permissions
|
||||||
?.query({name: "geolocation"})
|
?.query({name: "geolocation"})
|
||||||
|
@ -174,31 +201,6 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private locate() {
|
|
||||||
const self = this;
|
|
||||||
const map: any = this._leafletMap.data;
|
|
||||||
|
|
||||||
if (navigator.geolocation) {
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
function (position) {
|
|
||||||
self._currentGPSLocation.setData({
|
|
||||||
latlng: [position.coords.latitude, position.coords.longitude],
|
|
||||||
accuracy: position.coords.accuracy,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
console.warn("Could not get location with navigator.geolocation");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
map.findAccuratePosition({
|
|
||||||
maxWait: 10000, // defaults to 10000
|
|
||||||
desiredAccuracy: 50, // defaults to 20
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MoveToCurrentLoction(targetZoom = 16) {
|
private MoveToCurrentLoction(targetZoom = 16) {
|
||||||
const location = this._currentGPSLocation.data;
|
const location = this._currentGPSLocation.data;
|
||||||
this._lastUserRequest = undefined;
|
this._lastUserRequest = undefined;
|
||||||
|
@ -249,17 +251,21 @@ export default class GeoLocationHandler extends VariableUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Searching location using GPS");
|
console.log("Searching location using GPS");
|
||||||
this.locate();
|
|
||||||
|
|
||||||
if (!self._isActive.data) {
|
if (self._isActive.data) {
|
||||||
self._isActive.setData(true);
|
return;
|
||||||
Utils.DoEvery(60000, () => {
|
|
||||||
if (document.visibilityState !== "visible") {
|
|
||||||
console.log("Not starting gps: document not visible");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.locate();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
self._isActive.setData(true);
|
||||||
|
navigator.geolocation.watchPosition(
|
||||||
|
function (position) {
|
||||||
|
self._currentGPSLocation.setData({
|
||||||
|
latlng: [position.coords.latitude, position.coords.longitude],
|
||||||
|
accuracy: position.coords.accuracy,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
console.warn("Could not get location with navigator.geolocation");
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,12 @@ export default class StrayClickHandler {
|
||||||
popupAnchor: [0, -45]
|
popupAnchor: [0, -45]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const popup = L.popup().setContent("<div id='strayclick'></div>");
|
const popup = L.popup({
|
||||||
|
autoPan: true,
|
||||||
|
autoPanPaddingTopLeft: [15,15],
|
||||||
|
closeOnEscapeKey: true,
|
||||||
|
autoClose: true
|
||||||
|
}).setContent("<div id='strayclick' style='height: 65vh'></div>");
|
||||||
self._lastMarker.addTo(leafletMap.data);
|
self._lastMarker.addTo(leafletMap.data);
|
||||||
self._lastMarker.bindPopup(popup);
|
self._lastMarker.bindPopup(popup);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,45 @@
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
|
||||||
export default interface FeatureSource {
|
export default interface FeatureSource {
|
||||||
features: UIEventSource<{feature: any, freshness: Date}[]>;
|
features: UIEventSource<{ feature: any, freshness: Date }[]>;
|
||||||
/**
|
/**
|
||||||
* Mainly used for debuging
|
* Mainly used for debuging
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeatureSourceUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
|
||||||
|
* @param featurePipeline The FeaturePipeline you want to export
|
||||||
|
* @param options The options object
|
||||||
|
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
|
||||||
|
*/
|
||||||
|
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
|
||||||
|
let defaults = {
|
||||||
|
metadata: false,
|
||||||
|
}
|
||||||
|
options = Utils.setDefaults(options, defaults);
|
||||||
|
|
||||||
|
// Select all features, ignore the freshness and other data
|
||||||
|
let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature);
|
||||||
|
|
||||||
|
if (!options.metadata) {
|
||||||
|
for (let i = 0; i < featureList.length; i++) {
|
||||||
|
let feature = featureList[i];
|
||||||
|
for (let property in feature.properties) {
|
||||||
|
if (property[0] == "_") {
|
||||||
|
delete featureList[i]["properties"][property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {type: "FeatureCollection", features: featureList}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -276,6 +276,14 @@ export class GeoOperations {
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Generates the closest point on a way from a given point
|
||||||
|
* @param way The road on which you want to find a point
|
||||||
|
* @param point Point defined as [lon, lat]
|
||||||
|
*/
|
||||||
|
public static nearestPoint(way, point: [number, number]){
|
||||||
|
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,31 +6,38 @@ import Constants from "../../Models/Constants";
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||||
import {TagsFilter} from "../Tags/TagsFilter";
|
import {TagsFilter} from "../Tags/TagsFilter";
|
||||||
import {Tag} from "../Tags/Tag";
|
import {Tag} from "../Tags/Tag";
|
||||||
|
import {OsmConnection} from "./OsmConnection";
|
||||||
|
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all changes made to OSM.
|
* Handles all changes made to OSM.
|
||||||
* Needs an authenticator via OsmConnection
|
* Needs an authenticator via OsmConnection
|
||||||
*/
|
*/
|
||||||
export class Changes implements FeatureSource{
|
export class Changes implements FeatureSource {
|
||||||
|
|
||||||
|
|
||||||
|
private static _nextId = -1; // Newly assigned ID's are negative
|
||||||
public readonly name = "Newly added features"
|
public readonly name = "Newly added features"
|
||||||
/**
|
/**
|
||||||
* The newly created points, as a FeatureSource
|
* The newly created points, as a FeatureSource
|
||||||
*/
|
*/
|
||||||
public features = new UIEventSource<{feature: any, freshness: Date}[]>([]);
|
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||||
|
|
||||||
private static _nextId = -1; // Newly assigned ID's are negative
|
|
||||||
/**
|
/**
|
||||||
* All the pending changes
|
* All the pending changes
|
||||||
*/
|
*/
|
||||||
public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =
|
public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", [])
|
||||||
new UIEventSource<{elementId: string; key: string; value: string}[]>([]);
|
|
||||||
|
/**
|
||||||
|
* All the pending new objects to upload
|
||||||
|
*/
|
||||||
|
private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", [])
|
||||||
|
|
||||||
|
private readonly isUploading = new UIEventSource(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a change to the pending changes
|
* Adds a change to the pending changes
|
||||||
*/
|
*/
|
||||||
private static checkChange(kv: {k: string, v: string}): { k: string, v: string } {
|
private static checkChange(kv: { k: string, v: string }): { k: string, v: string } {
|
||||||
const key = kv.k;
|
const key = kv.k;
|
||||||
const value = kv.v;
|
const value = kv.v;
|
||||||
if (key === undefined || key === null) {
|
if (key === undefined || key === null) {
|
||||||
|
@ -49,8 +56,7 @@ export class Changes implements FeatureSource{
|
||||||
return {k: key.trim(), v: value.trim()};
|
return {k: key.trim(), v: value.trim()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
addTag(elementId: string, tagsFilter: TagsFilter,
|
addTag(elementId: string, tagsFilter: TagsFilter,
|
||||||
tags?: UIEventSource<any>) {
|
tags?: UIEventSource<any>) {
|
||||||
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
|
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
|
||||||
|
@ -59,7 +65,7 @@ export class Changes implements FeatureSource{
|
||||||
if (changes.length == 0) {
|
if (changes.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (elementTags[change.k] !== change.v) {
|
if (elementTags[change.k] !== change.v) {
|
||||||
elementTags[change.k] = change.v;
|
elementTags[change.k] = change.v;
|
||||||
|
@ -76,16 +82,16 @@ export class Changes implements FeatureSource{
|
||||||
* Uploads all the pending changes in one go.
|
* Uploads all the pending changes in one go.
|
||||||
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
||||||
*/
|
*/
|
||||||
public flushChanges(flushreason: string = undefined){
|
public flushChanges(flushreason: string = undefined) {
|
||||||
if(this.pending.data.length === 0){
|
if (this.pending.data.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(flushreason !== undefined){
|
if (flushreason !== undefined) {
|
||||||
console.log(flushreason)
|
console.log(flushreason)
|
||||||
}
|
}
|
||||||
this.uploadAll([], this.pending.data);
|
this.uploadAll();
|
||||||
this.pending.setData([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new node element at the given lat/long.
|
* Create a new node element at the given lat/long.
|
||||||
* An internal OsmObject is created to upload later on, a geojson represention is returned.
|
* An internal OsmObject is created to upload later on, a geojson represention is returned.
|
||||||
|
@ -93,12 +99,12 @@ export class Changes implements FeatureSource{
|
||||||
*/
|
*/
|
||||||
public createElement(basicTags: Tag[], lat: number, lon: number) {
|
public createElement(basicTags: Tag[], lat: number, lon: number) {
|
||||||
console.log("Creating a new element with ", basicTags)
|
console.log("Creating a new element with ", basicTags)
|
||||||
const osmNode = new OsmNode(Changes._nextId);
|
const newId = Changes._nextId;
|
||||||
Changes._nextId--;
|
Changes._nextId--;
|
||||||
|
|
||||||
const id = "node/" + osmNode.id;
|
const id = "node/" + newId;
|
||||||
osmNode.lat = lat;
|
|
||||||
osmNode.lon = lon;
|
|
||||||
const properties = {id: id};
|
const properties = {id: id};
|
||||||
|
|
||||||
const geojson = {
|
const geojson = {
|
||||||
|
@ -118,35 +124,49 @@ export class Changes implements FeatureSource{
|
||||||
// The tags are not yet written into the OsmObject, but this is applied onto a
|
// The tags are not yet written into the OsmObject, but this is applied onto a
|
||||||
const changes = [];
|
const changes = [];
|
||||||
for (const kv of basicTags) {
|
for (const kv of basicTags) {
|
||||||
properties[kv.key] = kv.value;
|
|
||||||
if (typeof kv.value !== "string") {
|
if (typeof kv.value !== "string") {
|
||||||
throw "Invalid value: don't use a regex in a preset"
|
throw "Invalid value: don't use a regex in a preset"
|
||||||
}
|
}
|
||||||
|
properties[kv.key] = kv.value;
|
||||||
changes.push({elementId: id, key: kv.key, value: kv.value})
|
changes.push({elementId: id, key: kv.key, value: kv.value})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("New feature added and pinged")
|
console.log("New feature added and pinged")
|
||||||
this.features.data.push({feature:geojson, freshness: new Date()});
|
this.features.data.push({feature: geojson, freshness: new Date()});
|
||||||
this.features.ping();
|
this.features.ping();
|
||||||
|
|
||||||
State.state.allElements.addOrGetElement(geojson).ping();
|
State.state.allElements.addOrGetElement(geojson).ping();
|
||||||
|
|
||||||
this.uploadAll([osmNode], changes);
|
if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) {
|
||||||
|
properties["_backend"] = State.state.osmConnection.userDetails.data.backend
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.newObjects.data.push({id: newId, lat: lat, lon: lon})
|
||||||
|
this.pending.data.push(...changes)
|
||||||
|
this.pending.ping();
|
||||||
|
this.newObjects.ping();
|
||||||
return geojson;
|
return geojson;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadChangesWithLatestVersions(
|
private uploadChangesWithLatestVersions(
|
||||||
knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
|
knownElements: OsmObject[]) {
|
||||||
const knownById = new Map<string, OsmObject>();
|
const knownById = new Map<string, OsmObject>();
|
||||||
|
|
||||||
knownElements.forEach(knownElement => {
|
knownElements.forEach(knownElement => {
|
||||||
knownById.set(knownElement.type + "/" + knownElement.id, knownElement)
|
knownById.set(knownElement.type + "/" + knownElement.id, knownElement)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const newElements: OsmNode [] = this.newObjects.data.map(spec => {
|
||||||
|
const newElement = new OsmNode(spec.id);
|
||||||
|
newElement.lat = spec.lat;
|
||||||
|
newElement.lon = spec.lon;
|
||||||
|
return newElement
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
|
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
|
||||||
// We apply the changes on them
|
// We apply the changes on them
|
||||||
for (const change of pending) {
|
for (const change of this.pending.data) {
|
||||||
if (parseInt(change.elementId.split("/")[1]) < 0) {
|
if (parseInt(change.elementId.split("/")[1]) < 0) {
|
||||||
// This is a new element - we should apply this on one of the new elements
|
// This is a new element - we should apply this on one of the new elements
|
||||||
for (const newElement of newElements) {
|
for (const newElement of newElements) {
|
||||||
|
@ -168,9 +188,17 @@ export class Changes implements FeatureSource{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedElements.length == 0 && newElements.length == 0) {
|
if (changedElements.length == 0 && newElements.length == 0) {
|
||||||
console.log("No changes in any object");
|
console.log("No changes in any object - clearing");
|
||||||
|
this.pending.setData([])
|
||||||
|
this.newObjects.setData([])
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if (this.isUploading.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isUploading.setData(true)
|
||||||
|
|
||||||
console.log("Beginning upload...");
|
console.log("Beginning upload...");
|
||||||
// At last, we build the changeset and upload
|
// At last, we build the changeset and upload
|
||||||
|
@ -213,17 +241,22 @@ export class Changes implements FeatureSource{
|
||||||
changes += "</osmChange>";
|
changes += "</osmChange>";
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
});
|
},
|
||||||
|
() => {
|
||||||
|
console.log("Upload successfull!")
|
||||||
|
self.newObjects.setData([])
|
||||||
|
self.pending.setData([]);
|
||||||
|
self.isUploading.setData(false)
|
||||||
|
},
|
||||||
|
() => self.isUploading.setData(false)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
private uploadAll(
|
private uploadAll() {
|
||||||
newElements: OsmObject[],
|
|
||||||
pending: { elementId: string; key: string; value: string }[]
|
|
||||||
) {
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
const pending = this.pending.data;
|
||||||
let neededIds: string[] = [];
|
let neededIds: string[] = [];
|
||||||
for (const change of pending) {
|
for (const change of pending) {
|
||||||
const id = change.elementId;
|
const id = change.elementId;
|
||||||
|
@ -236,8 +269,7 @@ export class Changes implements FeatureSource{
|
||||||
|
|
||||||
neededIds = Utils.Dedup(neededIds);
|
neededIds = Utils.Dedup(neededIds);
|
||||||
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
|
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
|
||||||
console.log("KnownElements:", knownElements)
|
self.uploadChangesWithLatestVersions(knownElements)
|
||||||
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class ChangesetHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) {
|
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void {
|
||||||
const nodes = response.getElementsByTagName("node");
|
const nodes = response.getElementsByTagName("node");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
@ -69,7 +69,9 @@ export class ChangesetHandler {
|
||||||
public UploadChangeset(
|
public UploadChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string) {
|
generateChangeXML: (csid: string) => string,
|
||||||
|
whenDone: (csId: string) => void,
|
||||||
|
onFail: () => void) {
|
||||||
|
|
||||||
if (this.userDetails.data.csCount == 0) {
|
if (this.userDetails.data.csCount == 0) {
|
||||||
// The user became a contributor!
|
// The user became a contributor!
|
||||||
|
@ -80,6 +82,7 @@ export class ChangesetHandler {
|
||||||
if (this._dryRun) {
|
if (this._dryRun) {
|
||||||
const changesetXML = generateChangeXML("123456");
|
const changesetXML = generateChangeXML("123456");
|
||||||
console.log(changesetXML);
|
console.log(changesetXML);
|
||||||
|
whenDone("123456")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,12 +96,14 @@ export class ChangesetHandler {
|
||||||
console.log(changeset);
|
console.log(changeset);
|
||||||
self.AddChange(csId, changeset,
|
self.AddChange(csId, changeset,
|
||||||
allElements,
|
allElements,
|
||||||
() => {
|
whenDone,
|
||||||
},
|
|
||||||
(e) => {
|
(e) => {
|
||||||
console.error("UPLOADING FAILED!", e)
|
console.error("UPLOADING FAILED!", e)
|
||||||
|
onFail()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}, {
|
||||||
|
onFail: onFail
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// There still exists an open changeset (or at least we hope so)
|
// There still exists an open changeset (or at least we hope so)
|
||||||
|
@ -107,15 +112,13 @@ export class ChangesetHandler {
|
||||||
csId,
|
csId,
|
||||||
generateChangeXML(csId),
|
generateChangeXML(csId),
|
||||||
allElements,
|
allElements,
|
||||||
() => {
|
whenDone,
|
||||||
},
|
|
||||||
(e) => {
|
(e) => {
|
||||||
console.warn("Could not upload, changeset is probably closed: ", e);
|
console.warn("Could not upload, changeset is probably closed: ", e);
|
||||||
// Mark the CS as closed...
|
// Mark the CS as closed...
|
||||||
this.currentChangeset.setData("");
|
this.currentChangeset.setData("");
|
||||||
// ... and try again. As the cs is closed, no recursive loop can exist
|
// ... and try again. As the cs is closed, no recursive loop can exist
|
||||||
self.UploadChangeset(layout, allElements, generateChangeXML);
|
self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -161,18 +164,22 @@ export class ChangesetHandler {
|
||||||
const self = this;
|
const self = this;
|
||||||
this.OpenChangeset(layout, (csId: string) => {
|
this.OpenChangeset(layout, (csId: string) => {
|
||||||
|
|
||||||
// The cs is open - let us actually upload!
|
// The cs is open - let us actually upload!
|
||||||
const changes = generateChangeXML(csId)
|
const changes = generateChangeXML(csId)
|
||||||
|
|
||||||
self.AddChange(csId, changes, allElements, (csId) => {
|
self.AddChange(csId, changes, allElements, (csId) => {
|
||||||
console.log("Successfully deleted ", object.id)
|
console.log("Successfully deleted ", object.id)
|
||||||
self.CloseChangeset(csId, continuation)
|
self.CloseChangeset(csId, continuation)
|
||||||
}, (csId) => {
|
}, (csId) => {
|
||||||
alert("Deletion failed... Should not happend")
|
alert("Deletion failed... Should not happend")
|
||||||
// FAILED
|
// FAILED
|
||||||
self.CloseChangeset(csId, continuation)
|
self.CloseChangeset(csId, continuation)
|
||||||
})
|
})
|
||||||
}, true, reason)
|
}, {
|
||||||
|
isDeletionCS: true,
|
||||||
|
deletionReason: reason
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
|
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
|
||||||
|
@ -204,15 +211,20 @@ export class ChangesetHandler {
|
||||||
private OpenChangeset(
|
private OpenChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
continuation: (changesetId: string) => void,
|
continuation: (changesetId: string) => void,
|
||||||
isDeletionCS: boolean = false,
|
options?: {
|
||||||
deletionReason: string = undefined) {
|
isDeletionCS?: boolean,
|
||||||
|
deletionReason?: string,
|
||||||
|
onFail?: () => void
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
options = options ?? {}
|
||||||
|
options.isDeletionCS = options.isDeletionCS ?? false
|
||||||
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
||||||
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
|
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
|
||||||
if (isDeletionCS) {
|
if (options.isDeletionCS) {
|
||||||
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
|
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
|
||||||
if (deletionReason) {
|
if (options.deletionReason) {
|
||||||
comment += ": " + deletionReason;
|
comment += ": " + options.deletionReason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +233,7 @@ export class ChangesetHandler {
|
||||||
const metadata = [
|
const metadata = [
|
||||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||||
["comment", comment],
|
["comment", comment],
|
||||||
["deletion", isDeletionCS ? "yes" : undefined],
|
["deletion", options.isDeletionCS ? "yes" : undefined],
|
||||||
["theme", layout.id],
|
["theme", layout.id],
|
||||||
["language", Locale.language.data],
|
["language", Locale.language.data],
|
||||||
["host", window.location.host],
|
["host", window.location.host],
|
||||||
|
@ -244,7 +256,9 @@ export class ChangesetHandler {
|
||||||
}, function (err, response) {
|
}, function (err, response) {
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
console.log("err", err);
|
console.log("err", err);
|
||||||
alert("Could not upload change (opening failed). Please file a bug report")
|
if(options.onFail){
|
||||||
|
options.onFail()
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
continuation(response);
|
continuation(response);
|
||||||
|
@ -265,7 +279,7 @@ export class ChangesetHandler {
|
||||||
private AddChange(changesetId: string,
|
private AddChange(changesetId: string,
|
||||||
changesetXML: string,
|
changesetXML: string,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
continuation: ((changesetId: string, idMapping: any) => void),
|
continuation: ((changesetId: string) => void),
|
||||||
onFail: ((changesetId: string, reason: string) => void) = undefined) {
|
onFail: ((changesetId: string, reason: string) => void) = undefined) {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -280,9 +294,9 @@ export class ChangesetHandler {
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements);
|
ChangesetHandler.parseUploadChangesetResponse(response, allElements);
|
||||||
console.log("Uploaded changeset ", changesetId);
|
console.log("Uploaded changeset ", changesetId);
|
||||||
continuation(changesetId, mapping);
|
continuation(changesetId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default class UserDetails {
|
||||||
|
|
||||||
export class OsmConnection {
|
export class OsmConnection {
|
||||||
|
|
||||||
public static readonly _oauth_configs = {
|
public static readonly oauth_configs = {
|
||||||
"osm": {
|
"osm": {
|
||||||
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
|
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
|
||||||
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
|
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
|
||||||
|
@ -47,6 +47,7 @@ export class OsmConnection {
|
||||||
public auth;
|
public auth;
|
||||||
public userDetails: UIEventSource<UserDetails>;
|
public userDetails: UIEventSource<UserDetails>;
|
||||||
public isLoggedIn: UIEventSource<boolean>
|
public isLoggedIn: UIEventSource<boolean>
|
||||||
|
private fakeUser: boolean;
|
||||||
_dryRun: boolean;
|
_dryRun: boolean;
|
||||||
public preferencesHandler: OsmPreferences;
|
public preferencesHandler: OsmPreferences;
|
||||||
public changesetHandler: ChangesetHandler;
|
public changesetHandler: ChangesetHandler;
|
||||||
|
@ -59,20 +60,31 @@ export class OsmConnection {
|
||||||
url: string
|
url: string
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
|
constructor(dryRun: boolean,
|
||||||
|
fakeUser: boolean,
|
||||||
|
oauth_token: UIEventSource<string>,
|
||||||
// Used to keep multiple changesets open and to write to the correct changeset
|
// Used to keep multiple changesets open and to write to the correct changeset
|
||||||
layoutName: string,
|
layoutName: string,
|
||||||
singlePage: boolean = true,
|
singlePage: boolean = true,
|
||||||
osmConfiguration: "osm" | "osm-test" = 'osm'
|
osmConfiguration: "osm" | "osm-test" = 'osm'
|
||||||
) {
|
) {
|
||||||
|
this.fakeUser = fakeUser;
|
||||||
this._singlePage = singlePage;
|
this._singlePage = singlePage;
|
||||||
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
|
this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm;
|
||||||
console.debug("Using backend", this._oauth_config.url)
|
console.debug("Using backend", this._oauth_config.url)
|
||||||
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
|
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
|
||||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||||
|
|
||||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||||
this.userDetails.data.dryRun = dryRun;
|
this.userDetails.data.dryRun = dryRun || fakeUser;
|
||||||
|
if(fakeUser){
|
||||||
|
const ud = this.userDetails.data;
|
||||||
|
ud.csCount = 5678
|
||||||
|
ud.loggedIn= true;
|
||||||
|
ud.unreadMessages = 0
|
||||||
|
ud.name = "Fake user"
|
||||||
|
ud.totalMessages = 42;
|
||||||
|
}
|
||||||
const self =this;
|
const self =this;
|
||||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||||
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
|
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
|
||||||
|
@ -110,8 +122,10 @@ export class OsmConnection {
|
||||||
public UploadChangeset(
|
public UploadChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string) {
|
generateChangeXML: (csid: string) => string,
|
||||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
|
whenDone: (csId: string) => void,
|
||||||
|
onFail: () => {}) {
|
||||||
|
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||||
|
@ -136,6 +150,10 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttemptLogin() {
|
public AttemptLogin() {
|
||||||
|
if(this.fakeUser){
|
||||||
|
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||||
|
return;
|
||||||
|
}
|
||||||
const self = this;
|
const self = this;
|
||||||
console.log("Trying to log in...");
|
console.log("Trying to log in...");
|
||||||
this.updateAuthObject();
|
this.updateAuthObject();
|
||||||
|
|
|
@ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
|
|
||||||
export abstract class OsmObject {
|
export abstract class OsmObject {
|
||||||
|
|
||||||
protected static backendURL = "https://www.openstreetmap.org/"
|
private static defaultBackend = "https://www.openstreetmap.org/"
|
||||||
|
protected static backendURL = OsmObject.defaultBackend;
|
||||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
||||||
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
|
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
|
||||||
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
|
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
|
||||||
|
@ -37,15 +38,15 @@ export abstract class OsmObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||||
let src : UIEventSource<OsmObject>;
|
let src: UIEventSource<OsmObject>;
|
||||||
if (OsmObject.objectCache.has(id)) {
|
if (OsmObject.objectCache.has(id)) {
|
||||||
src = OsmObject.objectCache.get(id)
|
src = OsmObject.objectCache.get(id)
|
||||||
if(forceRefresh){
|
if (forceRefresh) {
|
||||||
src.setData(undefined)
|
src.setData(undefined)
|
||||||
}else{
|
} else {
|
||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
src = new UIEventSource<OsmObject>(undefined)
|
src = new UIEventSource<OsmObject>(undefined)
|
||||||
}
|
}
|
||||||
const splitted = id.split("/");
|
const splitted = id.split("/");
|
||||||
|
@ -157,7 +158,7 @@ export abstract class OsmObject {
|
||||||
const minlat = bounds[1][0]
|
const minlat = bounds[1][0]
|
||||||
const maxlat = bounds[0][0];
|
const maxlat = bounds[0][0];
|
||||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||||
Utils.downloadJson(url).then( data => {
|
Utils.downloadJson(url).then(data => {
|
||||||
const elements: any[] = data.elements;
|
const elements: any[] = data.elements;
|
||||||
const objects = OsmObject.ParseObjects(elements)
|
const objects = OsmObject.ParseObjects(elements)
|
||||||
callback(objects);
|
callback(objects);
|
||||||
|
@ -291,6 +292,7 @@ export abstract class OsmObject {
|
||||||
|
|
||||||
self.LoadData(element)
|
self.LoadData(element)
|
||||||
self.SaveExtraData(element, nodes);
|
self.SaveExtraData(element, nodes);
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
"_last_edit:contributor": element.user,
|
"_last_edit:contributor": element.user,
|
||||||
"_last_edit:contributor:uid": element.uid,
|
"_last_edit:contributor:uid": element.uid,
|
||||||
|
@ -299,6 +301,11 @@ export abstract class OsmObject {
|
||||||
"_version_number": element.version
|
"_version_number": element.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||||
|
self.tags["_backend"] = OsmObject.backendURL
|
||||||
|
meta["_backend"] = OsmObject.backendURL;
|
||||||
|
}
|
||||||
|
|
||||||
continuation(self, meta);
|
continuation(self, meta);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,6 +84,7 @@ export default class SimpleMetaTagger {
|
||||||
},
|
},
|
||||||
(feature => {
|
(feature => {
|
||||||
const units = State.state?.layoutToUse?.data?.units ?? [];
|
const units = State.state?.layoutToUse?.data?.units ?? [];
|
||||||
|
let rewritten = false;
|
||||||
for (const key in feature.properties) {
|
for (const key in feature.properties) {
|
||||||
if (!feature.properties.hasOwnProperty(key)) {
|
if (!feature.properties.hasOwnProperty(key)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -95,16 +96,23 @@ export default class SimpleMetaTagger {
|
||||||
const value = feature.properties[key]
|
const value = feature.properties[key]
|
||||||
const [, denomination] = unit.findDenomination(value)
|
const [, denomination] = unit.findDenomination(value)
|
||||||
let canonical = denomination?.canonicalValue(value) ?? undefined;
|
let canonical = denomination?.canonicalValue(value) ?? undefined;
|
||||||
console.log("Rewritten ", key, " from", value, "into", canonical)
|
if(canonical === value){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
|
||||||
if (canonical === undefined && !unit.eraseInvalid) {
|
if (canonical === undefined && !unit.eraseInvalid) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
feature.properties[key] = canonical;
|
feature.properties[key] = canonical;
|
||||||
|
rewritten = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if(rewritten){
|
||||||
|
State.state.allElements.getEventSourceById(feature.id).ping();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
* UIEventsource-wrapper around localStorage
|
* UIEventsource-wrapper around localStorage
|
||||||
*/
|
*/
|
||||||
export class LocalStorageSource {
|
export class LocalStorageSource {
|
||||||
|
|
||||||
|
static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{
|
||||||
|
return LocalStorageSource.Get(key).map(
|
||||||
|
str => {
|
||||||
|
if(str === undefined){
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
return JSON.parse(str)
|
||||||
|
}catch{
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}, [],
|
||||||
|
value => JSON.stringify(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
|
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -7,4 +7,6 @@ export default interface BaseLayer {
|
||||||
max_zoom: number,
|
max_zoom: number,
|
||||||
min_zoom: number;
|
min_zoom: number;
|
||||||
feature: any,
|
feature: any,
|
||||||
|
isBest?: boolean,
|
||||||
|
category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
|
|
||||||
public static vNumber = "0.8.3f";
|
public static vNumber = "0.8.4-rc3";
|
||||||
|
|
||||||
// The user journey states thresholds when a new feature gets unlocked
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
|
|
8
Models/TileRange.ts
Normal file
8
Models/TileRange.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export interface TileRange {
|
||||||
|
xstart: number,
|
||||||
|
ystart: number,
|
||||||
|
xend: number,
|
||||||
|
yend: number,
|
||||||
|
total: number,
|
||||||
|
zoomlevel: number
|
||||||
|
}
|
21
State.ts
21
State.ts
|
@ -19,6 +19,7 @@ import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||||
|
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the global state: a bunch of UI-event sources
|
* Contains the global state: a bunch of UI-event sources
|
||||||
|
@ -95,6 +96,12 @@ export default class State {
|
||||||
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
|
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
|
||||||
public readonly featureSwitchApiURL: UIEventSource<string>;
|
public readonly featureSwitchApiURL: UIEventSource<string>;
|
||||||
public readonly featureSwitchFilter: UIEventSource<boolean>;
|
public readonly featureSwitchFilter: UIEventSource<boolean>;
|
||||||
|
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
|
||||||
|
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
|
||||||
|
public featurePipeline: FeaturePipeline;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The map location: currently centered lat, lon and zoom
|
* The map location: currently centered lat, lon and zoom
|
||||||
|
@ -311,11 +318,24 @@ export default class State {
|
||||||
(b) => "" + b
|
(b) => "" + b
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false",
|
||||||
|
"If true, 'dryrun' mode is activated and a fake user account is loaded")
|
||||||
|
.map(str => str === "true", [], b => "" + b);
|
||||||
|
|
||||||
|
|
||||||
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
|
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
|
||||||
"backend",
|
"backend",
|
||||||
"osm",
|
"osm",
|
||||||
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
|
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.featureSwitchUserbadge.addCallbackAndRun(userbadge => {
|
||||||
|
if (!userbadge) {
|
||||||
|
this.featureSwitchAddNew.setData(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// Some other feature switches
|
// Some other feature switches
|
||||||
|
@ -341,6 +361,7 @@ export default class State {
|
||||||
|
|
||||||
this.osmConnection = new OsmConnection(
|
this.osmConnection = new OsmConnection(
|
||||||
this.featureSwitchIsTesting.data,
|
this.featureSwitchIsTesting.data,
|
||||||
|
this.featureSwitchFakeUser.data,
|
||||||
QueryParameters.GetQueryParameter(
|
QueryParameters.GetQueryParameter(
|
||||||
"oauth_token",
|
"oauth_token",
|
||||||
undefined,
|
undefined,
|
||||||
|
|
22
Svg.ts
22
Svg.ts
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@ import Loc from "../../Models/Loc";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
import {Map} from "leaflet";
|
import {Map} from "leaflet";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
|
||||||
export default class Minimap extends BaseUIElement {
|
export default class Minimap extends BaseUIElement {
|
||||||
|
|
||||||
|
@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement {
|
||||||
private readonly _location: UIEventSource<Loc>;
|
private readonly _location: UIEventSource<Loc>;
|
||||||
private _isInited = false;
|
private _isInited = false;
|
||||||
private _allowMoving: boolean;
|
private _allowMoving: boolean;
|
||||||
|
private readonly _leafletoptions: any;
|
||||||
|
|
||||||
constructor(options?: {
|
constructor(options?: {
|
||||||
background?: UIEventSource<BaseLayer>,
|
background?: UIEventSource<BaseLayer>,
|
||||||
location?: UIEventSource<Loc>,
|
location?: UIEventSource<Loc>,
|
||||||
allowMoving?: boolean
|
allowMoving?: boolean,
|
||||||
|
leafletOptions?: any
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement {
|
||||||
this._location = options?.location ?? new UIEventSource<Loc>(undefined)
|
this._location = options?.location ?? new UIEventSource<Loc>(undefined)
|
||||||
this._id = "minimap" + Minimap._nextId;
|
this._id = "minimap" + Minimap._nextId;
|
||||||
this._allowMoving = options.allowMoving ?? true;
|
this._allowMoving = options.allowMoving ?? true;
|
||||||
|
this._leafletoptions = options.leafletOptions ?? {}
|
||||||
Minimap._nextId++
|
Minimap._nextId++
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
const div = document.createElement("div")
|
const div = document.createElement("div")
|
||||||
div.id = this._id;
|
div.id = this._id;
|
||||||
|
@ -44,7 +48,6 @@ export default class Minimap extends BaseUIElement {
|
||||||
const self = this;
|
const self = this;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const resizeObserver = new ResizeObserver(_ => {
|
const resizeObserver = new ResizeObserver(_ => {
|
||||||
console.log("Change in size detected!")
|
|
||||||
self.InitMap();
|
self.InitMap();
|
||||||
self.leafletMap?.data?.invalidateSize()
|
self.leafletMap?.data?.invalidateSize()
|
||||||
});
|
});
|
||||||
|
@ -72,8 +75,8 @@ export default class Minimap extends BaseUIElement {
|
||||||
const location = this._location;
|
const location = this._location;
|
||||||
|
|
||||||
let currentLayer = this._background.data.layer()
|
let currentLayer = this._background.data.layer()
|
||||||
const map = L.map(this._id, {
|
const options = {
|
||||||
center: [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
||||||
zoom: location.data?.zoom ?? 2,
|
zoom: location.data?.zoom ?? 2,
|
||||||
layers: [currentLayer],
|
layers: [currentLayer],
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
|
@ -82,8 +85,14 @@ export default class Minimap extends BaseUIElement {
|
||||||
scrollWheelZoom: this._allowMoving,
|
scrollWheelZoom: this._allowMoving,
|
||||||
doubleClickZoom: this._allowMoving,
|
doubleClickZoom: this._allowMoving,
|
||||||
keyboard: this._allowMoving,
|
keyboard: this._allowMoving,
|
||||||
touchZoom: this._allowMoving
|
touchZoom: this._allowMoving,
|
||||||
});
|
// Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
|
||||||
|
fadeAnimation: this._allowMoving
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.Merge(this._leafletoptions, options)
|
||||||
|
|
||||||
|
const map = L.map(this._id, options);
|
||||||
|
|
||||||
map.setMaxBounds(
|
map.setMaxBounds(
|
||||||
[[-100, -200], [100, 200]]
|
[[-100, -200], [100, 200]]
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
||||||
export class Basemap {
|
export class Basemap {
|
||||||
|
|
||||||
|
@ -35,9 +36,8 @@ export class Basemap {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.map.attributionControl.setPrefix(
|
this.map.attributionControl.setPrefix(
|
||||||
"<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>");
|
"<span id='leaflet-attribution'>A</span>");
|
||||||
|
|
||||||
extraAttribution.AttachTo('leaflet-attribution')
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
currentLayer.addCallbackAndRun(layer => {
|
currentLayer.addCallbackAndRun(layer => {
|
||||||
|
@ -77,6 +77,7 @@ export class Basemap {
|
||||||
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
extraAttribution.AttachTo('leaflet-attribution')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
UI/BigComponents/ExportDataButton.ts
Normal file
21
UI/BigComponents/ExportDataButton.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import State from "../../State";
|
||||||
|
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
|
||||||
|
export class ExportDataButton extends Combine {
|
||||||
|
constructor() {
|
||||||
|
const t = Translations.t.general.download
|
||||||
|
const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold"))
|
||||||
|
.onClick(() => {
|
||||||
|
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline)
|
||||||
|
const name = State.state.layoutToUse.data.id;
|
||||||
|
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`);
|
||||||
|
})
|
||||||
|
|
||||||
|
super([button, t.licenseInfo.Clone().SetClass("link-underline")])
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,12 @@ import State from "../../State";
|
||||||
import BackgroundSelector from "./BackgroundSelector";
|
import BackgroundSelector from "./BackgroundSelector";
|
||||||
import LayerSelection from "./LayerSelection";
|
import LayerSelection from "./LayerSelection";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import Toggle from "../Input/Toggle";
|
||||||
|
import {ExportDataButton} from "./ExportDataButton";
|
||||||
|
|
||||||
export default class LayerControlPanel extends ScrollableFullScreen {
|
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||||
|
|
||||||
|
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
|
||||||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenTitle():BaseUIElement {
|
private static GenTitle(): BaseUIElement {
|
||||||
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
|
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GeneratePanel() : BaseUIElement {
|
private static GeneratePanel(): BaseUIElement {
|
||||||
let layerControlPanel: BaseUIElement = new FixedUiElement("");
|
const elements: BaseUIElement[] = []
|
||||||
|
|
||||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||||
layerControlPanel = new BackgroundSelector();
|
const backgroundSelector = new BackgroundSelector();
|
||||||
layerControlPanel.SetStyle("margin:1em");
|
backgroundSelector.SetStyle("margin:1em");
|
||||||
layerControlPanel.onClick(() => {
|
backgroundSelector.onClick(() => {
|
||||||
});
|
});
|
||||||
|
elements.push(backgroundSelector)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (State.state.filteredLayers.data.length > 1) {
|
elements.push(new Toggle(
|
||||||
const layerSelection = new LayerSelection(State.state.filteredLayers);
|
new LayerSelection(State.state.filteredLayers),
|
||||||
layerSelection.onClick(() => {
|
undefined,
|
||||||
});
|
State.state.filteredLayers.map(layers => layers.length > 1)
|
||||||
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
))
|
||||||
}
|
|
||||||
|
|
||||||
return layerControlPanel;
|
elements.push(new Toggle(
|
||||||
|
new ExportDataButton(),
|
||||||
|
undefined,
|
||||||
|
State.state.featureSwitchEnableExport
|
||||||
|
))
|
||||||
|
|
||||||
|
return new Combine(elements).SetClass("flex flex-col")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -74,7 +74,6 @@ export default class LayerSelection extends Combine {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
super(checkboxes)
|
super(checkboxes)
|
||||||
this.SetStyle("display:flex;flex-direction:column;")
|
this.SetStyle("display:flex;flex-direction:column;")
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,10 @@ export default class MoreScreen extends Combine {
|
||||||
let officialThemes = AllKnownLayouts.layoutsList
|
let officialThemes = AllKnownLayouts.layoutsList
|
||||||
|
|
||||||
let buttons = officialThemes.map((layout) => {
|
let buttons = officialThemes.map((layout) => {
|
||||||
|
if(layout === undefined){
|
||||||
|
console.trace("Layout is undefined")
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
||||||
if(layout.id === personal.id){
|
if(layout.id === personal.id){
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
|
|
@ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||||
import {Translation} from "../i18n/Translation";
|
import {Translation} from "../i18n/Translation";
|
||||||
|
import LocationInput from "../Input/LocationInput";
|
||||||
|
import {InputElement} from "../Input/InputElement";
|
||||||
|
import Loc from "../../Models/Loc";
|
||||||
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||||
|
@ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation";
|
||||||
* - A 'read your unread messages before adding a point'
|
* - A 'read your unread messages before adding a point'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*private*/
|
||||||
interface PresetInfo {
|
interface PresetInfo {
|
||||||
description: string | Translation,
|
description: string | Translation,
|
||||||
name: string | BaseUIElement,
|
name: string | BaseUIElement,
|
||||||
icon: BaseUIElement,
|
icon: () => BaseUIElement,
|
||||||
tags: Tag[],
|
tags: Tag[],
|
||||||
layerToAddTo: {
|
layerToAddTo: {
|
||||||
layerDef: LayerConfig,
|
layerDef: LayerConfig,
|
||||||
isDisplayed: UIEventSource<boolean>
|
isDisplayed: UIEventSource<boolean>
|
||||||
|
},
|
||||||
|
preciseInput?: {
|
||||||
|
preferredBackground?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle {
|
||||||
new SubtleButton(Svg.envelope_ui(),
|
new SubtleButton(Svg.envelope_ui(),
|
||||||
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
|
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||||
|
|
||||||
function createNewPoint(tags: any[]){
|
function createNewPoint(tags: any[], location: { lat: number, lon: number }) {
|
||||||
const loc = State.state.LastClickLocation.data;
|
let feature = State.state.changes.createElement(tags, location.lat, location.lon);
|
||||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
|
||||||
State.state.selectedElement.setData(feature);
|
State.state.selectedElement.setData(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||||
|
|
||||||
const addUi = new VariableUiElement(
|
const addUi = new VariableUiElement(
|
||||||
|
@ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle {
|
||||||
return presetsOverview
|
return presetsOverview
|
||||||
}
|
}
|
||||||
return SimpleAddUI.CreateConfirmButton(preset,
|
return SimpleAddUI.CreateConfirmButton(preset,
|
||||||
tags => {
|
(tags, location) => {
|
||||||
createNewPoint(tags)
|
createNewPoint(tags, location)
|
||||||
selectedPreset.setData(undefined)
|
selectedPreset.setData(undefined)
|
||||||
}, () => {
|
}, () => {
|
||||||
selectedPreset.setData(undefined)
|
selectedPreset.setData(undefined)
|
||||||
|
@ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle {
|
||||||
addUi,
|
addUi,
|
||||||
State.state.layerUpdater.runningQuery
|
State.state.layerUpdater.runningQuery
|
||||||
),
|
),
|
||||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert") ,
|
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
|
||||||
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||||
),
|
),
|
||||||
readYourMessages,
|
readYourMessages,
|
||||||
|
@ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static CreateConfirmButton(preset: PresetInfo,
|
private static CreateConfirmButton(preset: PresetInfo,
|
||||||
confirm: (tags: any[]) => void,
|
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
|
||||||
cancel: () => void): BaseUIElement {
|
cancel: () => void): BaseUIElement {
|
||||||
|
|
||||||
|
let location = State.state.LastClickLocation;
|
||||||
|
let preciseInput: InputElement<Loc> = undefined
|
||||||
|
if (preset.preciseInput !== undefined) {
|
||||||
|
const locationSrc = new UIEventSource({
|
||||||
|
lat: location.data.lat,
|
||||||
|
lon: location.data.lon,
|
||||||
|
zoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
let backgroundLayer = undefined;
|
||||||
|
if(preset.preciseInput.preferredBackground){
|
||||||
|
backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
||||||
|
}
|
||||||
|
|
||||||
|
preciseInput = new LocationInput({
|
||||||
|
mapBackground: backgroundLayer,
|
||||||
|
centerLocation:locationSrc
|
||||||
|
|
||||||
|
})
|
||||||
|
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
||||||
|
}
|
||||||
|
|
||||||
const confirmButton = new SubtleButton(preset.icon,
|
|
||||||
|
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
|
||||||
new Combine([
|
new Combine([
|
||||||
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
||||||
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col")
|
||||||
).SetClass("font-bold break-words")
|
).SetClass("font-bold break-words")
|
||||||
.onClick(() => confirm(preset.tags));
|
.onClick(() => {
|
||||||
|
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (preciseInput !== undefined) {
|
||||||
|
confirmButton = new Combine([preciseInput, confirmButton])
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLayerControl =
|
||||||
const openLayerControl =
|
|
||||||
new SubtleButton(
|
new SubtleButton(
|
||||||
Svg.layers_ui(),
|
Svg.layers_ui(),
|
||||||
new Combine([
|
new Combine([
|
||||||
|
@ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle {
|
||||||
Translations.t.general.add.openLayerControl
|
Translations.t.general.add.openLayerControl
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||||
|
|
||||||
const openLayerOrConfirm = new Toggle(
|
const openLayerOrConfirm = new Toggle(
|
||||||
confirmButton,
|
confirmButton,
|
||||||
openLayerControl,
|
openLayerControl,
|
||||||
|
@ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||||
Translations.t.general.cancel
|
Translations.t.general.cancel
|
||||||
).onClick(cancel )
|
).onClick(cancel)
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||||
State.state.osmConnection.userDetails.data.dryRun ?
|
State.state.osmConnection.userDetails.data.dryRun ?
|
||||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
|
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
||||||
openLayerOrConfirm,
|
openLayerOrConfirm,
|
||||||
cancelButton,
|
cancelButton,
|
||||||
preset.description,
|
preset.description,
|
||||||
|
@ -180,11 +212,11 @@ export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CreatePresetSelectButton(preset: PresetInfo){
|
private static CreatePresetSelectButton(preset: PresetInfo) {
|
||||||
|
|
||||||
const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false);
|
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false);
|
||||||
return new SubtleButton(
|
return new SubtleButton(
|
||||||
preset.icon,
|
preset.icon(),
|
||||||
new Combine([
|
new Combine([
|
||||||
Translations.t.general.add.addNew.Subs({
|
Translations.t.general.add.addNew.Subs({
|
||||||
category: preset.name
|
category: preset.name
|
||||||
|
@ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle {
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates the list with all the buttons.*/
|
* Generates the list with all the buttons.*/
|
||||||
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||||
const allButtons = [];
|
const allButtons = [];
|
||||||
for (const layer of State.state.filteredLayers.data) {
|
for (const layer of State.state.filteredLayers.data) {
|
||||||
|
|
||||||
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){
|
if (layer.isDisplayed.data === false && State.state.featureSwitchLayers) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presets = layer.layerDef.presets;
|
const presets = layer.layerDef.presets;
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
|
|
||||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||||
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||||
.SetClass("w-12 h-12 block relative");
|
.SetClass("w-12 h-12 block relative");
|
||||||
const presetInfo: PresetInfo = {
|
const presetInfo: PresetInfo = {
|
||||||
tags: preset.tags,
|
tags: preset.tags,
|
||||||
layerToAddTo: layer,
|
layerToAddTo: layer,
|
||||||
name: preset.title,
|
name: preset.title,
|
||||||
description: preset.description,
|
description: preset.description,
|
||||||
icon: icon
|
icon: icon,
|
||||||
|
preciseInput: preset.preciseInput
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.RegisterTriggers(element)
|
this.RegisterTriggers(element)
|
||||||
|
element.style.overflow = "hidden"
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
35
UI/Input/InputElementWrapper.ts
Normal file
35
UI/Input/InputElementWrapper.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {InputElement} from "./InputElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {Translation} from "../i18n/Translation";
|
||||||
|
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||||
|
|
||||||
|
export default class InputElementWrapper<T> extends InputElement<T> {
|
||||||
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
|
private readonly _inputElement: InputElement<T>;
|
||||||
|
private readonly _renderElement: BaseUIElement
|
||||||
|
|
||||||
|
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>) {
|
||||||
|
super()
|
||||||
|
this._inputElement = inputElement;
|
||||||
|
this.IsSelected = inputElement.IsSelected
|
||||||
|
const mapping = new Map<string, BaseUIElement>()
|
||||||
|
|
||||||
|
mapping.set(key, inputElement)
|
||||||
|
|
||||||
|
this._renderElement = new SubstitutedTranslation(translation, tags, mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
GetValue(): UIEventSource<T> {
|
||||||
|
return this._inputElement.GetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
IsValid(t: T): boolean {
|
||||||
|
return this._inputElement.IsValid(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._renderElement.ConstructElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
185
UI/Input/LengthInput.ts
Normal file
185
UI/Input/LengthInput.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import {InputElement} from "./InputElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
import Loc from "../../Models/Loc";
|
||||||
|
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||||
|
import DirectionInput from "./DirectionInput";
|
||||||
|
import {RadioButton} from "./RadioButton";
|
||||||
|
import {FixedInputElement} from "./FixedInputElement";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a length after clicking on the minimap, in meters
|
||||||
|
*/
|
||||||
|
export default class LengthInput extends InputElement<string> {
|
||||||
|
private readonly _location: UIEventSource<Loc>;
|
||||||
|
|
||||||
|
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
private readonly value: UIEventSource<string>;
|
||||||
|
private background;
|
||||||
|
|
||||||
|
constructor(mapBackground: UIEventSource<any>,
|
||||||
|
location: UIEventSource<Loc>,
|
||||||
|
value?: UIEventSource<string>) {
|
||||||
|
super();
|
||||||
|
this._location = location;
|
||||||
|
this.value = value ?? new UIEventSource<string>(undefined);
|
||||||
|
this.background = mapBackground;
|
||||||
|
this.SetClass("block")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
GetValue(): UIEventSource<string> {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsValid(str: string): boolean {
|
||||||
|
const t = Number(str)
|
||||||
|
return !isNaN(t) && t >= 0 && t <= 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
const modeElement = new RadioButton([
|
||||||
|
new FixedInputElement("Measure", "measure"),
|
||||||
|
new FixedInputElement("Move", "move")
|
||||||
|
])
|
||||||
|
// @ts-ignore
|
||||||
|
let map = undefined
|
||||||
|
if (!Utils.runningFromConsole) {
|
||||||
|
map = DirectionInput.constructMinimap({
|
||||||
|
background: this.background,
|
||||||
|
allowMoving: false,
|
||||||
|
location: this._location,
|
||||||
|
leafletOptions: {
|
||||||
|
tap: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const element = new Combine([
|
||||||
|
new Combine([Svg.length_crosshair_svg().SetStyle(
|
||||||
|
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`)
|
||||||
|
])
|
||||||
|
.SetClass("block length-crosshair-svg relative")
|
||||||
|
.SetStyle("z-index: 1000; visibility: hidden"),
|
||||||
|
map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"),
|
||||||
|
])
|
||||||
|
.SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden")
|
||||||
|
.ConstructElement()
|
||||||
|
|
||||||
|
|
||||||
|
this.RegisterTriggers(element, map?.leafletMap)
|
||||||
|
element.style.overflow = "hidden"
|
||||||
|
element.style.display = "block"
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource<L.Map>) {
|
||||||
|
|
||||||
|
let firstClickXY: [number, number] = undefined
|
||||||
|
let lastClickXY: [number, number] = undefined
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
|
||||||
|
function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) {
|
||||||
|
if (x === undefined || y === undefined) {
|
||||||
|
// Touch end
|
||||||
|
firstClickXY = undefined;
|
||||||
|
lastClickXY = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = htmlElement.getBoundingClientRect();
|
||||||
|
// From the central part of location
|
||||||
|
const dx = x - rect.left;
|
||||||
|
const dy = y - rect.top;
|
||||||
|
if (isDown) {
|
||||||
|
if (lastClickXY === undefined && firstClickXY === undefined) {
|
||||||
|
firstClickXY = [dx, dy];
|
||||||
|
} else if (firstClickXY !== undefined && lastClickXY === undefined) {
|
||||||
|
lastClickXY = [dx, dy]
|
||||||
|
} else if (firstClickXY !== undefined && lastClickXY !== undefined) {
|
||||||
|
// we measure again
|
||||||
|
firstClickXY = [dx, dy]
|
||||||
|
lastClickXY = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isUp) {
|
||||||
|
const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0]))
|
||||||
|
if (distance > 15) {
|
||||||
|
lastClickXY = [dx, dy]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (lastClickXY !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement
|
||||||
|
|
||||||
|
const measurementCrosshairInner: HTMLElement = <HTMLElement>measurementCrosshair.firstChild
|
||||||
|
if (firstClickXY === undefined) {
|
||||||
|
measurementCrosshair.style.visibility = "hidden"
|
||||||
|
} else {
|
||||||
|
measurementCrosshair.style.visibility = "unset"
|
||||||
|
measurementCrosshair.style.left = firstClickXY[0] + "px";
|
||||||
|
measurementCrosshair.style.top = firstClickXY[1] + "px"
|
||||||
|
|
||||||
|
const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI;
|
||||||
|
const angleGeo = (angle + 270) % 360
|
||||||
|
measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`;
|
||||||
|
|
||||||
|
const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0]))
|
||||||
|
measurementCrosshairInner.style.width = (distance * 2) + "px"
|
||||||
|
measurementCrosshairInner.style.marginLeft = -distance + "px"
|
||||||
|
measurementCrosshairInner.style.marginTop = -distance + "px"
|
||||||
|
|
||||||
|
|
||||||
|
const leaflet = leafletMap?.data
|
||||||
|
if (leaflet) {
|
||||||
|
const first = leaflet.layerPointToLatLng(firstClickXY)
|
||||||
|
const last = leaflet.layerPointToLatLng([dx, dy])
|
||||||
|
const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100
|
||||||
|
self.value.setData("" + geoDist)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
htmlElement.ontouchstart = (ev: TouchEvent) => {
|
||||||
|
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.ontouchmove = (ev: TouchEvent) => {
|
||||||
|
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.ontouchend = (ev: TouchEvent) => {
|
||||||
|
onPosChange(undefined, undefined, false, true);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.onmousedown = (ev: MouseEvent) => {
|
||||||
|
onPosChange(ev.clientX, ev.clientY, true);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.onmouseup = (ev) => {
|
||||||
|
onPosChange(ev.clientX, ev.clientY, false, true);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.onmousemove = (ev: MouseEvent) => {
|
||||||
|
onPosChange(ev.clientX, ev.clientY, false);
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
76
UI/Input/LocationInput.ts
Normal file
76
UI/Input/LocationInput.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {InputElement} from "./InputElement";
|
||||||
|
import Loc from "../../Models/Loc";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import Minimap from "../Base/Minimap";
|
||||||
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import State from "../../State";
|
||||||
|
|
||||||
|
export default class LocationInput extends InputElement<Loc> {
|
||||||
|
|
||||||
|
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
private _centerLocation: UIEventSource<Loc>;
|
||||||
|
private readonly mapBackground : UIEventSource<BaseLayer>;
|
||||||
|
|
||||||
|
constructor(options?: {
|
||||||
|
mapBackground?: UIEventSource<BaseLayer>,
|
||||||
|
centerLocation?: UIEventSource<Loc>,
|
||||||
|
}) {
|
||||||
|
super();
|
||||||
|
options = options ?? {}
|
||||||
|
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
||||||
|
this._centerLocation = options.centerLocation;
|
||||||
|
|
||||||
|
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
|
||||||
|
this.SetClass("block h-full")
|
||||||
|
}
|
||||||
|
|
||||||
|
GetValue(): UIEventSource<Loc> {
|
||||||
|
return this._centerLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsValid(t: Loc): boolean {
|
||||||
|
return t !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
const map = new Minimap(
|
||||||
|
{
|
||||||
|
location: this._centerLocation,
|
||||||
|
background: this.mapBackground
|
||||||
|
}
|
||||||
|
)
|
||||||
|
map.leafletMap.addCallbackAndRunD(leaflet => {
|
||||||
|
console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15))
|
||||||
|
leaflet.setMaxBounds(
|
||||||
|
leaflet.getBounds().pad(0.15)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mapBackground.map(layer => {
|
||||||
|
|
||||||
|
const leaflet = map.leafletMap.data
|
||||||
|
if (leaflet === undefined || layer === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaflet.setMaxZoom(layer.max_zoom)
|
||||||
|
leaflet.setMinZoom(layer.max_zoom - 3)
|
||||||
|
leaflet.setZoom(layer.max_zoom - 1)
|
||||||
|
|
||||||
|
}, [map.leafletMap])
|
||||||
|
return new Combine([
|
||||||
|
new Combine([
|
||||||
|
Svg.crosshair_empty_ui()
|
||||||
|
.SetClass("block relative")
|
||||||
|
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
|
||||||
|
]).SetClass("block w-0 h-0 z-10 relative")
|
||||||
|
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
|
||||||
|
map
|
||||||
|
.SetClass("z-0 relative block w-full h-full bg-gray-100")
|
||||||
|
|
||||||
|
]).ConstructElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> {
|
||||||
const block = document.createElement("div")
|
const block = document.createElement("div")
|
||||||
block.appendChild(input)
|
block.appendChild(input)
|
||||||
block.appendChild(label)
|
block.appendChild(label)
|
||||||
block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1")
|
block.classList.add("flex","w-full","border", "rounded-3xl", "border-gray-400","m-1")
|
||||||
wrappers.push(block)
|
wrappers.push(block)
|
||||||
|
|
||||||
form.appendChild(block)
|
form.appendChild(block)
|
||||||
|
|
|
@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
|
||||||
this.SetClass("form-text-field")
|
this.SetClass("form-text-field")
|
||||||
let inputEl: HTMLElement
|
let inputEl: HTMLElement
|
||||||
if (options.htmlType === "area") {
|
if (options.htmlType === "area") {
|
||||||
|
this.SetClass("w-full box-border max-w-full")
|
||||||
const el = document.createElement("textarea")
|
const el = document.createElement("textarea")
|
||||||
el.placeholder = placeholder
|
el.placeholder = placeholder
|
||||||
el.rows = options.textAreaRows
|
el.rows = options.textAreaRows
|
||||||
el.cols = 50
|
el.cols = 50
|
||||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
|
||||||
inputEl = el;
|
inputEl = el;
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement("input")
|
const el = document.createElement("input")
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {Utils} from "../../Utils";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import LengthInput from "./LengthInput";
|
||||||
|
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||||
|
|
||||||
interface TextFieldDef {
|
interface TextFieldDef {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -21,14 +23,16 @@ interface TextFieldDef {
|
||||||
reformat?: ((s: string, country?: () => string) => string),
|
reformat?: ((s: string, country?: () => string) => string),
|
||||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||||
location: [number, number],
|
location: [number, number],
|
||||||
mapBackgroundLayer?: UIEventSource<any>
|
mapBackgroundLayer?: UIEventSource<any>,
|
||||||
|
args: (string | number | boolean)[]
|
||||||
|
feature?: any
|
||||||
}) => InputElement<string>,
|
}) => InputElement<string>,
|
||||||
|
|
||||||
inputmode?: string
|
inputmode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ValidatedTextField {
|
export default class ValidatedTextField {
|
||||||
|
|
||||||
|
public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any
|
||||||
|
|
||||||
public static tpList: TextFieldDef[] = [
|
public static tpList: TextFieldDef[] = [
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
|
@ -63,6 +67,83 @@ export default class ValidatedTextField {
|
||||||
return [year, month, day].join('-');
|
return [year, month, day].join('-');
|
||||||
},
|
},
|
||||||
(value) => new SimpleDatePicker(value)),
|
(value) => new SimpleDatePicker(value)),
|
||||||
|
ValidatedTextField.tp(
|
||||||
|
"direction",
|
||||||
|
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
|
||||||
|
(str) => {
|
||||||
|
str = "" + str;
|
||||||
|
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
|
||||||
|
}, str => str,
|
||||||
|
(value, options) => {
|
||||||
|
const args = options.args ?? []
|
||||||
|
let zoom = 19
|
||||||
|
if (args[0]) {
|
||||||
|
zoom = Number(args[0])
|
||||||
|
if (isNaN(zoom)) {
|
||||||
|
throw "Invalid zoom level for argument at 'length'-input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const location = new UIEventSource<Loc>({
|
||||||
|
lat: options.location[0],
|
||||||
|
lon: options.location[1],
|
||||||
|
zoom: zoom
|
||||||
|
})
|
||||||
|
if (args[1]) {
|
||||||
|
// We have a prefered map!
|
||||||
|
options.mapBackgroundLayer = ValidatedTextField.bestLayerAt(
|
||||||
|
location, new UIEventSource<string[]>(args[1].split(","))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const di = new DirectionInput(options.mapBackgroundLayer, location, value)
|
||||||
|
di.SetStyle("height: 20rem;");
|
||||||
|
|
||||||
|
return di;
|
||||||
|
},
|
||||||
|
"numeric"
|
||||||
|
),
|
||||||
|
ValidatedTextField.tp(
|
||||||
|
"length",
|
||||||
|
"A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma seperated) ], e.g. `[\"21\", \"map,photo\"]",
|
||||||
|
(str) => {
|
||||||
|
const t = Number(str)
|
||||||
|
return !isNaN(t)
|
||||||
|
},
|
||||||
|
str => str,
|
||||||
|
(value, options) => {
|
||||||
|
const args = options.args ?? []
|
||||||
|
let zoom = 19
|
||||||
|
if (args[0]) {
|
||||||
|
zoom = Number(args[0])
|
||||||
|
if (isNaN(zoom)) {
|
||||||
|
throw "Invalid zoom level for argument at 'length'-input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit of a hack: we project the centerpoint to the closes point on the road - if available
|
||||||
|
if(options.feature){
|
||||||
|
const lonlat: [number, number] = [...options.location]
|
||||||
|
lonlat.reverse()
|
||||||
|
options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
|
||||||
|
options.location.reverse()
|
||||||
|
}
|
||||||
|
options.feature
|
||||||
|
|
||||||
|
const location = new UIEventSource<Loc>({
|
||||||
|
lat: options.location[0],
|
||||||
|
lon: options.location[1],
|
||||||
|
zoom: zoom
|
||||||
|
})
|
||||||
|
if (args[1]) {
|
||||||
|
// We have a prefered map!
|
||||||
|
options.mapBackgroundLayer = ValidatedTextField.bestLayerAt(
|
||||||
|
location, new UIEventSource<string[]>(args[1].split(","))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const li = new LengthInput(options.mapBackgroundLayer, location, value)
|
||||||
|
li.SetStyle("height: 20rem;")
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
),
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"wikidata",
|
"wikidata",
|
||||||
"A wikidata identifier, e.g. Q42",
|
"A wikidata identifier, e.g. Q42",
|
||||||
|
@ -113,22 +194,6 @@ export default class ValidatedTextField {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
"numeric"),
|
"numeric"),
|
||||||
ValidatedTextField.tp(
|
|
||||||
"direction",
|
|
||||||
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
|
|
||||||
(str) => {
|
|
||||||
str = "" + str;
|
|
||||||
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
|
|
||||||
}, str => str,
|
|
||||||
(value, options) => {
|
|
||||||
return new DirectionInput(options.mapBackgroundLayer , new UIEventSource<Loc>({
|
|
||||||
lat: options.location[0],
|
|
||||||
lon: options.location[1],
|
|
||||||
zoom: 19
|
|
||||||
}),value);
|
|
||||||
},
|
|
||||||
"numeric"
|
|
||||||
),
|
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"float",
|
"float",
|
||||||
"A decimal",
|
"A decimal",
|
||||||
|
@ -222,6 +287,7 @@ export default class ValidatedTextField {
|
||||||
* {string (typename) --> TextFieldDef}
|
* {string (typename) --> TextFieldDef}
|
||||||
*/
|
*/
|
||||||
public static AllTypes = ValidatedTextField.allTypesDict();
|
public static AllTypes = ValidatedTextField.allTypesDict();
|
||||||
|
|
||||||
public static InputForType(type: string, options?: {
|
public static InputForType(type: string, options?: {
|
||||||
placeholder?: string | BaseUIElement,
|
placeholder?: string | BaseUIElement,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
|
@ -233,7 +299,9 @@ export default class ValidatedTextField {
|
||||||
country?: () => string,
|
country?: () => string,
|
||||||
location?: [number /*lat*/, number /*lon*/],
|
location?: [number /*lat*/, number /*lon*/],
|
||||||
mapBackgroundLayer?: UIEventSource<any>,
|
mapBackgroundLayer?: UIEventSource<any>,
|
||||||
unit?: Unit
|
unit?: Unit,
|
||||||
|
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
||||||
|
feature?: any
|
||||||
}): InputElement<string> {
|
}): InputElement<string> {
|
||||||
options = options ?? {};
|
options = options ?? {};
|
||||||
options.placeholder = options.placeholder ?? type;
|
options.placeholder = options.placeholder ?? type;
|
||||||
|
@ -247,7 +315,7 @@ export default class ValidatedTextField {
|
||||||
if (str === undefined) {
|
if (str === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(options.unit) {
|
if (options.unit) {
|
||||||
str = options.unit.stripUnitParts(str)
|
str = options.unit.stripUnitParts(str)
|
||||||
}
|
}
|
||||||
return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country);
|
return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country);
|
||||||
|
@ -268,7 +336,7 @@ export default class ValidatedTextField {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.unit) {
|
if (options.unit) {
|
||||||
// We need to apply a unit.
|
// We need to apply a unit.
|
||||||
// This implies:
|
// This implies:
|
||||||
// We have to create a dropdown with applicable denominations, and fuse those values
|
// We have to create a dropdown with applicable denominations, and fuse those values
|
||||||
|
@ -282,23 +350,22 @@ export default class ValidatedTextField {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
unitDropDown.GetValue().setData(unit.defaultDenom)
|
unitDropDown.GetValue().setData(unit.defaultDenom)
|
||||||
unitDropDown.SetStyle("width: min-content")
|
unitDropDown.SetClass("w-min")
|
||||||
|
|
||||||
input = new CombinedInputElement(
|
input = new CombinedInputElement(
|
||||||
input,
|
input,
|
||||||
unitDropDown,
|
unitDropDown,
|
||||||
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
|
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
|
||||||
(text, denom) => denom?.canonicalValue(text, true) ?? undefined,
|
(text, denom) => denom?.canonicalValue(text, true) ?? undefined,
|
||||||
(valueWithDenom: string) => {
|
(valueWithDenom: string) => {
|
||||||
// Take the value from OSM and feed it into the textfield and the dropdown
|
// Take the value from OSM and feed it into the textfield and the dropdown
|
||||||
const withDenom = unit.findDenomination(valueWithDenom);
|
const withDenom = unit.findDenomination(valueWithDenom);
|
||||||
if(withDenom === undefined)
|
if (withDenom === undefined) {
|
||||||
{
|
|
||||||
// Not a valid value at all - we give it undefined and leave the details up to the other elements
|
// Not a valid value at all - we give it undefined and leave the details up to the other elements
|
||||||
return [undefined, undefined]
|
return [undefined, undefined]
|
||||||
}
|
}
|
||||||
const [strippedText, denom] = withDenom
|
const [strippedText, denom] = withDenom
|
||||||
if(strippedText === undefined){
|
if (strippedText === undefined) {
|
||||||
return [undefined, undefined]
|
return [undefined, undefined]
|
||||||
}
|
}
|
||||||
return [strippedText, denom]
|
return [strippedText, denom]
|
||||||
|
@ -306,18 +373,20 @@ export default class ValidatedTextField {
|
||||||
).SetClass("flex")
|
).SetClass("flex")
|
||||||
}
|
}
|
||||||
if (tp.inputHelper) {
|
if (tp.inputHelper) {
|
||||||
const helper = tp.inputHelper(input.GetValue(), {
|
const helper = tp.inputHelper(input.GetValue(), {
|
||||||
location: options.location,
|
location: options.location,
|
||||||
mapBackgroundLayer: options.mapBackgroundLayer
|
mapBackgroundLayer: options.mapBackgroundLayer,
|
||||||
|
args: options.args,
|
||||||
|
feature: options.feature
|
||||||
})
|
})
|
||||||
input = new CombinedInputElement(input, helper,
|
input = new CombinedInputElement(input, helper,
|
||||||
(a, _) => a, // We can ignore b, as they are linked earlier
|
(a, _) => a, // We can ignore b, as they are linked earlier
|
||||||
a => [a, a]
|
a => [a, a]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HelpText(): string {
|
public static HelpText(): string {
|
||||||
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
|
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
|
||||||
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
|
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
|
||||||
|
@ -329,7 +398,9 @@ export default class ValidatedTextField {
|
||||||
reformat?: ((s: string, country?: () => string) => string),
|
reformat?: ((s: string, country?: () => string) => string),
|
||||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||||
location: [number, number],
|
location: [number, number],
|
||||||
mapBackgroundLayer: UIEventSource<any>
|
mapBackgroundLayer: UIEventSource<any>,
|
||||||
|
args: string[],
|
||||||
|
feature: any
|
||||||
}) => InputElement<string>,
|
}) => InputElement<string>,
|
||||||
inputmode?: string): TextFieldDef {
|
inputmode?: string): TextFieldDef {
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||||
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
|
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
|
||||||
const titleIcons = new Combine(
|
const titleIcons = new Combine(
|
||||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
||||||
"block w-8 h-8 align-baseline box-content sm:p-0.5")
|
"block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem;")
|
||||||
))
|
))
|
||||||
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||||
|
|
||||||
|
|
|
@ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
||||||
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
||||||
}
|
}
|
||||||
super(tagsSource.map(tags => {
|
super(tagsSource.map(tags => {
|
||||||
if(tags === undefined){
|
if (tags === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(configuration.condition){
|
if (configuration.condition) {
|
||||||
if(!configuration.condition.matchesProperties(tags)){
|
if (!configuration.condition.matchesProperties(tags)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
|
|
||||||
if(trs.length === 0){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
|
|
||||||
if(valuesToRender.length === 1){
|
|
||||||
return valuesToRender[0];
|
|
||||||
}else if(valuesToRender.length > 1){
|
|
||||||
return new List(valuesToRender)
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
|
|
||||||
|
|
||||||
this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer")
|
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
|
||||||
|
if (trs.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
|
||||||
|
if (valuesToRender.length === 1) {
|
||||||
|
return valuesToRender[0];
|
||||||
|
} else if (valuesToRender.length > 1) {
|
||||||
|
return new List(valuesToRender)
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
|
||||||
|
|
||||||
|
this.SetClass("flex items-center flex-row text-lg link-underline")
|
||||||
this.SetStyle("word-wrap: anywhere;");
|
this.SetStyle("word-wrap: anywhere;");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import {DropDown} from "../Input/DropDown";
|
import {DropDown} from "../Input/DropDown";
|
||||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||||
|
import InputElementWrapper from "../Input/InputElementWrapper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the question element.
|
* Shows the question element.
|
||||||
|
@ -128,7 +129,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
}
|
}
|
||||||
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
|
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
|
||||||
}
|
}
|
||||||
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data);
|
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource);
|
||||||
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
||||||
|
|
||||||
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
||||||
|
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
(t0, t1) => t1.isEquivalent(t0));
|
(t0, t1) => t1.isEquivalent(t0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> {
|
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> {
|
||||||
const freeform = configuration.freeform;
|
const freeform = configuration.freeform;
|
||||||
if (freeform === undefined) {
|
if (freeform === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -328,20 +329,34 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
const tagsData = tags.data;
|
||||||
|
const feature = State.state.allElements.ContainingFeatures.get(tagsData.id)
|
||||||
|
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
||||||
isValid: (str) => (str.length <= 255),
|
isValid: (str) => (str.length <= 255),
|
||||||
country: () => tagsData._country,
|
country: () => tagsData._country,
|
||||||
location: [tagsData._lat, tagsData._lon],
|
location: [tagsData._lat, tagsData._lon],
|
||||||
mapBackgroundLayer: State.state.backgroundLayer,
|
mapBackgroundLayer: State.state.backgroundLayer,
|
||||||
unit: applicableUnit
|
unit: applicableUnit,
|
||||||
|
args: configuration.freeform.helperArgs,
|
||||||
|
feature: feature
|
||||||
});
|
});
|
||||||
|
|
||||||
input.GetValue().setData(tagsData[configuration.freeform.key]);
|
input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
||||||
|
|
||||||
return new InputElementMap(
|
let inputTagsFilter : InputElement<TagsFilter> = new InputElementMap(
|
||||||
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
||||||
pickString, toString
|
pickString, toString
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(freeform.inline){
|
||||||
|
|
||||||
|
inputTagsFilter.SetClass("w-16-imp")
|
||||||
|
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
|
||||||
|
inputTagsFilter.SetClass("block")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputTagsFilter;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,9 +80,7 @@ export default class ShowDataLayer {
|
||||||
|
|
||||||
if (zoomToFeatures) {
|
if (zoomToFeatures) {
|
||||||
try {
|
try {
|
||||||
|
mp.fitBounds(geoLayer.getBounds(), {animate: false})
|
||||||
mp.fitBounds(geoLayer.getBounds())
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
@ -148,7 +146,9 @@ export default class ShowDataLayer {
|
||||||
const popup = L.popup({
|
const popup = L.popup({
|
||||||
autoPan: true,
|
autoPan: true,
|
||||||
closeOnEscapeKey: true,
|
closeOnEscapeKey: true,
|
||||||
closeButton: false
|
closeButton: false,
|
||||||
|
autoPanPaddingTopLeft: [15,15],
|
||||||
|
|
||||||
}, leafletLayer);
|
}, leafletLayer);
|
||||||
|
|
||||||
leafletLayer.bindPopup(popup);
|
leafletLayer.bindPopup(popup);
|
||||||
|
|
|
@ -39,7 +39,8 @@ export default class SpecialVisualizations {
|
||||||
static constructMiniMap: (options?: {
|
static constructMiniMap: (options?: {
|
||||||
background?: UIEventSource<BaseLayer>,
|
background?: UIEventSource<BaseLayer>,
|
||||||
location?: UIEventSource<Loc>,
|
location?: UIEventSource<Loc>,
|
||||||
allowMoving?: boolean
|
allowMoving?: boolean,
|
||||||
|
leafletOptions?: any
|
||||||
}) => BaseUIElement;
|
}) => BaseUIElement;
|
||||||
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
|
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
|
||||||
public static specialVisualizations: SpecialVisualization[] =
|
public static specialVisualizations: SpecialVisualization[] =
|
||||||
|
@ -369,7 +370,6 @@ export default class SpecialVisualizations {
|
||||||
if (unit === undefined) {
|
if (unit === undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return unit.asHumanLongValue(value);
|
return unit.asHumanLongValue(value);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -379,6 +379,7 @@ export default class SpecialVisualizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||||
private static GenHelpMessage() {
|
private static GenHelpMessage() {
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||||
import Combine from "./Base/Combine";
|
import Combine from "./Base/Combine";
|
||||||
|
import BaseUIElement from "./BaseUIElement";
|
||||||
|
|
||||||
export class SubstitutedTranslation extends VariableUiElement {
|
export class SubstitutedTranslation extends VariableUiElement {
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
translation: Translation,
|
translation: Translation,
|
||||||
tagsSource: UIEventSource<any>) {
|
tagsSource: UIEventSource<any>,
|
||||||
|
mapping: Map<string, BaseUIElement> = undefined) {
|
||||||
|
|
||||||
|
const extraMappings: SpecialVisualization[] = [];
|
||||||
|
|
||||||
|
mapping?.forEach((value, key) => {
|
||||||
|
console.log("KV:", key, value)
|
||||||
|
extraMappings.push(
|
||||||
|
{
|
||||||
|
funcName: key,
|
||||||
|
constr: (() => {
|
||||||
|
return value
|
||||||
|
}),
|
||||||
|
docs: "Dynamically injected input element",
|
||||||
|
args: [],
|
||||||
|
example: ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
super(
|
super(
|
||||||
Locale.language.map(language => {
|
Locale.language.map(language => {
|
||||||
const txt = translation.textFor(language)
|
let txt = translation.textFor(language);
|
||||||
if (txt === undefined) {
|
if (txt === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map(
|
mapping?.forEach((_, key) => {
|
||||||
|
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map(
|
||||||
proto => {
|
proto => {
|
||||||
if (proto.fixed !== undefined) {
|
if (proto.fixed !== undefined) {
|
||||||
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
||||||
|
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
this.SetClass("w-full")
|
this.SetClass("w-full")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static ExtractSpecialComponents(template: string): {
|
public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
|
||||||
fixed?: string, special?: {
|
fixed?: string,
|
||||||
|
special?: {
|
||||||
func: SpecialVisualization,
|
func: SpecialVisualization,
|
||||||
args: string[],
|
args: string[],
|
||||||
style: string
|
style: string
|
||||||
}
|
}
|
||||||
}[] {
|
}[] {
|
||||||
|
|
||||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
|
if (extraMappings.length > 0) {
|
||||||
|
|
||||||
|
console.log("Extra mappings are", extraMappings)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) {
|
||||||
|
|
||||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||||
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
||||||
if (matched != null) {
|
if (matched != null) {
|
||||||
|
|
||||||
// We found a special component that should be brought to live
|
// We found a special component that should be brought to live
|
||||||
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]);
|
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings);
|
||||||
const argument = matched[2].trim();
|
const argument = matched[2].trim();
|
||||||
const style = matched[3]?.substring(1) ?? ""
|
const style = matched[3]?.substring(1) ?? ""
|
||||||
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]);
|
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
|
||||||
const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
|
const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
|
||||||
if (argument.length > 0) {
|
if (argument.length > 0) {
|
||||||
const realArgs = argument.split(",").map(str => str.trim());
|
const realArgs = argument.split(",").map(str => str.trim());
|
||||||
|
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
element = {special:{
|
element = {
|
||||||
args: args,
|
special: {
|
||||||
style: style,
|
args: args,
|
||||||
func: knownSpecial
|
style: style,
|
||||||
}}
|
func: knownSpecial
|
||||||
|
}
|
||||||
|
}
|
||||||
return [...partBefore, element, ...partAfter]
|
return [...partBefore, element, ...partAfter]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
Utils.ts
19
Utils.ts
|
@ -1,4 +1,5 @@
|
||||||
import * as colors from "./assets/colors.json"
|
import * as colors from "./assets/colors.json"
|
||||||
|
import {TileRange} from "./Models/TileRange";
|
||||||
|
|
||||||
export class Utils {
|
export class Utils {
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@ export class Utils {
|
||||||
}
|
}
|
||||||
return newArr;
|
return newArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MergeTags(a: any, b: any) {
|
public static MergeTags(a: any, b: any) {
|
||||||
const t = {};
|
const t = {};
|
||||||
for (const k in a) {
|
for (const k in a) {
|
||||||
|
@ -450,14 +451,12 @@ export class Utils {
|
||||||
b: parseInt(hex.substr(5, 2), 16),
|
b: parseInt(hex.substr(5, 2), 16),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static setDefaults(options, defaults){
|
||||||
|
for (let key in defaults){
|
||||||
|
if (!(key in options)) options[key] = defaults[key];
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TileRange {
|
|
||||||
xstart: number,
|
|
||||||
ystart: number,
|
|
||||||
xend: number,
|
|
||||||
yend: number,
|
|
||||||
total: number,
|
|
||||||
zoomlevel: number
|
|
||||||
|
|
||||||
}
|
|
|
@ -73,7 +73,10 @@
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"amenity=public_bookcase"
|
"amenity=public_bookcase"
|
||||||
]
|
],
|
||||||
|
"preciseInput": {
|
||||||
|
"preferredBackground": "photo"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
|
@ -139,7 +142,8 @@
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "capacity",
|
"key": "capacity",
|
||||||
"type": "nat"
|
"type": "nat",
|
||||||
|
"inline": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
83
assets/svg/crosshair-empty.svg
Normal file
83
assets/svg/crosshair-empty.svg
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 26.458333 26.458334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||||
|
sodipodi:docname="crosshair-empty.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="5.6568542"
|
||||||
|
inkscape:cx="22.669779"
|
||||||
|
inkscape:cy="52.573519"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="g848"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="999"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="13.229167,23.859748"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide815"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="14.944824,13.229167"
|
||||||
|
orientation="0,1"
|
||||||
|
id="guide817"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-270.54165)">
|
||||||
|
<g
|
||||||
|
id="g848">
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#5555ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 13.162109,273.57617 c -5.6145729,0 -10.1933596,4.58074 -10.193359,10.19531 -6e-7,5.61458 4.5787861,10.19336 10.193359,10.19336 5.614574,0 10.195313,-4.57878 10.195313,-10.19336 0,-5.61457 -4.580739,-10.19531 -10.195313,-10.19531 z m 0,2.64649 c 4.184659,0 7.548829,3.36417 7.548829,7.54882 0,4.18466 -3.36417,7.54883 -7.548829,7.54883 -4.1846584,0 -7.546875,-3.36417 -7.5468746,-7.54883 -4e-7,-4.18465 3.3622162,-7.54882 7.5468746,-7.54882 z"
|
||||||
|
id="path815"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
id="path839"
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0055ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 13.212891,286.88672 a 1.0487243,1.0487243 0 0 0 -1.033203,1.06445 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.06445 z m 0,-16.36914 a 1.0487243,1.0487243 0 0 0 -1.033203,1.0625 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.0625 z m 4.246093,12.20508 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.949219 a 1.048825,1.048825 0 1 0 0,-2.09765 z m -16.4179684,0 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.9492188 a 1.048825,1.048825 0 1 0 0,-2.09765 z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
106
assets/svg/crosshair-locked.svg
Normal file
106
assets/svg/crosshair-locked.svg
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 26.458333 26.458334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||||
|
sodipodi:docname="crosshair-locked.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="5.6568542"
|
||||||
|
inkscape:cx="27.044982"
|
||||||
|
inkscape:cy="77.667126"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="999"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:snap-global="false">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="13.229167,23.859748"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide815"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="14.944824,13.229167"
|
||||||
|
orientation="0,1"
|
||||||
|
id="guide817"
|
||||||
|
inkscape:locked="false" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-270.54165)">
|
||||||
|
<g
|
||||||
|
id="g827">
|
||||||
|
<circle
|
||||||
|
r="8.8715391"
|
||||||
|
cy="283.77081"
|
||||||
|
cx="13.16302"
|
||||||
|
id="path815"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path817"
|
||||||
|
d="M 3.2841366,283.77082 H 1.0418969"
|
||||||
|
style="fill:none;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path817-3"
|
||||||
|
d="M 25.405696,283.77082 H 23.286471"
|
||||||
|
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path817-3-6"
|
||||||
|
d="m 13.229167,295.9489 v -2.11763"
|
||||||
|
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path817-3-6-7"
|
||||||
|
d="m 13.229167,275.05759 v -3.44507"
|
||||||
|
style="fill:none;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:#5555ec;fill-opacity:0.98823529;stroke-width:0.6151033"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 16.850267,281.91543 h -0.65616 v -1.85094 c 0,0 0,-3.08489 -3.066169,-3.08489 -3.066169,0 -3.066169,3.08489 -3.066169,3.08489 v 1.85094 H 9.4056091 a 1.1835412,1.1907685 0 0 0 -1.1835412,1.19077 v 5.02838 a 1.1835412,1.1907685 0 0 0 1.1835412,1.1846 h 7.4446579 a 1.1835412,1.1907685 0 0 0 1.183541,-1.19078 v -5.0222 a 1.1835412,1.1907685 0 0 0 -1.183541,-1.19077 z m -3.722329,4.93583 a 1.2264675,1.233957 0 1 1 1.226468,-1.23395 1.2264675,1.233957 0 0 1 -1.226468,1.23395 z m 1.839702,-4.93583 h -3.679403 v -1.54245 c 0,-0.92546 0,-2.15942 1.839701,-2.15942 1.839702,0 1.839702,1.23396 1.839702,2.15942 z"
|
||||||
|
id="path822" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
115
assets/svg/length-crosshair.svg
Normal file
115
assets/svg/length-crosshair.svg
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.0"
|
||||||
|
width="859.53607pt"
|
||||||
|
height="858.4754pt"
|
||||||
|
viewBox="0 0 859.53607 858.4754"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
id="svg14"
|
||||||
|
sodipodi:docname="length-crosshair.svg"
|
||||||
|
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||||
|
<defs
|
||||||
|
id="defs18" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="999"
|
||||||
|
id="namedview16"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="307.56567"
|
||||||
|
inkscape:cy="-35.669379"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg14"
|
||||||
|
inkscape:snap-smooth-nodes="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2">
|
||||||
|
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:71.99999853,71.99999853;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="path816"
|
||||||
|
transform="rotate(-89.47199)"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="-425.24921"
|
||||||
|
sodipodi:cy="433.71375"
|
||||||
|
sodipodi:rx="428.34982"
|
||||||
|
sodipodi:ry="427.81949"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:end="4.7117019"
|
||||||
|
sodipodi:open="true"
|
||||||
|
d="M 3.1006165,433.71375 A 428.34982,427.81949 0 0 1 -425.1511,861.53322 428.34982,427.81949 0 0 1 -853.59898,433.90971 428.34982,427.81949 0 0 1 -425.54352,5.8943576" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:4.49999991;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 429.76804,430.08754 0,-429.19968"
|
||||||
|
id="path820"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0"
|
||||||
|
d="m 857.58749,429.23771 -855.6389371,0 v 0"
|
||||||
|
id="path822"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path814"
|
||||||
|
d="M 429.76804,857.30628 V 428.78674"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path826"
|
||||||
|
d="M 857.32232,1.0332137 H 1.6833879 v 0"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963, 17.99999963;stroke-dashoffset:0;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path828"
|
||||||
|
d="M 857.58749,858.2377 H 1.9485529 v 0"
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.99999982, 8.99999982;stroke-dashoffset:0;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
cx="-429.2377"
|
||||||
|
cy="429.76804"
|
||||||
|
rx="428.34982"
|
||||||
|
ry="427.81949"
|
||||||
|
transform="rotate(-90)"
|
||||||
|
id="path825"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:11.99999975;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M -5.3639221,-424.71887 A 428.34982,427.81949 0 0 1 -429.83855,3.0831087 428.34982,427.81949 0 0 1 -861.99345,-416.97839"
|
||||||
|
sodipodi:open="true"
|
||||||
|
sodipodi:end="3.1234988"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:ry="427.81949"
|
||||||
|
sodipodi:rx="428.34982"
|
||||||
|
sodipodi:cy="-424.71887"
|
||||||
|
sodipodi:cx="-433.71375"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
transform="rotate(-179.47199)"
|
||||||
|
id="path827"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
|
@ -646,5 +646,611 @@
|
||||||
"path": "arrow-left-thin.svg",
|
"path": "arrow-left-thin.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "direction_masked.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "direction_outline.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "direction_stroke.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "SocialImageForeground.svg",
|
||||||
|
"license": "CC-BY-SA",
|
||||||
|
"sources": [
|
||||||
|
"https://mapcomplete.osm.be"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "add.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "addSmall.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "ampersand.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "arrow-left-smooth.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "arrow-right-smooth.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "back.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Github"
|
||||||
|
],
|
||||||
|
"path": "bug.svg",
|
||||||
|
"license": "MIT",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Octicons-bug.svg",
|
||||||
|
" https://github.com/primer/octicons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "camera-plus.svg",
|
||||||
|
"license": "CC-BY-SA 3.0",
|
||||||
|
"authors": [
|
||||||
|
"Dave Gandy",
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://fontawesome.com/",
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "checkmark.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "circle.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"path": "clock.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "close.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "compass.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "cross_bottom_right.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "crosshair-blue-center.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "crosshair-blue.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "crosshair.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "crosshair-empty.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "crosshair-locked.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Dave Gandy"
|
||||||
|
],
|
||||||
|
"path": "delete_icon.svg",
|
||||||
|
"license": "CC-BY-SA",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Trash_font_awesome.svg\rT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "direction.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "direction_gradient.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "down.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "envelope.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"The Tango Desktop Project"
|
||||||
|
],
|
||||||
|
"path": "floppy.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg",
|
||||||
|
"http://tango.freedesktop.org/Tango_Desktop_Project"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "gear.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "help.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Timothy Miller"
|
||||||
|
],
|
||||||
|
"path": "home.svg",
|
||||||
|
"license": "CC-BY-SA 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Timothy Miller"
|
||||||
|
],
|
||||||
|
"path": "home_white_bg.svg",
|
||||||
|
"license": "CC-BY-SA 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"JOSM Team"
|
||||||
|
],
|
||||||
|
"path": "josm_logo.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": [
|
||||||
|
"https://wiki.openstreetmap.org/wiki/File:JOSM_Logotype_2019.svg",
|
||||||
|
" https://josm.openstreetmap.de/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "layers.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "layersAdd.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-0.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-1.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-2.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-3.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-4.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-5.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Ornament-Horiz-6.svg",
|
||||||
|
"license": "CC-BY",
|
||||||
|
"authors": [
|
||||||
|
"Nightwolfdezines"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "star.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "star_outline.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "star_half.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "star_outline_half.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "logo.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "logout.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet",
|
||||||
|
" OSM"
|
||||||
|
],
|
||||||
|
"path": "mapcomplete_logo.svg",
|
||||||
|
"license": "Logo; CC-BY-SA",
|
||||||
|
"sources": [
|
||||||
|
"https://mapcomplete.osm.be"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Mapillary"
|
||||||
|
],
|
||||||
|
"path": "mapillary.svg",
|
||||||
|
"license": "Logo; All rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://mapillary.com/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "min.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "no_checkmark.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "or.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "osm-copyright.svg",
|
||||||
|
"license": "logo; all rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://www.OpenStreetMap.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"OpenStreetMap U.S. Chapter"
|
||||||
|
],
|
||||||
|
"path": "osm-logo-us.svg",
|
||||||
|
"license": "Logo",
|
||||||
|
"sources": [
|
||||||
|
"https://www.openstreetmap.us/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "osm-logo.svg",
|
||||||
|
"license": "logo; all rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://www.OpenStreetMap.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"GitHub Octicons"
|
||||||
|
],
|
||||||
|
"path": "pencil.svg",
|
||||||
|
"license": "MIT",
|
||||||
|
"sources": [
|
||||||
|
"https://github.com/primer/octicons",
|
||||||
|
" https://commons.wikimedia.org/wiki/File:Octicons-pencil.svg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"@ tyskrat"
|
||||||
|
],
|
||||||
|
"path": "phone.svg",
|
||||||
|
"license": "CC-BY 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://www.onlinewebfonts.com/icon/1059"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "pin.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "plus.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"@fatih"
|
||||||
|
],
|
||||||
|
"path": "pop-out.svg",
|
||||||
|
"license": "CC-BY 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://www.onlinewebfonts.com/icon/2151"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "reload.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "ring.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"OOjs UI Team and other contributors"
|
||||||
|
],
|
||||||
|
"path": "search.svg",
|
||||||
|
"license": "MIT",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:OOjs_UI_indicator_search-rtl.svg",
|
||||||
|
"https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/AUTHORS.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "send_email.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "share.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "square.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"@felpgrc"
|
||||||
|
],
|
||||||
|
"path": "statistics.svg",
|
||||||
|
"license": "CC-BY 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://www.onlinewebfonts.com/icon/197818"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"MGalloway (WMF)"
|
||||||
|
],
|
||||||
|
"path": "translate.svg",
|
||||||
|
"license": "CC-BY-SA 3.0",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [],
|
||||||
|
"path": "up.svg",
|
||||||
|
"license": "CC0; trivial",
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Wikidata"
|
||||||
|
],
|
||||||
|
"path": "wikidata.svg",
|
||||||
|
"license": "Logo; All rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://www.wikidata.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Wikimedia"
|
||||||
|
],
|
||||||
|
"path": "wikimedia-commons-white.svg",
|
||||||
|
"license": "Logo; All rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Wikipedia"
|
||||||
|
],
|
||||||
|
"path": "wikipedia.svg",
|
||||||
|
"license": "Logo; All rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://www.wikipedia.org/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"Mapillary"
|
||||||
|
],
|
||||||
|
"path": "mapillary_black.svg",
|
||||||
|
"license": "Logo; All rights reserved",
|
||||||
|
"sources": [
|
||||||
|
"https://www.mapillary.com/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"The Tango! Desktop Project"
|
||||||
|
],
|
||||||
|
"path": "floppy.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"sources": [
|
||||||
|
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"wikipedialink": {
|
"wikipedialink": {
|
||||||
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>",
|
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>",
|
||||||
"condition": "wikipedia~*",
|
"condition": {
|
||||||
|
"or": [
|
||||||
|
"wikipedia~*",
|
||||||
|
"wikidata~*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": {
|
"if": "wikipedia=",
|
||||||
"and": [
|
|
||||||
"wikipedia=",
|
|
||||||
"wikidata~*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
|
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -59,8 +59,12 @@
|
||||||
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>",
|
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "id~=-",
|
"if": "id~.*/-.*",
|
||||||
"then": "<span class='alert'>Uploading...</alert>"
|
"then": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "_backend~*",
|
||||||
|
"then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"condition": "id~(node|way|relation)/[0-9]*"
|
"condition": "id~(node|way|relation)/[0-9]*"
|
||||||
|
|
|
@ -736,7 +736,7 @@
|
||||||
"_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)",
|
"_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)",
|
||||||
"_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])",
|
"_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])",
|
||||||
"_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])",
|
"_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])",
|
||||||
"_contained_climbing_routes_count=JSON.parse(_contained_climbing_routes).length"
|
"_contained_climbing_routes_count=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').length"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1412,8 +1412,8 @@
|
||||||
"_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})",
|
"_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})",
|
||||||
"_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]",
|
"_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]",
|
||||||
"_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'",
|
"_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'",
|
||||||
"_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock",
|
"_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock",
|
||||||
"_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id",
|
"_embedding_features_with_rock:id=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.id",
|
||||||
"_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access",
|
"_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access",
|
||||||
"_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
|
"_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
|
||||||
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"
|
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"
|
||||||
|
|
|
@ -62,7 +62,8 @@
|
||||||
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
|
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "generator:output:electricity"
|
"key": "generator:output:electricity",
|
||||||
|
"type": "pfloat"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "height",
|
"key": "height",
|
||||||
"type": "float"
|
"type": "pfloat"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -179,6 +180,24 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"eraseInvalidValues": true
|
"eraseInvalidValues": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appliesToKey": [
|
||||||
|
"height",
|
||||||
|
"rotor:diameter"
|
||||||
|
],
|
||||||
|
"applicableUnits": [
|
||||||
|
{
|
||||||
|
"canonicalDenomination": "m",
|
||||||
|
"alternativeDenomination": [
|
||||||
|
"meter"
|
||||||
|
],
|
||||||
|
"human": {
|
||||||
|
"en": " meter",
|
||||||
|
"nl": " meter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultBackgroundId": "CartoDB.Voyager"
|
"defaultBackgroundId": "CartoDB.Voyager"
|
||||||
|
|
|
@ -105,11 +105,31 @@
|
||||||
{
|
{
|
||||||
"builtin": "slow_roads",
|
"builtin": "slow_roads",
|
||||||
"override": {
|
"override": {
|
||||||
|
"+tagRenderings": [
|
||||||
|
{
|
||||||
|
"question": "Is dit een publiek toegankelijk pad?",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "access=private",
|
||||||
|
"then": "Dit is een privaat pad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "access=no",
|
||||||
|
"then": "Dit is een privaat pad",
|
||||||
|
"hideInAnswer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "access=permissive",
|
||||||
|
"then": "Dit pad is duidelijk in private eigendom, maar er hangen geen verbodsborden dus mag men erover"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')",
|
"_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')",
|
||||||
"_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''"
|
"_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''"
|
||||||
],
|
],
|
||||||
"minzoom": 9,
|
"minzoom": 18,
|
||||||
"source": {
|
"source": {
|
||||||
"geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson",
|
"geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson",
|
||||||
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
|
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
|
||||||
|
|
|
@ -64,7 +64,13 @@
|
||||||
},
|
},
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"render": "Deze straat is <b>{width:carriageway}m</b> breed"
|
"render": "Deze straat is <b>{width:carriageway}m</b> breed",
|
||||||
|
"question": "Hoe breed is deze straat?",
|
||||||
|
"freeform": {
|
||||||
|
"key": "width:carriageway",
|
||||||
|
"type": "length",
|
||||||
|
"helperArgs": [21, "map"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",
|
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",
|
||||||
|
|
|
@ -82,6 +82,10 @@ html, body {
|
||||||
box-sizing: initial !important;
|
box-sizing: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-control-attribution {
|
||||||
|
display: block ruby;
|
||||||
|
}
|
||||||
|
|
||||||
svg, img {
|
svg, img {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -101,6 +105,10 @@ a {
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-16-imp {
|
||||||
|
width: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.space-between{
|
.space-between{
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
3
index.ts
3
index.ts
|
@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput";
|
||||||
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
||||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
|
import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
||||||
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
|
|
||||||
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
|
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
|
||||||
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
||||||
DirectionInput.constructMinimap = options => new Minimap(options)
|
DirectionInput.constructMinimap = options => new Minimap(options)
|
||||||
|
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
|
||||||
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
||||||
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
||||||
leafletMap: UIEventSource<L.Map>,
|
leafletMap: UIEventSource<L.Map>,
|
||||||
|
|
|
@ -149,6 +149,10 @@
|
||||||
"zoomInToSeeThisLayer": "Zoom in to see this layer",
|
"zoomInToSeeThisLayer": "Zoom in to see this layer",
|
||||||
"title": "Select layers"
|
"title": "Select layers"
|
||||||
},
|
},
|
||||||
|
"download": {
|
||||||
|
"downloadGeojson": "Download visible data as geojson",
|
||||||
|
"licenseInfo": "<h3>Copyright notice</h3>The provided is available under ODbL. Reusing this data is free for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b></li><li>Any change to this data must be republished under the same license</li></ul>. Please see the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details"
|
||||||
|
},
|
||||||
"weekdays": {
|
"weekdays": {
|
||||||
"abbreviations": {
|
"abbreviations": {
|
||||||
"monday": "Mon",
|
"monday": "Mon",
|
||||||
|
|
|
@ -487,6 +487,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"0": {
|
||||||
|
"title": "Обслуживание велосипедов/магазин"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defibrillator": {
|
"defibrillator": {
|
||||||
|
@ -1064,6 +1069,7 @@
|
||||||
"1": {
|
"1": {
|
||||||
"question": "Вы хотите добавить описание?"
|
"question": "Вы хотите добавить описание?"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"name": "Смотровая площадка"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,8 +122,10 @@
|
||||||
"thanksForSharing": "Obrigado por compartilhar!",
|
"thanksForSharing": "Obrigado por compartilhar!",
|
||||||
"copiedToClipboard": "Link copiado para a área de transferência",
|
"copiedToClipboard": "Link copiado para a área de transferência",
|
||||||
"addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.",
|
"addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.",
|
||||||
"intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:"
|
"intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:",
|
||||||
}
|
"embedIntro": "<h3>Incorpore em seu site</h3>Por favor, incorpore este mapa em seu site.<br>Nós o encorajamos a fazer isso - você nem precisa pedir permissão.<br>É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará."
|
||||||
|
},
|
||||||
|
"aboutMapcomplete": "<h3>Sobre o MapComplete</h3><p>Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre um<b>único tema.</b>Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! O<b>mantenedor do tema</b>define elementos, questões e linguagens para o tema.</p><h3>Saiba mais</h3><p>MapComplete sempre<b>oferece a próxima etapa</b>para saber mais sobre o OpenStreetMap.</p><ul><li>Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira</li><li>A versão em tela inteira oferece informações sobre o OpenStreetMap</li><li>A visualização funciona sem login, mas a edição requer um login do OSM.</li><li>Se você não estiver conectado, será solicitado que você faça o login</li><li>Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa </li><li> Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki </li></ul><p></p><br><p>Você percebeu<b>um problema</b>? Você tem uma<b>solicitação de recurso </b>? Quer<b>ajudar a traduzir</b>? Acesse <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">o código-fonte</a>ou <a href=\"https: //github.com/pietervdvn/MapComplete / issues \" target=\" _ blank \">rastreador de problemas.</a></p><p>Quer ver<b>seu progresso</b>? Siga a contagem de edição em<a href=\"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D\" target=\"_blank\">OsmCha</a>.</p>"
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
"pickTheme": "Escolha um tema abaixo para começar.",
|
"pickTheme": "Escolha um tema abaixo para começar.",
|
||||||
|
@ -142,10 +144,13 @@
|
||||||
"no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!",
|
"no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!",
|
||||||
"name_required": "É necessário um nome para exibir e criar comentários",
|
"name_required": "É necessário um nome para exibir e criar comentários",
|
||||||
"title_singular": "Um comentário",
|
"title_singular": "Um comentário",
|
||||||
"title": "{count} comentários"
|
"title": "{count} comentários",
|
||||||
|
"tos": "Se você criar um comentário, você concorda com <a href=\"https://mangrove.reviews/terms\" target=\"_blank\"> o TOS e a política de privacidade de Mangrove.reviews </a>",
|
||||||
|
"affiliated_reviewer_warning": "(Revisão de afiliados)"
|
||||||
},
|
},
|
||||||
"favourite": {
|
"favourite": {
|
||||||
"reload": "Recarregar dados",
|
"reload": "Recarregar dados",
|
||||||
"panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais"
|
"panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais",
|
||||||
|
"loginNeeded": "<h3>Entrar</h3> Um layout pessoal está disponível apenas para usuários do OpenStreetMap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,27 @@
|
||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"question": "Was sind die Öffnungszeiten von {name}?",
|
"question": "Was sind die Öffnungszeiten von {name}?",
|
||||||
"render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}"
|
"render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"mappings": {
|
||||||
|
"2": {
|
||||||
|
"then": "Ist im ersten Stock"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"then": "Ist im Erdgeschoss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": "Befindet sich im {level}ten Stock",
|
||||||
|
"question": "In welchem Stockwerk befindet sich dieses Objekt?"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.<br/><span style='font-size: small'>Bitte keine bereits erhobenen Informationen.</span>"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"question": "Was ist die Website von {name}?"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"question": "Was ist die Mail-Adresse von {name}?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,30 @@
|
||||||
{}
|
{
|
||||||
|
"undefined": {
|
||||||
|
"level": {
|
||||||
|
"render": "Localizado no {level}o andar",
|
||||||
|
"mappings": {
|
||||||
|
"2": {
|
||||||
|
"then": "Localizado no primeiro andar"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"then": "Localizado no térreo"
|
||||||
|
},
|
||||||
|
"0": {
|
||||||
|
"then": "Localizado no subsolo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opening_hours": {
|
||||||
|
"question": "Qual o horário de funcionamento de {name}?"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"question": "Qual o site de {name}?"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"question": "Qual o endereço de e-mail de {name}?"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"question": "Qual o número de telefone de {name}?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,20 @@
|
||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"question": "Какое время работы у {name}?",
|
"question": "Какое время работы у {name}?",
|
||||||
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
|
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"mappings": {
|
||||||
|
"2": {
|
||||||
|
"then": "Расположено на первом этаже"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"then": "Расположено на первом этаже"
|
||||||
|
},
|
||||||
|
"0": {
|
||||||
|
"then": "Расположено под землей"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": "Расположено на {level}ом этаже"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1148,6 +1148,13 @@
|
||||||
"human": " gigawatts"
|
"human": " gigawatts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"applicableUnits": {
|
||||||
|
"0": {
|
||||||
|
"human": " meter"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -956,6 +956,13 @@
|
||||||
"human": " gigawatt"
|
"human": " gigawatt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"applicableUnits": {
|
||||||
|
"0": {
|
||||||
|
"human": " meter"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
|
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
|
||||||
"start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
|
"start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*",
|
||||||
"test": "ts-node test/TestAll.ts",
|
"test": "ts-node test/TestAll.ts",
|
||||||
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean",
|
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean",
|
||||||
"add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers",
|
"add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers",
|
||||||
|
|
|
@ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement";
|
||||||
import Table from "./UI/Base/Table";
|
import Table from "./UI/Base/Table";
|
||||||
|
|
||||||
|
|
||||||
const connection = new OsmConnection(false, new UIEventSource<string>(undefined), "");
|
const connection = new OsmConnection(false, false, new UIEventSource<string>(undefined), "");
|
||||||
|
|
||||||
let rendered = false;
|
let rendered = false;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Generates a collection of geojson files based on an overpass query for a given theme
|
* Generates a collection of geojson files based on an overpass query for a given theme
|
||||||
*/
|
*/
|
||||||
import {TileRange, Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
|
|
||||||
Utils.runningFromConsole = true
|
Utils.runningFromConsole = true
|
||||||
import {Overpass} from "../Logic/Osm/Overpass";
|
import {Overpass} from "../Logic/Osm/Overpass";
|
||||||
|
@ -18,6 +18,7 @@ import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||||
import {GeoOperations} from "../Logic/GeoOperations";
|
import {GeoOperations} from "../Logic/GeoOperations";
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import {TileRange} from "../Models/TileRange";
|
||||||
|
|
||||||
|
|
||||||
function createOverpassObject(theme: LayoutConfig) {
|
function createOverpassObject(theme: LayoutConfig) {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||||
import * as licenses from "../assets/generated/license_info.json"
|
import * as licenses from "../assets/generated/license_info.json"
|
||||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
||||||
import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson";
|
import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson";
|
||||||
import {Translation} from "../UI/i18n/Translation";
|
|
||||||
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
|
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
|
||||||
import AllKnownLayers from "../Customizations/AllKnownLayers";
|
import AllKnownLayers from "../Customizations/AllKnownLayers";
|
||||||
|
|
||||||
|
@ -77,63 +76,6 @@ class LayerOverviewUtils {
|
||||||
return errorCount
|
return errorCount
|
||||||
}
|
}
|
||||||
|
|
||||||
validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) {
|
|
||||||
const missingTranlations = []
|
|
||||||
const translations: { tr: Translation, context: string }[] = [];
|
|
||||||
const queue: { object: any, context: string }[] = [{object: object, context: context}]
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const item = queue.pop();
|
|
||||||
const o = item.object
|
|
||||||
for (const key in o) {
|
|
||||||
const v = o[key];
|
|
||||||
if (v === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (v instanceof Translation || v?.translations !== undefined) {
|
|
||||||
translations.push({tr: v, context: item.context});
|
|
||||||
} else if (
|
|
||||||
["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) {
|
|
||||||
queue.push({object: v, context: item.context + "." + key})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const missing = {}
|
|
||||||
const present = {}
|
|
||||||
for (const ln of expectedLanguages) {
|
|
||||||
missing[ln] = 0;
|
|
||||||
present[ln] = 0;
|
|
||||||
for (const translation of translations) {
|
|
||||||
if (translation.tr.translations["*"] !== undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const txt = translation.tr.translations[ln];
|
|
||||||
const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0;
|
|
||||||
if (isMissing) {
|
|
||||||
missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`)
|
|
||||||
missing[ln]++
|
|
||||||
} else {
|
|
||||||
present[ln]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = `Translation completeness for ${context}`
|
|
||||||
let isComplete = true;
|
|
||||||
for (const ln of expectedLanguages) {
|
|
||||||
const amiss = missing[ln];
|
|
||||||
const ok = present[ln];
|
|
||||||
const total = amiss + ok;
|
|
||||||
message += ` ${ln}: ${ok}/${total}`
|
|
||||||
if (ok !== total) {
|
|
||||||
isComplete = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return missingTranlations
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
main(args: string[]) {
|
main(args: string[]) {
|
||||||
|
|
||||||
const lt = this.loadThemesAndLayers();
|
const lt = this.loadThemesAndLayers();
|
||||||
|
@ -160,7 +102,6 @@ class LayerOverviewUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
let themeErrorCount = []
|
let themeErrorCount = []
|
||||||
let missingTranslations = []
|
|
||||||
for (const themeFile of themeFiles) {
|
for (const themeFile of themeFiles) {
|
||||||
if (typeof themeFile.language === "string") {
|
if (typeof themeFile.language === "string") {
|
||||||
themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings")
|
themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings")
|
||||||
|
@ -169,10 +110,6 @@ class LayerOverviewUtils {
|
||||||
if (typeof layer === "string") {
|
if (typeof layer === "string") {
|
||||||
if (!knownLayerIds.has(layer)) {
|
if (!knownLayerIds.has(layer)) {
|
||||||
themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`)
|
themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`)
|
||||||
} else {
|
|
||||||
const layerConfig = knownLayerIds.get(layer);
|
|
||||||
missingTranslations.push(...this.validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (layer.builtin !== undefined) {
|
} else if (layer.builtin !== undefined) {
|
||||||
let names = layer.builtin;
|
let names = layer.builtin;
|
||||||
|
@ -197,7 +134,6 @@ class LayerOverviewUtils {
|
||||||
.filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
|
.filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
|
||||||
.filter(l => l.builtin === undefined)
|
.filter(l => l.builtin === undefined)
|
||||||
|
|
||||||
missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const theme = new LayoutConfig(themeFile, true, "test")
|
const theme = new LayoutConfig(themeFile, true, "test")
|
||||||
|
@ -209,11 +145,6 @@ class LayerOverviewUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingTranslations.length > 0) {
|
|
||||||
console.log(missingTranslations.length, "missing translations")
|
|
||||||
writeFileSync("missing_translations.txt", missingTranslations.join("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layerErrorCount.length + themeErrorCount.length == 0) {
|
if (layerErrorCount.length + themeErrorCount.length == 0) {
|
||||||
console.log("All good!")
|
console.log("All good!")
|
||||||
|
|
||||||
|
|
35
test.ts
35
test.ts
|
@ -7,6 +7,11 @@ import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
import {Tag} from "./Logic/Tags/Tag";
|
import {Tag} from "./Logic/Tags/Tag";
|
||||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||||
import {Translation} from "./UI/i18n/Translation";
|
import {Translation} from "./UI/i18n/Translation";
|
||||||
|
import LocationInput from "./UI/Input/LocationInput";
|
||||||
|
import Loc from "./Models/Loc";
|
||||||
|
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||||
|
import LengthInput from "./UI/Input/LengthInput";
|
||||||
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
/*import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
/*import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
||||||
import Combine from "./UI/Base/Combine";
|
import Combine from "./UI/Base/Combine";
|
||||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||||
|
@ -148,19 +153,17 @@ function TestMiniMap() {
|
||||||
featureSource.ping()
|
featureSource.ping()
|
||||||
}
|
}
|
||||||
//*/
|
//*/
|
||||||
QueryParameters.GetQueryParameter("test", "true").setData("true")
|
|
||||||
State.state= new State(undefined)
|
const loc = new UIEventSource<Loc>({
|
||||||
const id = "node/5414688303"
|
zoom: 24,
|
||||||
State.state.allElements.addElementById(id, new UIEventSource<any>({id: id}))
|
lat: 51.21043,
|
||||||
new Combine([
|
lon: 3.21389
|
||||||
new DeleteWizard(id, {
|
})
|
||||||
noDeleteOptions: [
|
const li = new LengthInput(
|
||||||
{
|
AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")),
|
||||||
if:[ new Tag("access","private")],
|
loc
|
||||||
then: new Translation({
|
)
|
||||||
en: "Very private! Delete now or me send lawfull lawyer"
|
li.SetStyle("height: 30rem; background: aliceblue;")
|
||||||
})
|
.AttachTo("maindiv")
|
||||||
}
|
|
||||||
]
|
new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv")
|
||||||
}),
|
|
||||||
]).AttachTo("maindiv")
|
|
|
@ -15,7 +15,7 @@ export default class OsmConnectionSpec extends T {
|
||||||
super("OsmConnectionSpec-test", [
|
super("OsmConnectionSpec-test", [
|
||||||
["login on dev",
|
["login on dev",
|
||||||
() => {
|
() => {
|
||||||
const osmConn = new OsmConnection(false,
|
const osmConn = new OsmConnection(false,false,
|
||||||
new UIEventSource<string>(undefined),
|
new UIEventSource<string>(undefined),
|
||||||
"Unit test",
|
"Unit test",
|
||||||
true,
|
true,
|
||||||
|
|
10
tslint.json
10
tslint.json
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"defaultSeverity": "error",
|
|
||||||
"extends": [
|
|
||||||
"tslint:recommended",
|
|
||||||
"tslint-no-circular-imports"
|
|
||||||
],
|
|
||||||
"jsRules": {},
|
|
||||||
"rules": {},
|
|
||||||
"rulesDirectory": []
|
|
||||||
}
|
|
Loading…
Reference in a new issue