Merge develop

This commit is contained in:
pietervdvn 2021-07-16 01:43:24 +02:00
commit 93ae7441d1
98 changed files with 2446 additions and 725 deletions

16
.devcontainer/Dockerfile Normal file
View 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>"

View 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
View file

@ -0,0 +1,3 @@
{
"files.eol": "\n"
}

View file

@ -3,6 +3,7 @@ import UnitConfigJson from "./UnitConfigJson";
import Translations from "../../UI/i18n/Translations";
import BaseUIElement from "../../UI/BaseUIElement";
import Combine from "../../UI/Base/Combine";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
export class Unit {
public readonly appliesToKeys: Set<string>;
@ -81,7 +82,10 @@ export class Unit {
return undefined;
}
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];
return new Combine(elems)
@ -152,7 +156,7 @@ export class Denomination {
if (stripped === null) {
return null;
}
return stripped + " " + this.canonical.trim()
return (stripped + " " + this.canonical.trim()).trim();
}
/**

View file

@ -54,6 +54,7 @@ export default class LayerConfig {
title: Translation,
tags: Tag[],
description?: Translation,
preciseInput?: {preferredBackground?: string}
}[];
tagRenderings: TagRenderingConfig [];
@ -130,12 +131,19 @@ export default class LayerConfig {
this.minzoom = json.minzoom ?? 0;
this.maxzoom = json.maxzoom ?? 1000;
this.wayHandling = json.wayHandling ?? 0;
this.presets = (json.presets ?? []).map((pr, i) =>
({
this.presets = (json.presets ?? []).map((pr, i) => {
if(pr.preciseInput === true){
pr.preciseInput = {
preferredBackground: undefined
}
}
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`)
}))
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

View file

@ -217,6 +217,16 @@ export interface LayerConfigJson {
* (The first sentence is until the first '.'-character in the description)
*/
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
}
}[],
/**

View file

@ -42,6 +42,7 @@ export default class LayoutConfig {
public readonly enableGeolocation: boolean;
public readonly enableBackgroundLayerSelection: boolean;
public readonly enableShowAllQuestions: boolean;
public readonly enableExportButton: boolean;
public readonly customCss?: string;
/*
How long is the cache valid, in seconds?
@ -152,6 +153,7 @@ export default class LayoutConfig {
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
this.enableExportButton = json.enableExportButton ?? false;
this.customCss = json.customCss;
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)

View file

@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson";
* General remark: a type (string | any) indicates either a fixed or a translatable string.
*/
export interface LayoutConfigJson {
/**
* 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.
* This is handled by defining units.
*
* # Rendering
*
* To render a value with long (human) denomination, use {canonical(key)}
*
* # Usage
*
@ -331,4 +336,5 @@ export interface LayoutConfigJson {
enableGeolocation?: boolean;
enableBackgroundLayerSelection?: boolean;
enableShowAllQuestions?: boolean;
enableExportButton?: boolean;
}

View file

@ -26,6 +26,8 @@ export default class TagRenderingConfig {
readonly key: string,
readonly type: string,
readonly addExtraTags: TagsFilter[];
readonly inline: boolean,
readonly default?: string
};
readonly multiAnswer: boolean;
@ -73,6 +75,8 @@ export default class TagRenderingConfig {
type: json.freeform.type ?? "string",
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default
}

View file

@ -46,7 +46,19 @@ export interface TagRenderingConfigJson {
**/
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
},
/**

View file

@ -18,9 +18,9 @@
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 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
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
--------------------

View file

@ -13,7 +13,8 @@ Before you start, you should have the following qualifications:
- You're theme will add well-understood tags (aka: the tags have a wiki page, are not controversial and are objective)
- You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to help testing
If you do not have those qualifications, reach out to the MapComplete community channel on [Telegram](https://t.me/joinchat/HiMUavahRG--SCvC)
If you do not have those qualifications, reach out to the MapComplete community channel on [Telegram](https://t.me/MapComplete)
or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org).
The custom theme generator
--------------------------
@ -55,12 +56,9 @@ The preferred way to add your theme is via a Pull Request. A Pull Request is les
- If an SVG version is available, use the SVG version
- Make sure all the links in `yourtheme.json` are updated. You can use `./assets/themes/yourtheme/yourimage.svg` instead of the HTML link
- Create a file `license_info.json` in the theme directory, which contains metadata on every artwork source
5) Add your theme to the code base:
- Open [AllKnownLayouts.ts](https://github.com/pietervdvn/MapComplete/blob/master/Customizations/AllKnownLayouts.ts)
- Add an import statement, e.g. `import * as yourtheme from "../assets/themes/yourtheme/yourthemes.json";`
- Add your theme to the `LayoutsList`, by adding a line `new LayoutConfig(yourtheme)`
5) Add your theme to the code base: add it into "assets/themes" and make sure all the images are there too. Running 'ts-node scripts/fixTheme <path to your theme>' will help downloading the images and attempts to get the licenses if on wikimedia.
6) Add some finishing touches, such as a social image. See [this blog post](https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit) for some hints
7) Test your theme: run the project as described [above](../README.md#Dev)
7) Test your theme: run the project as described in [development_deployment](Development_deployment.md)
8) Happy with your theme? Time to open a Pull Request!
9) Thanks a lot for improving MapComplete!

View file

@ -341,7 +341,7 @@ export class InitUiElements {
private static InitBaseMap() {
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl);
State.state.backgroundLayer = State.state.backgroundLayerId
.map((selectedId: string) => {
@ -427,6 +427,8 @@ export class InitUiElements {
state.locationControl,
state.selectedElement);
State.state.featurePipeline = source;
new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse);
const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource);

View file

@ -1,11 +1,12 @@
import * as editorlayerindex from "../../assets/editor-layer-index.json"
import BaseLayer from "../../Models/BaseLayer";
import * as L from "leaflet";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers";
import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations";
import {TileLayer} from "leaflet";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
/**
* Calculates which layers are available at the current location
@ -24,45 +25,87 @@ export default class AvailableBaseLayers {
false, false),
feature: null,
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 availableEditorLayers: UIEventSource<BaseLayer[]>;
constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) {
const self = this;
this.availableEditorLayers =
location.map(
(currentLocation) => {
public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const source = location.map(
(currentLocation) => {
if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview;
}
if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview;
}
const currentLayers = self.availableEditorLayers?.data;
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
const currentLayers = source?.data; // A bit unorthodox - I know
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;
}
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 globalLayers = [];
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
@ -140,7 +183,9 @@ export default class AvailableBaseLayers {
min_zoom: props.min_zoom ?? 1,
name: props.name,
layer: leafletLayer,
feature: layer
feature: layer,
isBest: props.best ?? false,
category: props.category
});
}
return layers;
@ -152,15 +197,16 @@ export default class AvailableBaseLayers {
function l(id: string, name: string): BaseLayer {
try {
const layer: any = () => L.tileLayer.provider(id, undefined);
const baseLayer: BaseLayer = {
return {
feature: null,
id: id,
name: name,
layer: layer,
min_zoom: layer.minzoom,
max_zoom: layer.maxzoom
max_zoom: layer.maxzoom,
category: "osmbasedmap",
isBest: false
}
return baseLayer
} catch (e) {
console.error("Could not find provided layer", name, e);
return null;

View file

@ -1,244 +1,262 @@
import * as L from "leaflet";
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import { UIEventSource } from "../UIEventSource";
import { Utils } from "../../Utils";
import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import { LocalStorageSource } from "../Web/LocalStorageSource";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import { VariableUiElement } from "../../UI/Base/VariableUIElement";
export default class GeoLocationHandler extends VariableUiElement {
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
*/
private readonly _isActive: UIEventSource<boolean>;
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
*/
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
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
/**
* Kept in order to update the marker
* @private
*/
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
* @private
*/
private _lastUserRequest: Date;
/**
* 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.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{
latlng: any;
accuracy: number;
}>;
/**
* Kept in order to update the marker
* @private
*/
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
* @private
*/
private _lastUserRequest: Date;
/**
* 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.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
constructor(
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>
) {
const hasLocation = currentGPSLocation.map(
(location) => location !== undefined
);
const previousLocationGrant = LocalStorageSource.Get(
"geolocation-permissions"
);
const isActive = new UIEventSource<boolean>(false);
const isLocked = new UIEventSource<boolean>(false);
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>) {
super(
hasLocation.map(
(hasLocationData) => {
if (isLocked.data) {
return Svg.crosshair_locked_ui();
} else if (hasLocationData) {
return Svg.crosshair_blue_ui();
} else if (isActive.data) {
return Svg.crosshair_blue_center_ui();
} else {
return Svg.crosshair_ui();
}
},
[isActive, isLocked]
)
);
this._isActive = isActive;
this._isLocked = isLocked;
this._permission = new UIEventSource<string>("");
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
const hasLocation = currentGPSLocation.map((location) => location !== undefined);
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
const isActive = new UIEventSource<boolean>(false);
const currentPointer = this._isActive.map(
(isActive) => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait";
}
return "cursor-pointer";
},
[this._hasLocation]
);
currentPointer.addCallbackAndRun((pointerClass) => {
self.SetClass(pointerClass);
});
super(
hasLocation.map(hasLocation => {
this.onClick(() => {
if (self._hasLocation.data) {
self._isLocked.setData(!self._isLocked.data);
}
self.init(true);
});
this.init(false);
if (hasLocation) {
return Svg.crosshair_blue_ui()
}
if (isActive.data) {
return Svg.crosshair_blue_center_ui();
}
return Svg.crosshair_ui();
}, [isActive])
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest =
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
self.MoveToCurrentLoction(16);
} else if (self._isLocked.data) {
self.MoveToCurrentLoction();
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
this._isActive = isActive;
this._permission = new UIEventSource<string>("")
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
} catch (e) {
console.error(e);
}
const icon = L.icon({
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
});
const currentPointer = this._isActive.map(isActive => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait"
}
return "cursor-pointer"
}, [this._hasLocation])
currentPointer.addCallbackAndRun(pointerClass => {
self.SetClass(pointerClass);
})
const map = self._leafletMap.data;
const newMarker = L.marker(location.latlng, { icon: icon });
newMarker.addTo(map);
this.onClick(() => self.init(true))
this.init(false)
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
}
private init(askPermission: boolean) {
const self = this;
if (self._isActive.data) {
self.MoveToCurrentLoction(16);
return;
}
private init(askPermission: boolean) {
const self = this;
const map = this._leafletMap.data;
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest = (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
self.MoveToCurrentLoction(16)
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
} catch (e) {
console.error(e)
}
const icon = L.icon(
{
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
})
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
try {
navigator?.permissions?.query({name: 'geolocation'})
?.then(function (status) {
console.log("Geolocation is already", status)
if (status.state === "granted") {
self.StartGeolocating(false);
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
}
});
} catch (e) {
console.error(e)
}
if (askPermission) {
self.StartGeolocating(true);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
try {
navigator?.permissions
?.query({ name: "geolocation" })
?.then(function (status) {
console.log("Geolocation is already", status);
if (status.state === "granted") {
self.StartGeolocating(false);
}
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
};
});
} catch (e) {
console.error(e);
}
private locate() {
const self = this;
const map: any = this._leafletMap.data;
if (askPermission) {
self.StartGeolocating(true);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
self.StartGeolocating(false);
}
}
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?: number) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
if (
this._currentGPSLocation.data.latlng[0] === 0 &&
this._currentGPSLocation.data.latlng[1] === 0
) {
console.debug("Not moving to GPS-location: it is null island");
return;
}
private MoveToCurrentLoction(targetZoom = 16) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.data.lockLocation;
let inRange = true;
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange =
b[0][0] <= location.latlng[0] &&
location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] &&
location.latlng[1] <= b[1][1];
}
}
if (!inRange) {
console.log(
"Not zooming to GPS location: out of bounds",
b,
location.latlng
);
} else {
this._leafletMap.data.setView(location.latlng, targetZoom);
}
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
console.log("Starting geolocation");
if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) {
console.debug("Not moving to GPS-location: it is null island")
return;
}
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.data.lockLocation
let inRange = true;
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange = b[0][0] <= location.latlng[0] && location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] && location.latlng[1] <= b[1][1];
}
}
if (!inRange) {
console.log("Not zooming to GPS location: out of bounds", b, location.latlng)
} else {
this._leafletMap.data.setView(
location.latlng, targetZoom
);
}
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16);
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
console.log("Starting geolocation")
console.log("Searching location using GPS");
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16)
}
console.log("Searching location using GPS")
this.locate();
if (!self._isActive.data) {
self._isActive.setData(true);
Utils.DoEvery(60000, () => {
if (document.visibilityState !== "visible") {
console.log("Not starting gps: document not visible")
return;
}
this.locate();
})
}
if (self._isActive.data) {
return;
}
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");
}
);
}
}

View file

@ -1,9 +1,45 @@
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
export default interface FeatureSource {
features: UIEventSource<{feature: any, freshness: Date}[]>;
features: UIEventSource<{ feature: any, freshness: Date }[]>;
/**
* Mainly used for debuging
*/
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}
}
}

View file

@ -6,31 +6,38 @@ import Constants from "../../Models/Constants";
import FeatureSource from "../FeatureSource/FeatureSource";
import {TagsFilter} from "../Tags/TagsFilter";
import {Tag} from "../Tags/Tag";
import {OsmConnection} from "./OsmConnection";
import {LocalStorageSource} from "../Web/LocalStorageSource";
/**
* Handles all changes made to OSM.
* 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"
/**
* The newly created points, as a FeatureSource
*/
public features = new UIEventSource<{feature: any, freshness: Date}[]>([]);
private static _nextId = -1; // Newly assigned ID's are negative
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
/**
* All the pending changes
*/
public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =
new UIEventSource<{elementId: string; key: string; value: string}[]>([]);
public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", [])
/**
* 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
*/
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 value = kv.v;
if (key === undefined || key === null) {
@ -49,8 +56,7 @@ export class Changes implements FeatureSource{
return {k: key.trim(), v: value.trim()};
}
addTag(elementId: string, tagsFilter: TagsFilter,
tags?: UIEventSource<any>) {
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
@ -59,7 +65,7 @@ export class Changes implements FeatureSource{
if (changes.length == 0) {
return;
}
for (const change of changes) {
if (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.
* Triggered by the 'PendingChangeUploader'-actor in Actors
*/
public flushChanges(flushreason: string = undefined){
if(this.pending.data.length === 0){
public flushChanges(flushreason: string = undefined) {
if (this.pending.data.length === 0) {
return;
}
if(flushreason !== undefined){
if (flushreason !== undefined) {
console.log(flushreason)
}
this.uploadAll([], this.pending.data);
this.pending.setData([]);
this.uploadAll();
}
/**
* Create a new node element at the given lat/long.
* 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) {
console.log("Creating a new element with ", basicTags)
const osmNode = new OsmNode(Changes._nextId);
const newId = Changes._nextId;
Changes._nextId--;
const id = "node/" + osmNode.id;
osmNode.lat = lat;
osmNode.lon = lon;
const id = "node/" + newId;
const properties = {id: id};
const geojson = {
@ -118,28 +124,49 @@ export class Changes implements FeatureSource{
// The tags are not yet written into the OsmObject, but this is applied onto a
const changes = [];
for (const kv of basicTags) {
properties[kv.key] = kv.value;
if (typeof kv.value !== "string") {
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})
}
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();
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;
}
private uploadChangesWithLatestVersions(
knownElements, newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
knownElements: OsmObject[]) {
const knownById = new Map<string, OsmObject>();
knownElements.forEach(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
// 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) {
// This is a new element - we should apply this on one of the new elements
for (const newElement of newElements) {
@ -147,9 +174,8 @@ export class Changes implements FeatureSource{
newElement.addTag(change.key, change.value);
}
}
} else {
knownElements[change.elementId].addTag(change.key, change.value);
knownById.get(change.elementId).addTag(change.key, change.value);
}
}
@ -162,9 +188,17 @@ export class Changes implements FeatureSource{
}
}
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;
}
const self = this;
if (this.isUploading.data) {
return;
}
this.isUploading.setData(true)
console.log("Beginning upload...");
// At last, we build the changeset and upload
@ -207,17 +241,22 @@ export class Changes implements FeatureSource{
changes += "</osmChange>";
return changes;
});
},
() => {
console.log("Upload successfull!")
self.newObjects.setData([])
self.pending.setData([]);
self.isUploading.setData(false)
},
() => self.isUploading.setData(false)
);
};
private uploadAll(
newElements: OsmObject[],
pending: { elementId: string; key: string; value: string }[]
) {
private uploadAll() {
const self = this;
const pending = this.pending.data;
let neededIds: string[] = [];
for (const change of pending) {
const id = change.elementId;
@ -230,7 +269,7 @@ export class Changes implements FeatureSource{
neededIds = Utils.Dedup(neededIds);
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
self.uploadChangesWithLatestVersions(knownElements)
})
}

View file

@ -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");
// @ts-ignore
for (const node of nodes) {
@ -69,7 +69,9 @@ export class ChangesetHandler {
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string) {
generateChangeXML: (csid: string) => string,
whenDone: (csId: string) => void,
onFail: () => void) {
if (this.userDetails.data.csCount == 0) {
// The user became a contributor!
@ -80,6 +82,7 @@ export class ChangesetHandler {
if (this._dryRun) {
const changesetXML = generateChangeXML("123456");
console.log(changesetXML);
whenDone("123456")
return;
}
@ -93,12 +96,14 @@ export class ChangesetHandler {
console.log(changeset);
self.AddChange(csId, changeset,
allElements,
() => {
},
whenDone,
(e) => {
console.error("UPLOADING FAILED!", e)
onFail()
}
)
}, {
onFail: onFail
})
} else {
// There still exists an open changeset (or at least we hope so)
@ -107,15 +112,13 @@ export class ChangesetHandler {
csId,
generateChangeXML(csId),
allElements,
() => {
},
whenDone,
(e) => {
console.warn("Could not upload, changeset is probably closed: ", e);
// Mark the CS as closed...
this.currentChangeset.setData("");
// ... 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;
this.OpenChangeset(layout, (csId: string) => {
// The cs is open - let us actually upload!
const changes = generateChangeXML(csId)
// The cs is open - let us actually upload!
const changes = generateChangeXML(csId)
self.AddChange(csId, changes, allElements, (csId) => {
console.log("Successfully deleted ", object.id)
self.CloseChangeset(csId, continuation)
}, (csId) => {
alert("Deletion failed... Should not happend")
// FAILED
self.CloseChangeset(csId, continuation)
})
}, true, reason)
self.AddChange(csId, changes, allElements, (csId) => {
console.log("Successfully deleted ", object.id)
self.CloseChangeset(csId, continuation)
}, (csId) => {
alert("Deletion failed... Should not happend")
// FAILED
self.CloseChangeset(csId, continuation)
})
}, {
isDeletionCS: true,
deletionReason: reason
}
)
}
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
@ -204,15 +211,20 @@ export class ChangesetHandler {
private OpenChangeset(
layout: LayoutConfig,
continuation: (changesetId: string) => void,
isDeletionCS: boolean = false,
deletionReason: string = undefined) {
options?: {
isDeletionCS?: boolean,
deletionReason?: string,
onFail?: () => void
}
) {
options = options ?? {}
options.isDeletionCS = options.isDeletionCS ?? false
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
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}`
if (deletionReason) {
comment += ": " + deletionReason;
if (options.deletionReason) {
comment += ": " + options.deletionReason;
}
}
@ -221,7 +233,7 @@ export class ChangesetHandler {
const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`],
["comment", comment],
["deletion", isDeletionCS ? "yes" : undefined],
["deletion", options.isDeletionCS ? "yes" : undefined],
["theme", layout.id],
["language", Locale.language.data],
["host", window.location.host],
@ -244,7 +256,9 @@ export class ChangesetHandler {
}, function (err, response) {
if (response === undefined) {
console.log("err", err);
alert("Could not upload change (opening failed). Please file a bug report")
if(options.onFail){
options.onFail()
}
return;
} else {
continuation(response);
@ -265,7 +279,7 @@ export class ChangesetHandler {
private AddChange(changesetId: string,
changesetXML: string,
allElements: ElementStorage,
continuation: ((changesetId: string, idMapping: any) => void),
continuation: ((changesetId: string) => void),
onFail: ((changesetId: string, reason: string) => void) = undefined) {
this.auth.xhr({
method: 'POST',
@ -280,9 +294,9 @@ export class ChangesetHandler {
}
return;
}
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements);
ChangesetHandler.parseUploadChangesetResponse(response, allElements);
console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping);
continuation(changesetId);
});
}

View file

@ -30,7 +30,7 @@ export default class UserDetails {
export class OsmConnection {
public static readonly _oauth_configs = {
public static readonly oauth_configs = {
"osm": {
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
@ -66,7 +66,7 @@ export class OsmConnection {
osmConfiguration: "osm" | "osm-test" = 'osm'
) {
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)
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
@ -110,8 +110,10 @@ export class OsmConnection {
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string) {
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
generateChangeXML: (csid: string) => string,
whenDone: (csId: string) => void,
onFail: () => {}) {
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {

View file

@ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource";
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 objectCache = new Map<string, UIEventSource<OsmObject>>();
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
@ -36,15 +37,22 @@ export abstract class OsmObject {
this.backendURL = url;
}
static DownloadObject(id): UIEventSource<OsmObject> {
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
let src: UIEventSource<OsmObject>;
if (OsmObject.objectCache.has(id)) {
return OsmObject.objectCache.get(id)
src = OsmObject.objectCache.get(id)
if (forceRefresh) {
src.setData(undefined)
} else {
return src;
}
} else {
src = new UIEventSource<OsmObject>(undefined)
}
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
const src = new UIEventSource<OsmObject>(undefined)
OsmObject.objectCache.set(id, src);
const newContinuation = (element: OsmObject) => {
src.setData(element)
@ -150,7 +158,7 @@ export abstract class OsmObject {
const minlat = bounds[1][0]
const maxlat = bounds[0][0];
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 objects = OsmObject.ParseObjects(elements)
callback(objects);
@ -158,11 +166,11 @@ export abstract class OsmObject {
})
}
public static DownloadAll(neededIds): UIEventSource<OsmObject[]> {
public static DownloadAll(neededIds, forceRefresh = true): UIEventSource<OsmObject[]> {
// local function which downloads all the objects one by one
// this is one big loop, running one download, then rerunning the entire function
const allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id))
const allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id, forceRefresh))
const allCompleted = new UIEventSource(undefined).map(_ => {
return !allSources.some(uiEventSource => uiEventSource.data === undefined)
}, allSources)
@ -170,7 +178,7 @@ export abstract class OsmObject {
if (completed) {
return allSources.map(src => src.data)
}
return []
return undefined
});
}
@ -284,6 +292,7 @@ export abstract class OsmObject {
self.LoadData(element)
self.SaveExtraData(element, nodes);
const meta = {
"_last_edit:contributor": element.user,
"_last_edit:contributor:uid": element.uid,
@ -292,6 +301,11 @@ export abstract class OsmObject {
"_version_number": element.version
}
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
self.tags["_backend"] = OsmObject.backendURL
meta["_backend"] = OsmObject.backendURL;
}
continuation(self, meta);
}
);

View file

@ -84,6 +84,7 @@ export default class SimpleMetaTagger {
},
(feature => {
const units = State.state.layoutToUse.data.units ?? [];
let rewritten = false;
for (const key in feature.properties) {
if (!feature.properties.hasOwnProperty(key)) {
continue;
@ -95,16 +96,23 @@ export default class SimpleMetaTagger {
const value = feature.properties[key]
const [, denomination] = unit.findDenomination(value)
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) {
break;
}
feature.properties[key] = canonical;
rewritten = true;
break;
}
}
if(rewritten){
State.state.allElements.getEventSourceById(feature.id).ping();
}
})
)

View file

@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource";
* UIEventsource-wrapper around localStorage
*/
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> {
try {

View file

@ -7,4 +7,6 @@ export default interface BaseLayer {
max_zoom: number,
min_zoom: number;
feature: any,
isBest?: boolean,
category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string
}

View file

@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants {
public static vNumber = "0.8.3/bike-infra";
public static vNumber = "0.8.4/bike-infra";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {

8
Models/TileRange.ts Normal file
View file

@ -0,0 +1,8 @@
export interface TileRange {
xstart: number,
ystart: number,
xend: number,
yend: number,
total: number,
zoomlevel: number
}

View file

@ -19,6 +19,7 @@ import TitleHandler from "./Logic/Actors/TitleHandler";
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
import {Relation} from "./Logic/Osm/ExtractRelations";
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
/**
* Contains the global state: a bunch of UI-event sources
@ -95,6 +96,11 @@ export default class State {
public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
public readonly featureSwitchApiURL: UIEventSource<string>;
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
public readonly featurePipeline: FeaturePipeline;
/**
@ -125,7 +131,7 @@ export default class State {
public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `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 >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>(
str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n
);
constructor(layoutToUse: LayoutConfig) {
const self = this;
@ -199,6 +205,8 @@ export default class State {
"Disables/Enables the geolocation button");
this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
"Always show all questions");
this.featureSwitchEnableExport = featSw("fs-export",(layoutToUse) => layoutToUse?.enableExportButton ?? false,
"If set, enables the 'download'-button to download everything as geojson")
this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false",
"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")
@ -210,7 +218,7 @@ export default class State {
this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","osm",
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'")
}
{
// Some other feature switches

12
Svg.ts

File diff suppressed because one or more lines are too long

View file

@ -44,7 +44,6 @@ export default class Minimap extends BaseUIElement {
const self = this;
// @ts-ignore
const resizeObserver = new ResizeObserver(_ => {
console.log("Change in size detected!")
self.InitMap();
self.leafletMap?.data?.invalidateSize()
});
@ -53,7 +52,7 @@ export default class Minimap extends BaseUIElement {
return wrapper;
}
private InitMap() {
if (this._constructedHtmlElement === undefined) {
// This element isn't initialized yet
@ -82,7 +81,9 @@ export default class Minimap extends BaseUIElement {
scrollWheelZoom: this._allowMoving,
doubleClickZoom: this._allowMoving,
keyboard: this._allowMoving,
touchZoom: this._allowMoving
touchZoom: this._allowMoving,
zoomAnimation: this._allowMoving,
fadeAnimation: this._allowMoving
});
map.setMaxBounds(

View file

@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export class Basemap {
@ -35,9 +36,8 @@ export class Basemap {
);
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;
currentLayer.addCallbackAndRun(layer => {
@ -77,6 +77,7 @@ export class Basemap {
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
});
extraAttribution.AttachTo('leaflet-attribution')
}

View 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")])
}
}

View file

@ -2,11 +2,12 @@ import State from "../../State";
import BackgroundSelector from "./BackgroundSelector";
import LayerSelection from "./LayerSelection";
import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import {ExportDataButton} from "./ExportDataButton";
export default class LayerControlPanel extends ScrollableFullScreen {
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
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")
}
private static GeneratePanel() : BaseUIElement {
let layerControlPanel: BaseUIElement = new FixedUiElement("");
private static GeneratePanel(): BaseUIElement {
const elements: BaseUIElement[] = []
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
layerControlPanel = new BackgroundSelector();
layerControlPanel.SetStyle("margin:1em");
layerControlPanel.onClick(() => {
const backgroundSelector = new BackgroundSelector();
backgroundSelector.SetStyle("margin:1em");
backgroundSelector.onClick(() => {
});
elements.push(backgroundSelector)
}
if (State.state.filteredLayers.data.length > 1) {
const layerSelection = new LayerSelection(State.state.filteredLayers);
layerSelection.onClick(() => {
});
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
}
elements.push(new Toggle(
new LayerSelection(State.state.filteredLayers),
undefined,
State.state.filteredLayers.map(layers => layers.length > 1)
))
return layerControlPanel;
elements.push(new Toggle(
new ExportDataButton(),
undefined,
State.state.featureSwitchEnableExport
))
return new Combine(elements).SetClass("flex flex-col")
}
}

View file

@ -74,7 +74,6 @@ export default class LayerSelection extends Combine {
);
}
super(checkboxes)
this.SetStyle("display:flex;flex-direction:column;")

View file

@ -62,6 +62,10 @@ export default class MoreScreen extends Combine {
let officialThemes = AllKnownLayouts.layoutsList
let buttons = officialThemes.map((layout) => {
if(layout === undefined){
console.trace("Layout is undefined")
return undefined
}
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
if(layout.id === personal.id){
return new VariableUiElement(

View file

@ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import UserDetails from "../../Logic/Osm/OsmConnection";
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:
@ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation";
* - A 'read your unread messages before adding a point'
*/
/*private*/
interface PresetInfo {
description: string | Translation,
name: string | BaseUIElement,
icon: BaseUIElement,
icon: () => BaseUIElement,
tags: Tag[],
layerToAddTo: {
layerDef: LayerConfig,
isDisplayed: UIEventSource<boolean>
},
preciseInput?: {
preferredBackground?: string
}
}
@ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle {
new SubtleButton(Svg.envelope_ui(),
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
]);
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
function createNewPoint(tags: any[]){
const loc = State.state.LastClickLocation.data;
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
function createNewPoint(tags: any[], location: { lat: number, lon: number }) {
let feature = State.state.changes.createElement(tags, location.lat, location.lon);
State.state.selectedElement.setData(feature);
}
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
const addUi = new VariableUiElement(
@ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle {
return presetsOverview
}
return SimpleAddUI.CreateConfirmButton(preset,
tags => {
createNewPoint(tags)
(tags, location) => {
createNewPoint(tags, location)
selectedPreset.setData(undefined)
}, () => {
selectedPreset.setData(undefined)
@ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle {
addUi,
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)
),
readYourMessages,
@ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle {
}
private static CreateConfirmButton(preset: PresetInfo,
confirm: (tags: any[]) => void,
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
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([
Translations.t.general.add.addNew.Subs({category: preset.name}),
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col")
).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(
Svg.layers_ui(),
new Combine([
@ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle {
Translations.t.general.add.openLayerControl
])
)
.onClick(() => State.state.layerControlIsOpened.setData(true))
.onClick(() => State.state.layerControlIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
confirmButton,
openLayerControl,
@ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle {
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel )
).onClick(cancel)
return new Combine([
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
State.state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
State.state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
openLayerOrConfirm,
cancelButton,
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(
preset.icon,
preset.icon(),
new Combine([
Translations.t.general.add.addNew.Subs({
category: preset.name
@ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle {
]).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 {
const allButtons = [];
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;
}
const presets = layer.layerDef.presets;
for (const preset of presets) {
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");
const presetInfo: PresetInfo = {
tags: preset.tags,
layerToAddTo: layer,
name: preset.title,
description: preset.description,
icon: icon
icon: icon,
preciseInput: preset.preciseInput
}
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);

View file

@ -68,7 +68,7 @@ export default class UserBadge extends Toggle {
if (user.unreadMessages > 0) {
messageSpan = new Link(
new Combine([Svg.envelope, "" + user.unreadMessages]),
'${user.backend}/messages/inbox',
`${user.backend}/messages/inbox`,
true
).SetClass("alert")
}

View file

@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> {
})
this.RegisterTriggers(element)
element.style.overflow = "hidden"
return element;
}

View 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();
}
}

76
UI/Input/LocationInput.ts Normal file
View 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();
}
}

View file

@ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> {
const block = document.createElement("div")
block.appendChild(input)
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)
form.appendChild(block)

View file

@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
this.SetClass("form-text-field")
let inputEl: HTMLElement
if (options.htmlType === "area") {
this.SetClass("w-full box-border max-w-full")
const el = document.createElement("textarea")
el.placeholder = placeholder
el.rows = options.textAreaRows
el.cols = 50
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
inputEl = el;
} else {
const el = document.createElement("input")

View file

@ -282,7 +282,7 @@ export default class ValidatedTextField {
})
)
unitDropDown.GetValue().setData(unit.defaultDenom)
unitDropDown.SetStyle("width: min-content")
unitDropDown.SetClass("w-min")
input = new CombinedInputElement(
input,

View file

@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement";
import {DropDown} from "../Input/DropDown";
import {Unit} from "../../Customizations/JSON/Denomination";
import InputElementWrapper from "../Input/InputElementWrapper";
/**
* 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))
}
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
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
(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;
if (freeform === undefined) {
return undefined;
@ -328,7 +329,8 @@ export default class TagRenderingQuestion extends Combine {
return undefined;
}
let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
const tagsData = tags.data;
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
isValid: (str) => (str.length <= 255),
country: () => tagsData._country,
location: [tagsData._lat, tagsData._lon],
@ -336,12 +338,22 @@ export default class TagRenderingQuestion extends Combine {
unit: applicableUnit
});
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),
pickString, toString
);
if(freeform.inline){
inputTagsFilter.SetClass("w-16-imp")
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
inputTagsFilter.SetClass("block")
}
return inputTagsFilter;
}

View file

@ -80,9 +80,7 @@ export default class ShowDataLayer {
if (zoomToFeatures) {
try {
mp.fitBounds(geoLayer.getBounds())
mp.fitBounds(geoLayer.getBounds(), {animate: false})
} catch (e) {
console.error(e)
}

View file

@ -369,7 +369,6 @@ export default class SpecialVisualizations {
if (unit === undefined) {
return value;
}
return unit.asHumanLongValue(value);
},
@ -379,6 +378,7 @@ export default class SpecialVisualizations {
}
]
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
private static GenHelpMessage() {

View file

@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
import {Utils} from "../Utils";
import {VariableUiElement} from "./Base/VariableUIElement";
import Combine from "./Base/Combine";
import BaseUIElement from "./BaseUIElement";
export class SubstitutedTranslation extends VariableUiElement {
public constructor(
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(
Locale.language.map(language => {
const txt = translation.textFor(language)
let txt = translation.textFor(language);
if (txt === 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 => {
if (proto.fixed !== undefined) {
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
})
)
this.SetClass("w-full")
}
public static ExtractSpecialComponents(template: string): {
fixed?: string, special?: {
public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
fixed?: string,
special?: {
func: SpecialVisualization,
args: 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'
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
if (matched != null) {
// 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 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 ?? "");
if (argument.length > 0) {
const realArgs = argument.split(",").map(str => str.trim());
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
}
let element;
element = {special:{
args: args,
style: style,
func: knownSpecial
}}
element = {
special: {
args: args,
style: style,
func: knownSpecial
}
}
return [...partBefore, element, ...partAfter]
}
}

View file

@ -1,4 +1,5 @@
import * as colors from "./assets/colors.json"
import {TileRange} from "./Models/TileRange";
export class Utils {
@ -134,7 +135,7 @@ export class Utils {
}
return newArr;
}
public static MergeTags(a: any, b: any) {
const t = {};
for (const k in a) {
@ -447,14 +448,12 @@ export class Utils {
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
}

View file

@ -12,7 +12,8 @@
"ru": "Скамейки",
"zh_Hans": "长椅",
"zh_Hant": "長椅",
"nb_NO": "Benker"
"nb_NO": "Benker",
"fi": "Penkit"
},
"minzoom": 14,
"source": {
@ -31,7 +32,8 @@
"ru": "Скамейка",
"zh_Hans": "长椅",
"zh_Hant": "長椅",
"nb_NO": "Benk"
"nb_NO": "Benk",
"fi": "Penkki"
}
},
"tagRenderings": [
@ -49,7 +51,8 @@
"ru": "Спинка",
"zh_Hans": "靠背",
"zh_Hant": "靠背",
"nb_NO": "Rygglene"
"nb_NO": "Rygglene",
"fi": "Selkänoja"
},
"freeform": {
"key": "backrest"
@ -69,7 +72,8 @@
"ru": "Со спинкой",
"zh_Hans": "靠背:有",
"zh_Hant": "靠背:有",
"nb_NO": "Rygglene: Ja"
"nb_NO": "Rygglene: Ja",
"fi": "Selkänoja: kyllä"
}
},
{
@ -86,7 +90,8 @@
"ru": "Без спинки",
"zh_Hans": "靠背:无",
"zh_Hant": "靠背:無",
"nb_NO": "Rygglene: Nei"
"nb_NO": "Rygglene: Nei",
"fi": "Selkänoja: ei"
}
}
],
@ -149,7 +154,8 @@
"ru": "Материал: {material}",
"zh_Hans": "材质: {material}",
"zh_Hant": "材質:{material}",
"nb_NO": "Materiale: {material}"
"nb_NO": "Materiale: {material}",
"fi": "Materiaali: {material}"
},
"freeform": {
"key": "material",
@ -170,7 +176,8 @@
"zh_Hans": "材质:木",
"nb_NO": "Materiale: tre",
"zh_Hant": "材質:木頭",
"pt_BR": "Material: madeira"
"pt_BR": "Material: madeira",
"fi": "Materiaali: puu"
}
},
{
@ -203,7 +210,8 @@
"zh_Hans": "材质:石头",
"nb_NO": "Materiale: stein",
"zh_Hant": "材質:石頭",
"pt_BR": "Material: pedra"
"pt_BR": "Material: pedra",
"fi": "Materiaali: kivi"
}
},
{
@ -220,7 +228,8 @@
"zh_Hans": "材质:混凝土",
"nb_NO": "Materiale: betong",
"zh_Hant": "材質:水泥",
"pt_BR": "Material: concreto"
"pt_BR": "Material: concreto",
"fi": "Materiaali: betoni"
}
},
{
@ -237,7 +246,8 @@
"zh_Hans": "材质:塑料",
"nb_NO": "Materiale: plastikk",
"zh_Hant": "材質:塑膠",
"pt_BR": "Material: plástico"
"pt_BR": "Material: plástico",
"fi": "Materiaali: muovi"
}
},
{
@ -254,7 +264,8 @@
"zh_Hans": "材质:不锈钢",
"nb_NO": "Materiale: stål",
"zh_Hant": "材質:鋼鐵",
"pt_BR": "Material: aço"
"pt_BR": "Material: aço",
"fi": "Materiaali: teräs"
}
}
],
@ -313,7 +324,8 @@
"zh_Hans": "颜色: {colour}",
"zh_Hant": "顏色:{colour}",
"nb_NO": "Farge: {colour}",
"pt_BR": "Cor: {colour}"
"pt_BR": "Cor: {colour}",
"fi": "Väri: {colour}"
},
"question": {
"en": "Which colour does this bench have?",
@ -345,7 +357,8 @@
"zh_Hans": "颜色:棕",
"zh_Hant": "顏色:棕色",
"nb_NO": "Farge: brun",
"pt_BR": "Cor: marrom"
"pt_BR": "Cor: marrom",
"fi": "Väri: ruskea"
}
},
{
@ -361,7 +374,8 @@
"zh_Hans": "颜色:绿",
"zh_Hant": "顏色:綠色",
"nb_NO": "Farge: grønn",
"pt_BR": "Cor: verde"
"pt_BR": "Cor: verde",
"fi": "Väri: vihreä"
}
},
{
@ -377,7 +391,8 @@
"zh_Hans": "颜色:灰",
"zh_Hant": "顏色:灰色",
"nb_NO": "Farge: grå",
"pt_BR": "Cor: cinza"
"pt_BR": "Cor: cinza",
"fi": "Väri: harmaa"
}
},
{
@ -393,7 +408,8 @@
"zh_Hans": "颜色:白",
"zh_Hant": "顏色:白色",
"nb_NO": "Farge: hvit",
"pt_BR": "Cor: branco"
"pt_BR": "Cor: branco",
"fi": "Väri: valkoinen"
}
},
{
@ -409,7 +425,8 @@
"zh_Hans": "颜色:红",
"zh_Hant": "顏色:紅色",
"nb_NO": "Farge: rød",
"pt_BR": "Cor: vermelho"
"pt_BR": "Cor: vermelho",
"fi": "Väri: punainen"
}
},
{
@ -425,7 +442,8 @@
"zh_Hans": "颜色:黑",
"zh_Hant": "顏色:黑色",
"nb_NO": "Farge: svart",
"pt_BR": "Cor: preto"
"pt_BR": "Cor: preto",
"fi": "Väri: musta"
}
},
{
@ -441,7 +459,8 @@
"zh_Hans": "颜色:蓝",
"zh_Hant": "顏色:藍色",
"nb_NO": "Farge: blå",
"pt_BR": "Cor: azul"
"pt_BR": "Cor: azul",
"fi": "Väri: sininen"
}
},
{
@ -457,7 +476,8 @@
"zh_Hans": "颜色:黄",
"zh_Hant": "顏色:黃色",
"nb_NO": "Farge: gul",
"pt_BR": "Cor: amarelo"
"pt_BR": "Cor: amarelo",
"fi": "Väri: keltainen"
}
}
]
@ -528,7 +548,8 @@
"zh_Hans": "长椅",
"nb_NO": "Benk",
"zh_Hant": "長椅",
"pt_BR": "Banco"
"pt_BR": "Banco",
"fi": "Penkki"
},
"description": {
"en": "Add a new bench",
@ -542,7 +563,8 @@
"zh_Hans": "增加一个新的长椅",
"nb_NO": "Legg til en ny benk",
"zh_Hant": "新增長椅",
"pt_BR": "Adicionar um novo banco"
"pt_BR": "Adicionar um novo banco",
"fi": "Lisää uusi penkki"
}
}
]

View file

@ -37,7 +37,8 @@
"zh_Hans": "长椅",
"nb_NO": "Benk",
"zh_Hant": "長椅",
"pt_BR": "Banco"
"pt_BR": "Banco",
"fi": "Penkki"
},
"mappings": [
{
@ -96,7 +97,8 @@
"id": "{name}",
"zh_Hans": "{name}",
"zh_Hant": "{name}",
"pt_BR": "{name}"
"pt_BR": "{name}",
"fi": "{name}"
},
"freeform": {
"key": "name"

View file

@ -145,7 +145,8 @@
"fr": "Emprunter un vélo coûte 20 €/an et 20 € de garantie",
"it": "Il prestito di una bicicletta costa 20 €/anno più 20 € di garanzia",
"de": "Das Ausleihen eines Fahrrads kostet 20€ pro Jahr und 20€ Gebühr",
"zh_Hant": "租借單車價錢 €20/year 與 €20 保證金"
"zh_Hant": "租借單車價錢 €20/year 與 €20 保證金",
"ru": "Прокат велосипеда стоит €20/год и €20 залог"
}
}
]

View file

@ -117,7 +117,8 @@
"de": "Dieses Fahrrad-Café bietet eine Fahrradpumpe an, die von jedem benutzt werden kann",
"it": "Questo caffè in bici offre una pompa per bici liberamente utilizzabile",
"zh_Hans": "这家自行车咖啡为每个人提供打气筒",
"zh_Hant": "這個單車咖啡廳有提供給任何人都能使用的單車打氣甬"
"zh_Hant": "這個單車咖啡廳有提供給任何人都能使用的單車打氣甬",
"ru": "В этом велосипедном кафе есть велосипедный насос для всеобщего использования"
}
},
{
@ -130,7 +131,8 @@
"de": "Dieses Fahrrad-Café bietet keine Fahrradpumpe an, die von jedem benutzt werden kann",
"it": "Questo caffè in bici non offre una pompa per bici liberamente utilizzabile",
"zh_Hans": "这家自行车咖啡不为每个人提供打气筒",
"zh_Hant": "這個單車咖啡廳並沒有為所有人提供單車打氣甬"
"zh_Hant": "這個單車咖啡廳並沒有為所有人提供單車打氣甬",
"ru": "В этом велосипедном кафе нет велосипедного насоса для всеобщего использования"
}
}
]
@ -144,7 +146,8 @@
"de": "Gibt es hier Werkzeuge, um das eigene Fahrrad zu reparieren?",
"it": "Ci sono degli strumenti per riparare la propria bicicletta?",
"zh_Hans": "这里有供你修车用的工具吗?",
"zh_Hant": "這裡是否有工具修理你的單車嗎?"
"zh_Hant": "這裡是否有工具修理你的單車嗎?",
"ru": "Есть ли здесь инструменты для починки вашего велосипеда?"
},
"mappings": [
{
@ -157,7 +160,8 @@
"de": "Dieses Fahrrad-Café bietet Werkzeuge für die selbständige Reparatur an",
"it": "Questo caffè in bici fornisce degli attrezzi per la riparazione fai-da-te",
"zh_Hans": "这家自行车咖啡为DIY修理者提供工具",
"zh_Hant": "這個單車咖啡廳提供工具讓你修理"
"zh_Hant": "這個單車咖啡廳提供工具讓你修理",
"ru": "В этом велосипедном кафе есть инструменты для починки своего велосипеда"
}
},
{
@ -170,7 +174,8 @@
"de": "Dieses Fahrrad-Café bietet keine Werkzeuge für die selbständige Reparatur an",
"it": "Questo caffè in bici non fornisce degli attrezzi per la riparazione fai-da-te",
"zh_Hans": "这家自行车咖啡不为DIY修理者提供工具",
"zh_Hant": "這個單車咖啡廳並沒有提供工具讓你修理"
"zh_Hant": "這個單車咖啡廳並沒有提供工具讓你修理",
"ru": "В этом велосипедном кафе нет инструментов для починки своего велосипеда"
}
}
]
@ -184,7 +189,8 @@
"de": "Repariert dieses Fahrrad-Café Fahrräder?",
"it": "Questo caffè in bici ripara le bici?",
"zh_Hans": "这家自行车咖啡t提供修车服务吗",
"zh_Hant": "這個單車咖啡廳是否能修理單車?"
"zh_Hant": "這個單車咖啡廳是否能修理單車?",
"ru": "Есть ли услуги ремонта велосипедов в этом велосипедном кафе?"
},
"mappings": [
{
@ -197,7 +203,8 @@
"de": "Dieses Fahrrad-Café repariert Fahrräder",
"it": "Questo caffè in bici ripara le bici",
"zh_Hans": "这家自行车咖啡可以修车",
"zh_Hant": "這個單車咖啡廳修理單車"
"zh_Hant": "這個單車咖啡廳修理單車",
"ru": "В этом велосипедном кафе есть услуги ремонта велосипедов"
}
},
{
@ -210,7 +217,8 @@
"de": "Dieses Fahrrad-Café repariert keine Fahrräder",
"it": "Questo caffè in bici non ripara le bici",
"zh_Hans": "这家自行车咖啡不能修车",
"zh_Hant": "這個單車咖啡廳並不修理單車"
"zh_Hant": "這個單車咖啡廳並不修理單車",
"ru": "В этом велосипедном кафе нет услуг ремонта велосипедов"
}
}
]
@ -275,7 +283,8 @@
"fr": "Quand ce Café vélo est-t-il ouvert ?",
"it": "Quando è aperto questo caffè in bici?",
"zh_Hans": "这家自行车咖啡什么时候开门营业?",
"zh_Hant": "何時這個單車咖啡廳營運?"
"zh_Hant": "何時這個單車咖啡廳營運?",
"ru": "Каков режим работы этого велосипедного кафе?"
},
"render": "{opening_hours_table(opening_hours)}",
"freeform": {

View file

@ -5,7 +5,8 @@
"nl": "Telstation",
"fr": "Stations de contrôle",
"it": "Stazioni di monitoraggio",
"zh_Hant": "監視站"
"zh_Hant": "監視站",
"ru": "Станции мониторинга"
},
"minzoom": 12,
"source": {

View file

@ -77,7 +77,8 @@
"de": "Dies ist ein Fahrrad-Parkplatz der Art: {bicycle_parking}",
"hu": "Ez egy {bicycle_parking} típusú kerékpáros parkoló",
"it": "È un parcheggio bici del tipo: {bicycle_parking}",
"zh_Hant": "這個單車停車場的類型是:{bicycle_parking}"
"zh_Hant": "這個單車停車場的類型是:{bicycle_parking}",
"ru": "Это велопарковка типа {bicycle_parking}"
},
"freeform": {
"key": "bicycle_parking",
@ -288,7 +289,8 @@
"fr": "Ce parking est couvert (il a un toit)",
"hu": "A parkoló fedett",
"it": "È un parcheggio coperto (ha un tetto)",
"zh_Hant": "這個停車場有遮蔽 (有屋頂)"
"zh_Hant": "這個停車場有遮蔽 (有屋頂)",
"ru": "Это крытая парковка (есть крыша/навес)"
}
},
{
@ -301,7 +303,8 @@
"fr": "Ce parking n'est pas couvert",
"hu": "A parkoló nem fedett",
"it": "Non è un parcheggio coperto",
"zh_Hant": "這個停車場沒有遮蔽"
"zh_Hant": "這個停車場沒有遮蔽",
"ru": "Это открытая парковка"
}
}
]
@ -324,7 +327,8 @@
"gl": "Lugar para {capacity} bicicletas",
"de": "Platz für {capacity} Fahrräder",
"it": "Posti per {capacity} bici",
"zh_Hant": "{capacity} 單車的地方"
"zh_Hant": "{capacity} 單車的地方",
"ru": "Место для {capacity} велосипеда(ов)"
},
"freeform": {
"key": "capacity",
@ -339,7 +343,8 @@
"fr": "Qui peut utiliser ce parking à vélo ?",
"it": "Chi può usare questo parcheggio bici?",
"de": "Wer kann diesen Fahrradparplatz nutzen?",
"zh_Hant": "誰可以使用這個單車停車場?"
"zh_Hant": "誰可以使用這個單車停車場?",
"ru": "Кто может пользоваться этой велопарковкой?"
},
"render": {
"en": "{access}",
@ -349,7 +354,8 @@
"it": "{access}",
"ru": "{access}",
"id": "{access}",
"zh_Hant": "{access}"
"zh_Hant": "{access}",
"fi": "{access}"
},
"freeform": {
"key": "access",

View file

@ -218,7 +218,8 @@
"en": "When is this bicycle repair point open?",
"fr": "Quand ce point de réparation de vélo est-il ouvert ?",
"it": "Quando è aperto questo punto riparazione bici?",
"de": "Wann ist diese Fahrradreparaturstelle geöffnet?"
"de": "Wann ist diese Fahrradreparaturstelle geöffnet?",
"ru": "Когда работает эта точка обслуживания велосипедов?"
},
"render": "{opening_hours_table()}",
"freeform": {
@ -233,7 +234,8 @@
"en": "Always open",
"fr": "Ouvert en permanence",
"it": "Sempre aperto",
"de": "Immer geöffnet"
"de": "Immer geöffnet",
"ru": "Всегда открыто"
}
},
{
@ -512,7 +514,8 @@
"render": {
"en": "./assets/layers/bike_repair_station/repair_station.svg",
"ru": "./assets/layers/bike_repair_station/repair_station.svg",
"it": "./assets/layers/bike_repair_station/repair_station.svg"
"it": "./assets/layers/bike_repair_station/repair_station.svg",
"fi": "./assets/layers/bike_repair_station/repair_station.svg"
},
"mappings": [
{
@ -584,7 +587,8 @@
"gl": "Bomba de ar",
"de": "Fahrradpumpe",
"it": "Pompa per bici",
"ru": "Велосипедный насос"
"ru": "Велосипедный насос",
"fi": "Pyöräpumppu"
},
"tags": [
"amenity=bicycle_repair_station",

View file

@ -6,7 +6,8 @@
"fr": "Magasin ou réparateur de vélo",
"gl": "Tenda/arranxo de bicicletas",
"de": "Fahrradwerkstatt/geschäft",
"it": "Venditore/riparatore bici"
"it": "Venditore/riparatore bici",
"ru": "Обслуживание велосипедов/магазин"
},
"minzoom": 13,
"source": {
@ -54,7 +55,8 @@
"fr": "Magasin ou réparateur de vélo",
"gl": "Tenda/arranxo de bicicletas",
"de": "Fahrradwerkstatt/geschäft",
"it": "Venditore/riparatore bici"
"it": "Venditore/riparatore bici",
"ru": "Обслуживание велосипедов/магазин"
},
"mappings": [
{
@ -207,7 +209,8 @@
"fr": "Ce magasin s'appelle {name}",
"gl": "Esta tenda de bicicletas chámase {name}",
"de": "Dieses Fahrradgeschäft heißt {name}",
"it": "Questo negozio di biciclette è chiamato {name}"
"it": "Questo negozio di biciclette è chiamato {name}",
"ru": "Этот магазин велосипедов называется {name}"
},
"freeform": {
"key": "name"
@ -284,7 +287,8 @@
"fr": "Est-ce que ce magasin vend des vélos ?",
"gl": "Esta tenda vende bicicletas?",
"de": "Verkauft dieser Laden Fahrräder?",
"it": "Questo negozio vende bici?"
"it": "Questo negozio vende bici?",
"ru": "Продаются ли велосипеды в этом магазине?"
},
"mappings": [
{
@ -368,7 +372,8 @@
"fr": "Ce magasin ne répare seulement des marques spécifiques",
"gl": "Esta tenda só arranxa bicicletas dunha certa marca",
"de": "Dieses Geschäft repariert nur Fahrräder einer bestimmten Marke",
"it": "Questo negozio ripara solo le biciclette di una certa marca"
"it": "Questo negozio ripara solo le biciclette di una certa marca",
"ru": "В этом магазине обслуживают велосипеды определённого бренда"
}
}
]
@ -466,7 +471,8 @@
"fr": "Est-ce que ce magasin offre une pompe en accès libre ?",
"gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa?",
"de": "Bietet dieses Geschäft eine Fahrradpumpe zur Benutzung für alle an?",
"it": "Questo negozio offre luso a chiunque di una pompa per bici?"
"it": "Questo negozio offre luso a chiunque di una pompa per bici?",
"ru": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?"
},
"mappings": [
{
@ -477,7 +483,8 @@
"fr": "Ce magasin offre une pompe en acces libre",
"gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa",
"de": "Dieses Geschäft bietet eine Fahrradpumpe für alle an",
"it": "Questo negozio offre luso pubblico di una pompa per bici"
"it": "Questo negozio offre luso pubblico di una pompa per bici",
"ru": "В этом магазине есть велосипедный насос для всеобщего пользования"
}
},
{
@ -488,7 +495,8 @@
"fr": "Ce magasin n'offre pas de pompe en libre accès",
"gl": "Esta tenda non ofrece unha bomba de ar para uso de calquera persoa",
"de": "Dieses Geschäft bietet für niemanden eine Fahrradpumpe an",
"it": "Questo negozio non offre luso pubblico di una pompa per bici"
"it": "Questo negozio non offre luso pubblico di una pompa per bici",
"ru": "В этом магазине нет велосипедного насоса для всеобщего пользования"
}
},
{
@ -509,7 +517,8 @@
"fr": "Est-ce qu'il y a des outils pour réparer son vélo dans ce magasin ?",
"gl": "Hai ferramentas aquí para arranxar a túa propia bicicleta?",
"de": "Gibt es hier Werkzeuge, um das eigene Fahrrad zu reparieren?",
"it": "Sono presenti degli attrezzi per riparare la propria bici?"
"it": "Sono presenti degli attrezzi per riparare la propria bici?",
"ru": "Есть ли здесь инструменты для починки собственного велосипеда?"
},
"mappings": [
{
@ -541,7 +550,8 @@
"nl": "Het gereedschap aan om je fiets zelf te herstellen is enkel voor als je de fiets er kocht of huurt",
"fr": "Des outils d'auto-réparation sont disponibles uniquement si vous avez acheté ou loué le vélo dans ce magasin",
"it": "Gli attrezzi per la riparazione fai-da-te sono disponibili solamente se hai acquistato/noleggiato la bici nel negozio",
"de": "Werkzeuge für die Selbstreparatur sind nur verfügbar, wenn Sie das Fahrrad im Laden gekauft/gemietet haben"
"de": "Werkzeuge für die Selbstreparatur sind nur verfügbar, wenn Sie das Fahrrad im Laden gekauft/gemietet haben",
"ru": "Инструменты для починки доступны только при покупке/аренде велосипеда в магазине"
}
}
]
@ -563,7 +573,8 @@
"nl": "Deze winkel biedt fietsschoonmaak aan",
"fr": "Ce magasin lave les vélos",
"it": "Questo negozio lava le biciclette",
"de": "Dieses Geschäft reinigt Fahrräder"
"de": "Dieses Geschäft reinigt Fahrräder",
"ru": "В этом магазине оказываются услуги мойки/чистки велосипедов"
}
},
{
@ -583,7 +594,8 @@
"nl": "Deze winkel biedt geen fietsschoonmaak aan",
"fr": "Ce magasin ne fait pas le nettoyage de vélo",
"it": "Questo negozio non offre la pulizia della bicicletta",
"de": "Dieser Laden bietet keine Fahrradreinigung an"
"de": "Dieser Laden bietet keine Fahrradreinigung an",
"ru": "В этом магазине нет услуг мойки/чистки велосипедов"
}
}
]

View file

@ -567,7 +567,8 @@
"nl": "Extra informatie voor OpenStreetMap experts: {fixme}",
"fr": "Informations supplémentaires pour les experts d'OpenStreetMap : {fixme}",
"it": "Informazioni supplementari per gli esperti di OpenStreetMap: {fixme}",
"de": "Zusätzliche Informationen für OpenStreetMap-Experten: {fixme}"
"de": "Zusätzliche Informationen für OpenStreetMap-Experten: {fixme}",
"ru": "Дополнительная информация для экспертов OpenStreetMap: {fixme}"
},
"question": {
"en": "Is there something wrong with how this is mapped, that you weren't able to fix here? (leave a note to OpenStreetMap experts)",

View file

@ -76,7 +76,8 @@
"nl": "Ter nagedachtenis van {name}",
"de": "Im Gedenken an {name}",
"it": "In ricordo di {name}",
"fr": "En souvenir de {name}"
"fr": "En souvenir de {name}",
"ru": "В знак памяти о {name}"
},
"freeform": {
"key": "name"
@ -149,7 +150,8 @@
"nl": "Geplaatst op {start_date}",
"en": "Placed on {start_date}",
"it": "Piazzata in data {start_date}",
"fr": "Placé le {start_date}"
"fr": "Placé le {start_date}",
"ru": "Установлен {start_date}"
},
"freeform": {
"key": "start_date",

View file

@ -26,7 +26,8 @@
"en": "The layer showing picnic tables",
"nl": "Deze laag toont picnictafels",
"it": "Il livello che mostra i tavoli da picnic",
"fr": "La couche montrant les tables de pique-nique"
"fr": "La couche montrant les tables de pique-nique",
"ru": "Слой, отображающий столы для пикника"
},
"tagRenderings": [
{
@ -34,13 +35,15 @@
"en": "What material is this picnic table made of?",
"nl": "Van welk materiaal is deze picnictafel gemaakt?",
"it": "Di che materiale è fatto questo tavolo da picnic?",
"de": "Aus welchem Material besteht dieser Picknicktisch?"
"de": "Aus welchem Material besteht dieser Picknicktisch?",
"ru": "Из чего изготовлен этот стол для пикника?"
},
"render": {
"en": "This picnic table is made of {material}",
"nl": "Deze picnictafel is gemaakt van {material}",
"it": "Questo tavolo da picnic è fatto di {material}",
"de": "Dieser Picknicktisch besteht aus {material}"
"de": "Dieser Picknicktisch besteht aus {material}",
"ru": "Этот стол для пикника сделан из {material}"
},
"freeform": {
"key": "material"

View file

@ -93,7 +93,8 @@
"nl": "De ondergrond bestaat uit <b>houtsnippers</b>",
"en": "The surface consist of <b>woodchips</b>",
"it": "La superficie consiste di <b>trucioli di legno</b>",
"de": "Die Oberfläche besteht aus <b>Holzschnitzeln</b>"
"de": "Die Oberfläche besteht aus <b>Holzschnitzeln</b>",
"ru": "Покрытие из <b>щепы</b>"
}
},
{
@ -154,7 +155,8 @@
"en": "Is this playground lit at night?",
"it": "È illuminato di notte questo parco giochi?",
"fr": "Ce terrain de jeux est-il éclairé la nuit ?",
"de": "Ist dieser Spielplatz nachts beleuchtet?"
"de": "Ist dieser Spielplatz nachts beleuchtet?",
"ru": "Эта игровая площадка освещается ночью?"
},
"mappings": [
{
@ -163,7 +165,8 @@
"nl": "Deze speeltuin is 's nachts verlicht",
"en": "This playground is lit at night",
"it": "Questo parco giochi è illuminato di notte",
"de": "Dieser Spielplatz ist nachts beleuchtet"
"de": "Dieser Spielplatz ist nachts beleuchtet",
"ru": "Эта детская площадка освещается ночью"
}
},
{
@ -172,7 +175,8 @@
"nl": "Deze speeltuin is 's nachts niet verlicht",
"en": "This playground is not lit at night",
"it": "Questo parco giochi non è illuminato di notte",
"de": "Dieser Spielplatz ist nachts nicht beleuchtet"
"de": "Dieser Spielplatz ist nachts nicht beleuchtet",
"ru": "Эта детская площадка не освещается ночью"
}
}
]
@ -189,7 +193,8 @@
"nl": "Wat is de minimale leeftijd om op deze speeltuin te mogen?",
"en": "What is the minimum age required to access this playground?",
"it": "Qual è letà minima per accedere a questo parco giochi?",
"fr": "Quel est l'âge minimal requis pour accéder à ce terrain de jeux ?"
"fr": "Quel est l'âge minimal requis pour accéder à ce terrain de jeux ?",
"ru": "С какого возраста доступна эта детская площадка?"
},
"freeform": {
"key": "min_age",
@ -201,7 +206,8 @@
"nl": "Toegankelijk tot {max_age}",
"en": "Accessible to kids of at most {max_age}",
"it": "Accessibile ai bambini di età inferiore a {max_age}",
"fr": "Accessible aux enfants de {max_age} au maximum"
"fr": "Accessible aux enfants de {max_age} au maximum",
"ru": "Доступно детям до {max_age}"
},
"question": {
"nl": "Wat is de maximaal toegestane leeftijd voor deze speeltuin?",
@ -340,7 +346,8 @@
"en": "Is this playground accessible to wheelchair users?",
"fr": "Ce terrain de jeux est-il accessible aux personnes en fauteuil roulant ?",
"de": "Ist dieser Spielplatz für Rollstuhlfahrer zugänglich?",
"it": "Il campetto è accessibile a persone in sedia a rotelle?"
"it": "Il campetto è accessibile a persone in sedia a rotelle?",
"ru": "Доступна ли детская площадка пользователям кресел-колясок?"
},
"mappings": [
{
@ -350,7 +357,8 @@
"en": "Completely accessible for wheelchair users",
"fr": "Entièrement accessible aux personnes en fauteuil roulant",
"de": "Vollständig zugänglich für Rollstuhlfahrer",
"it": "Completamente accessibile in sedia a rotelle"
"it": "Completamente accessibile in sedia a rotelle",
"ru": "Полностью доступна пользователям кресел-колясок"
}
},
{
@ -360,7 +368,8 @@
"en": "Limited accessibility for wheelchair users",
"fr": "Accessibilité limitée pour les personnes en fauteuil roulant",
"de": "Eingeschränkte Zugänglichkeit für Rollstuhlfahrer",
"it": "Accesso limitato in sedia a rotelle"
"it": "Accesso limitato in sedia a rotelle",
"ru": "Частично доступна пользователям кресел-колясок"
}
},
{
@ -370,7 +379,8 @@
"en": "Not accessible for wheelchair users",
"fr": "Non accessible aux personnes en fauteuil roulant",
"de": "Nicht zugänglich für Rollstuhlfahrer",
"it": "Non accessibile in sedia a rotelle"
"it": "Non accessibile in sedia a rotelle",
"ru": "Недоступна пользователям кресел-колясок"
}
}
]
@ -385,7 +395,8 @@
"nl": "Op welke uren is deze speeltuin toegankelijk?",
"en": "When is this playground accessible?",
"fr": "Quand ce terrain de jeux est-il accessible ?",
"it": "Quando si può accedere a questo campetto?"
"it": "Quando si può accedere a questo campetto?",
"ru": "Когда открыта эта игровая площадка?"
},
"mappings": [
{
@ -394,7 +405,8 @@
"nl": "Van zonsopgang tot zonsondergang",
"en": "Accessible from sunrise till sunset",
"fr": "Accessible du lever au coucher du soleil",
"it": "Si può accedere dall'alba al tramonto"
"it": "Si può accedere dall'alba al tramonto",
"ru": "Открыто от рассвета до заката"
}
},
{

View file

@ -13,7 +13,8 @@
"nl": "Een straatkastje met boeken voor iedereen",
"de": "Ein Bücherschrank am Straßenrand mit Büchern, für jedermann zugänglich",
"fr": "Une armoire ou une boite contenant des livres en libre accès",
"it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico"
"it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico",
"ru": "Уличный шкаф с книгами, доступными для всех"
},
"source": {
"osmTags": "amenity=public_bookcase"
@ -72,7 +73,10 @@
},
"tags": [
"amenity=public_bookcase"
]
],
"preciseInput": {
"preferredBackground": "photo"
}
}
],
"tagRenderings": [
@ -94,7 +98,7 @@
"nl": "Wat is de naam van dit boekenuilkastje?",
"de": "Wie heißt dieser öffentliche Bücherschrank?",
"fr": "Quel est le nom de cette microbibliothèque ?",
"ru": "Как называется общественный книжный шкаф?",
"ru": "Как называется этот общественный книжный шкаф?",
"it": "Come si chiama questa microbiblioteca pubblica?"
},
"freeform": {
@ -125,7 +129,8 @@
"nl": "Er passen {capacity} boeken",
"de": "{capacity} Bücher passen in diesen Bücherschrank",
"fr": "{capacity} livres peuvent entrer dans cette microbibliothèque",
"it": "Questa microbiblioteca può contenere fino a {capacity} libri"
"it": "Questa microbiblioteca può contenere fino a {capacity} libri",
"ru": "{capacity} книг помещается в этот книжный шкаф"
},
"question": {
"en": "How many books fit into this public bookcase?",
@ -137,7 +142,8 @@
},
"freeform": {
"key": "capacity",
"type": "nat"
"type": "nat",
"inline": true
}
},
{
@ -146,7 +152,8 @@
"nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?",
"de": "Welche Art von Büchern sind in diesem öffentlichen Bücherschrank zu finden?",
"fr": "Quel type de livres peut-on dans cette microbibliothèque ?",
"it": "Che tipo di libri si possono trovare in questa microbiblioteca?"
"it": "Che tipo di libri si possono trovare in questa microbiblioteca?",
"ru": "Какие книги можно найти в этом общественном книжном шкафу?"
},
"mappings": [
{
@ -178,7 +185,8 @@
"nl": "Boeken voor zowel kinderen als volwassenen",
"de": "Sowohl Bücher für Kinder als auch für Erwachsene",
"fr": "Livres pour enfants et adultes également",
"it": "Sia libri per l'infanzia, sia per l'età adulta"
"it": "Sia libri per l'infanzia, sia per l'età adulta",
"ru": "Книги и для детей, и для взрослых"
}
}
]
@ -231,7 +239,8 @@
"nl": "Is dit boekenruilkastje publiek toegankelijk?",
"de": "Ist dieser öffentliche Bücherschrank frei zugänglich?",
"fr": "Cette microbibliothèque est-elle librement accèssible ?",
"it": "Questa microbiblioteca è ad accesso libero?"
"it": "Questa microbiblioteca è ad accesso libero?",
"ru": "Имеется ли свободный доступ к этому общественному книжному шкафу?"
},
"condition": "indoor=yes",
"mappings": [
@ -241,7 +250,8 @@
"nl": "Publiek toegankelijk",
"de": "Öffentlich zugänglich",
"fr": "Accèssible au public",
"it": "È ad accesso libero"
"it": "È ad accesso libero",
"ru": "Свободный доступ"
},
"if": "access=yes"
},
@ -373,14 +383,16 @@
"nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?",
"de": "Wann wurde dieser öffentliche Bücherschrank installiert?",
"fr": "Quand a été installée cette microbibliothèque ?",
"it": "Quando è stata inaugurata questa microbiblioteca?"
"it": "Quando è stata inaugurata questa microbiblioteca?",
"ru": "Когда был установлен этот общественный книжный шкаф?"
},
"render": {
"en": "Installed on {start_date}",
"nl": "Geplaatst op {start_date}",
"de": "Installiert am {start_date}",
"fr": "Installée le {start_date}",
"it": "È stata inaugurata il {start_date}"
"it": "È stata inaugurata il {start_date}",
"ru": "Установлен {start_date}"
},
"freeform": {
"key": "start_date",
@ -401,7 +413,8 @@
"nl": "Is er een website over dit boekenruilkastje?",
"de": "Gibt es eine Website mit weiteren Informationen über diesen öffentlichen Bücherschrank?",
"fr": "Y a-t-il un site web avec plus d'informations sur cette microbibliothèque ?",
"it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?"
"it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?",
"ru": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?"
},
"freeform": {
"key": "website",

View file

@ -32,7 +32,8 @@
"nl": "Een sportterrein",
"fr": "Un terrain de sport",
"en": "A sport pitch",
"it": "Un campo sportivo"
"it": "Un campo sportivo",
"ru": "Спортивная площадка"
},
"tagRenderings": [
"images",
@ -64,7 +65,8 @@
"nl": "Hier kan men basketbal spelen",
"fr": "Ici, on joue au basketball",
"en": "Basketball is played here",
"it": "Qui si gioca a basket"
"it": "Qui si gioca a basket",
"ru": "Здесь можно играть в баскетбол"
}
},
{
@ -77,7 +79,8 @@
"nl": "Hier kan men voetbal spelen",
"fr": "Ici, on joue au football",
"en": "Soccer is played here",
"it": "Qui si gioca a calcio"
"it": "Qui si gioca a calcio",
"ru": "Здесь можно играть в футбол"
}
},
{
@ -104,7 +107,8 @@
"nl": "Hier kan men tennis spelen",
"fr": "Ici, on joue au tennis",
"en": "Tennis is played here",
"it": "Qui si gioca a tennis"
"it": "Qui si gioca a tennis",
"ru": "Здесь можно играть в теннис"
}
},
{
@ -117,7 +121,8 @@
"nl": "Hier kan men korfbal spelen",
"fr": "Ici, on joue au korfball",
"en": "Korfball is played here",
"it": "Qui si gioca a korfball"
"it": "Qui si gioca a korfball",
"ru": "Здесь можно играть в корфбол"
}
},
{
@ -130,7 +135,8 @@
"nl": "Hier kan men basketbal beoefenen",
"fr": "Ici, on joue au basketball",
"en": "Basketball is played here",
"it": "Qui si gioca a basket"
"it": "Qui si gioca a basket",
"ru": "Здесь можно играть в баскетбол"
},
"hideInAnswer": true
}
@ -141,7 +147,8 @@
"nl": "Wat is de ondergrond van dit sportveld?",
"fr": "De quelle surface est fait ce terrain de sport ?",
"en": "Which is the surface of this sport pitch?",
"it": "Qual è la superficie di questo campo sportivo?"
"it": "Qual è la superficie di questo campo sportivo?",
"ru": "Какое покрытие на этой спортивной площадке?"
},
"render": {
"nl": "De ondergrond is <b>{surface}</b>",
@ -211,7 +218,8 @@
"nl": "Is dit sportterrein publiek toegankelijk?",
"fr": "Est-ce que ce terrain de sport est accessible au public ?",
"en": "Is this sport pitch publicly accessible?",
"it": "Questo campo sportivo è aperto al pubblico?"
"it": "Questo campo sportivo è aperto al pubblico?",
"ru": "Есть ли свободный доступ к этой спортивной площадке?"
},
"mappings": [
{
@ -220,7 +228,8 @@
"nl": "Publiek toegankelijk",
"fr": "Accessible au public",
"en": "Public access",
"it": "Aperto al pubblico"
"it": "Aperto al pubblico",
"ru": "Свободный доступ"
}
},
{
@ -229,7 +238,8 @@
"nl": "Beperkt toegankelijk (enkel na reservatie, tijdens bepaalde uren, ...)",
"fr": "Accès limité (par exemple uniquement sur réservation, à certains horaires…)",
"en": "Limited access (e.g. only with an appointment, during certain hours, ...)",
"it": "Accesso limitato (p.es. solo con prenotazione, in certi orari, ...)"
"it": "Accesso limitato (p.es. solo con prenotazione, in certi orari, ...)",
"ru": "Ограниченный доступ (напр., только по записи, в определённые часы, ...)"
}
},
{
@ -238,7 +248,8 @@
"nl": "Enkel toegankelijk voor leden van de bijhorende sportclub",
"fr": "Accessible uniquement aux membres du club",
"en": "Only accessible for members of the club",
"it": "Accesso limitato ai membri dell'associazione"
"it": "Accesso limitato ai membri dell'associazione",
"ru": "Доступ только членам клуба"
}
},
{
@ -257,7 +268,8 @@
"nl": "Moet men reserveren om gebruik te maken van dit sportveld?",
"fr": "Doit-on réserver pour utiliser ce terrain de sport ?",
"en": "Does one have to make an appointment to use this sport pitch?",
"it": "È necessario prenotarsi per usare questo campo sportivo?"
"it": "È necessario prenotarsi per usare questo campo sportivo?",
"ru": "Нужна ли предварительная запись для доступа на эту спортивную площадку?"
},
"condition": {
"and": [
@ -282,7 +294,8 @@
"nl": "Reserveren is sterk aangeraden om gebruik te maken van dit sportterrein",
"fr": "Il est recommendé de réserver pour utiliser ce terrain de sport",
"en": "Making an appointment is recommended when using this sport pitch",
"it": "La prenotazione è consigliata per usare questo campo sportivo"
"it": "La prenotazione è consigliata per usare questo campo sportivo",
"ru": "Желательна предварительная запись для доступа на эту спортивную площадку"
}
},
{
@ -291,7 +304,8 @@
"nl": "Reserveren is mogelijk, maar geen voorwaarde",
"fr": "Il est possible de réserver, mais ce n'est pas nécéssaire pour utiliser ce terrain de sport",
"en": "Making an appointment is possible, but not necessary to use this sport pitch",
"it": "La prenotazione è consentita, ma non è obbligatoria per usare questo campo sportivo"
"it": "La prenotazione è consentita, ma non è obbligatoria per usare questo campo sportivo",
"ru": "Предварительная запись для доступа на эту спортивную площадку возможна, но не обязательна"
}
},
{
@ -300,7 +314,8 @@
"nl": "Reserveren is niet mogelijk",
"fr": "On ne peut pas réserver",
"en": "Making an appointment is not possible",
"it": "Non è possibile prenotare"
"it": "Non è possibile prenotare",
"ru": "Невозможна предварительная запись"
}
}
]
@ -336,7 +351,8 @@
"nl": "Wanneer is dit sportveld toegankelijk?",
"fr": "Quand ce terrain est-il accessible ?",
"en": "When is this pitch accessible?",
"it": "Quando è aperto questo campo sportivo?"
"it": "Quando è aperto questo campo sportivo?",
"ru": "В какое время доступна эта площадка?"
},
"render": "Openingsuren: {opening_hours_table()}",
"freeform": {
@ -446,7 +462,8 @@
"nl": "Ping-pong tafel",
"fr": "Table de ping-pong",
"en": "Tabletennis table",
"it": "Tavolo da tennistavolo"
"it": "Tavolo da tennistavolo",
"ru": "Стол для настольного тенниса"
},
"tags": [
"leisure=pitch",

View file

@ -39,7 +39,8 @@
"en": "What kind of camera is this?",
"nl": "Wat voor soort camera is dit?",
"fr": "Quel genre de caméra est-ce ?",
"it": "Di che tipo di videocamera si tratta?"
"it": "Di che tipo di videocamera si tratta?",
"ru": "Какая это камера?"
},
"mappings": [
{
@ -65,7 +66,8 @@
"en": "A dome camera (which can turn)",
"nl": "Een dome (bolvormige camera die kan draaien)",
"fr": "Une caméra dôme (qui peut tourner)",
"it": "Una videocamera a cupola (che può ruotare)"
"it": "Una videocamera a cupola (che può ruotare)",
"ru": "Камера с поворотным механизмом"
}
},
{
@ -230,7 +232,8 @@
"en": "This camera is located outdoors",
"nl": "Deze camera bevindt zich buiten",
"fr": "Cette caméra est située à l'extérieur",
"it": "Questa videocamera si trova all'aperto"
"it": "Questa videocamera si trova all'aperto",
"ru": "Эта камера расположена снаружи"
}
},
{
@ -239,7 +242,8 @@
"en": "This camera is probably located outdoors",
"nl": "Deze camera bevindt zich waarschijnlijk buiten",
"fr": "Cette caméra est probablement située à l'extérieur",
"it": "Questa videocamera si trova probabilmente all'esterno"
"it": "Questa videocamera si trova probabilmente all'esterno",
"ru": "Возможно, эта камера расположена снаружи"
},
"hideInAnswer": true
}
@ -374,7 +378,8 @@
"en": "How is this camera placed?",
"nl": "Hoe is deze camera geplaatst?",
"fr": "Comment cette caméra est-elle placée ?",
"it": "Com'è posizionata questa telecamera?"
"it": "Com'è posizionata questa telecamera?",
"ru": "Как расположена эта камера?"
},
"render": {
"en": "Mounting method: {mount}",

View file

@ -57,7 +57,8 @@
"de": "Eine öffentlich zugängliche Toilette",
"fr": "Des toilettes",
"nl": "Een publieke toilet",
"it": "Servizi igienici aperti al pubblico"
"it": "Servizi igienici aperti al pubblico",
"ru": "Туалет или комната отдыха со свободным доступом"
}
},
{
@ -66,7 +67,8 @@
"de": "Toiletten mit rollstuhlgerechter Toilette",
"fr": "Toilettes accessible aux personnes à mobilité réduite",
"nl": "Een rolstoeltoegankelijke toilet",
"it": "Servizi igienici accessibili per persone in sedia a rotelle"
"it": "Servizi igienici accessibili per persone in sedia a rotelle",
"ru": "Туалет с доступом для пользователей кресел-колясок"
},
"tags": [
"amenity=toilets",
@ -89,7 +91,8 @@
"de": "Sind diese Toiletten öffentlich zugänglich?",
"fr": "Ces toilettes sont-elles accessibles au public ?",
"nl": "Zijn deze toiletten publiek toegankelijk?",
"it": "Questi servizi igienici sono aperti al pubblico?"
"it": "Questi servizi igienici sono aperti al pubblico?",
"ru": "Есть ли свободный доступ к этим туалетам?"
},
"render": {
"en": "Access is {access}",
@ -112,7 +115,8 @@
"de": "Öffentlicher Zugang",
"fr": "Accès publique",
"nl": "Publiek toegankelijk",
"it": "Accesso pubblico"
"it": "Accesso pubblico",
"ru": "Свободный доступ"
}
},
{
@ -186,14 +190,16 @@
"de": "Wie viel muss man für diese Toiletten bezahlen?",
"fr": "Quel est le prix d'accès de ces toilettes ?",
"nl": "Hoeveel moet men betalen om deze toiletten te gebruiken?",
"it": "Quanto costa l'accesso a questi servizi igienici?"
"it": "Quanto costa l'accesso a questi servizi igienici?",
"ru": "Сколько стоит посещение туалета?"
},
"render": {
"en": "The fee is {charge}",
"de": "Die Gebühr beträgt {charge}",
"fr": "Le prix est {charge}",
"nl": "De toiletten gebruiken kost {charge}",
"it": "La tariffa è {charge}"
"it": "La tariffa è {charge}",
"ru": "Стоимость {charge}"
},
"condition": "fee=yes",
"freeform": {
@ -227,7 +233,8 @@
"de": "Kein Zugang für Rollstuhlfahrer",
"fr": "Non accessible aux personnes à mobilité réduite",
"nl": "Niet toegankelijk voor rolstoelgebruikers",
"it": "Non accessibile in sedia a rotelle"
"it": "Non accessibile in sedia a rotelle",
"ru": "Недоступно пользователям кресел-колясок"
}
}
]
@ -238,7 +245,8 @@
"de": "Welche Art von Toiletten sind das?",
"fr": "De quel type sont ces toilettes ?",
"nl": "Welke toiletten zijn dit?",
"it": "Di che tipo di servizi igienici si tratta?"
"it": "Di che tipo di servizi igienici si tratta?",
"ru": "Какие это туалеты?"
},
"mappings": [
{

View file

@ -230,7 +230,8 @@
"question": {
"nl": "Is deze boom groenblijvend of bladverliezend?",
"en": "Is this tree evergreen or deciduous?",
"it": "È un sempreverde o caduco?"
"it": "È un sempreverde o caduco?",
"ru": "Это дерево вечнозелёное или листопадное?"
},
"mappings": [
{
@ -242,7 +243,8 @@
"then": {
"nl": "Bladverliezend: de boom is een periode van het jaar kaal.",
"en": "Deciduous: the tree loses its leaves for some time of the year.",
"it": "Caduco: lalbero perde le sue foglie per un periodo dellanno."
"it": "Caduco: lalbero perde le sue foglie per un periodo dellanno.",
"ru": "Листопадное: у дерева опадают листья в определённое время года."
}
},
{
@ -255,7 +257,8 @@
"nl": "Groenblijvend.",
"en": "Evergreen.",
"it": "Sempreverde.",
"fr": "À feuilles persistantes."
"fr": "À feuilles persistantes.",
"ru": "Вечнозелёное."
}
}
],
@ -278,7 +281,8 @@
"nl": "Heeft de boom een naam?",
"en": "Does the tree have a name?",
"it": "Lalbero ha un nome?",
"fr": "L'arbre a-t-il un nom ?"
"fr": "L'arbre a-t-il un nom ?",
"ru": "Есть ли у этого дерева название?"
},
"freeform": {
"key": "name",
@ -298,7 +302,8 @@
"nl": "De boom heeft geen naam.",
"en": "The tree does not have a name.",
"it": "Lalbero non ha un nome.",
"fr": "L'arbre n'a pas de nom."
"fr": "L'arbre n'a pas de nom.",
"ru": "У этого дерева нет названия."
}
}
],
@ -399,7 +404,8 @@
"render": {
"nl": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed-ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>",
"en": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>",
"it": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>"
"it": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>",
"ru": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>"
},
"question": {
"nl": "Wat is het ID uitgegeven door Onroerend Erfgoed Vlaanderen?",
@ -421,7 +427,8 @@
"render": {
"nl": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>",
"en": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>",
"it": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>"
"it": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>",
"ru": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>"
},
"question": {
"nl": "Wat is het Wikidata-ID van deze boom?",
@ -484,7 +491,8 @@
"nl": "Loofboom",
"en": "Broadleaved tree",
"it": "Albero latifoglia",
"fr": "Arbre feuillu"
"fr": "Arbre feuillu",
"ru": "Лиственное дерево"
},
"description": {
"nl": "Een boom van een soort die blaadjes heeft, bijvoorbeeld eik of populier.",
@ -501,12 +509,14 @@
"title": {
"nl": "Naaldboom",
"en": "Needleleaved tree",
"it": "Albero aghifoglia"
"it": "Albero aghifoglia",
"ru": "Хвойное дерево"
},
"description": {
"nl": "Een boom van een soort met naalden, bijvoorbeeld den of spar.",
"en": "A tree of a species with needles, such as pine or spruce.",
"it": "Un albero di una specie con aghi come il pino o labete."
"it": "Un albero di una specie con aghi come il pino o labete.",
"ru": "Дерево с хвоей (иглами), например, сосна или ель."
}
},
{
@ -524,7 +534,8 @@
"nl": "Wanneer je niet zeker bent of het nu een loof- of naaldboom is.",
"en": "If you're not sure whether it's a broadleaved or needleleaved tree.",
"it": "Qualora non si sia sicuri se si tratta di un albero latifoglia o aghifoglia.",
"fr": "Si vous n'êtes pas sûr(e) de savoir s'il s'agit d'un arbre à feuilles larges ou à aiguilles."
"fr": "Si vous n'êtes pas sûr(e) de savoir s'il s'agit d'un arbre à feuilles larges ou à aiguilles.",
"ru": "Если вы не уверены в том, лиственное это дерево или хвойное."
}
}
]

View 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

View 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

View file

@ -172,6 +172,18 @@
"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"
@ -214,7 +226,7 @@
"license": "CC0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg",
" http://tango.freedesktop.org/Tango_Desktop_Project"
"http://tango.freedesktop.org/Tango_Desktop_Project"
]
},
{
@ -592,5 +604,15 @@
"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"
]
}
]

View file

@ -1,15 +1,15 @@
{
"wikipedialink": {
"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": [
{
"if": {
"and": [
"wikipedia=",
"wikidata~*"
]
},
"if": "wikipedia=",
"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>",
"mappings": [
{
"if": "id~=-",
"then": "<span class='alert'>Uploading...</alert>"
"if": "id~.*/-.*",
"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]*"

View file

@ -374,7 +374,7 @@
"en": "Is there a website with more information about this artwork?",
"nl": "Op welke website kan men meer informatie vinden over dit kunstwerk?",
"fr": "Sur quel site web pouvons-nous trouver plus d'informations sur cette œuvre d'art?",
"de": "Auf welcher Website gibt es mehr Informationen über dieses Kunstwerk?",
"de": "Gibt es eine Website mit weiteren Informationen über dieses Kunstwerk?",
"it": "Esiste un sito web con maggiori informazioni su questopera?",
"ru": "Есть ли сайт с более подробной информацией об этой работе?",
"ja": "この作品についての詳しい情報はどのウェブサイトにありますか?",

View file

@ -10,7 +10,8 @@
"ja",
"fr",
"zh_Hant",
"nb_NO"
"nb_NO",
"de"
],
"title": {
"en": "Bicycle libraries",
@ -20,7 +21,8 @@
"ja": "自転車ライブラリ",
"fr": "Vélothèques",
"zh_Hant": "單車圖書館",
"nb_NO": "Sykkelbibliotek"
"nb_NO": "Sykkelbibliotek",
"de": "Fahrradbibliothek"
},
"description": {
"nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.",

View file

@ -23,7 +23,8 @@
"it": "Questo sito raccoglie tutti i luoghi ufficiali dove sostare con il camper e aree dove è possibile scaricare acque grigie e nere. Puoi aggiungere dettagli riguardanti i servizi forniti e il loro costo. Aggiungi foto e recensioni. Questo è al contempo un sito web e una web app. I dati sono memorizzati su OpenStreetMap in modo tale che siano per sempre liberi e riutilizzabili da qualsiasi app.",
"ru": "На этом сайте собраны все официальные места остановки кемперов и места, где можно сбросить серую и черную воду. Вы можете добавить подробную информацию о предоставляемых услугах и их стоимости. Добавлять фотографии и отзывы. Это веб-сайт и веб-приложение. Данные хранятся в OpenStreetMap, поэтому они будут бесплатными всегда и могут быть повторно использованы любым приложением.",
"ja": "このWebサイトでは、すべてのキャンピングカーの公式停車場所と、汚水を捨てることができる場所を収集します。提供されるサービスとコストに関する詳細を追加できます。写真とレビューを追加します。これはウェブサイトとウェブアプリです。データはOpenStreetMapに保存されるので、永遠に無料で、どんなアプリからでも再利用できます。",
"zh_Hant": "這個網站收集所有官方露營地點,以及那邊能排放廢水。你可以加上詳細的服務項目與價格,加上圖片以及評價。這是網站與網路 app資料則是存在開放街圖因此會永遠免費而且可以被所有 app 再利用。"
"zh_Hant": "這個網站收集所有官方露營地點,以及那邊能排放廢水。你可以加上詳細的服務項目與價格,加上圖片以及評價。這是網站與網路 app資料則是存在開放街圖因此會永遠免費而且可以被所有 app 再利用。",
"nl": "Deze website verzamelt en toont alle officiële plaatsen waar een camper mag overnachten en afvalwater kan lozen. Ook jij kan extra gegevens toevoegen, zoals welke services er geboden worden en hoeveel dit kot, ook afbeeldingen en reviews kan je toevoegen. De data wordt op OpenStreetMap opgeslaan en is dus altijd gratis te hergebruiken, ook door andere applicaties."
},
"language": [
"en",
@ -53,7 +54,8 @@
"ru": "Площадки для кемпинга",
"ja": "キャンプサイト",
"fr": "Campings",
"zh_Hant": "露營地"
"zh_Hant": "露營地",
"nl": "Camperplaatsen"
},
"minzoom": 10,
"source": {
@ -71,7 +73,8 @@
"ru": "Место для кемпинга {name}",
"ja": "キャンプサイト {name}",
"fr": "Camping {name}",
"zh_Hant": "露營地 {name}"
"zh_Hant": "露營地 {name}",
"nl": "Camperplaats {name}"
},
"mappings": [
{
@ -86,7 +89,8 @@
"ru": "Место для кемпинга без названия",
"ja": "無名のキャンプサイト",
"fr": "Camping sans nom",
"zh_Hant": "沒有名稱的露營地"
"zh_Hant": "沒有名稱的露營地",
"nl": "Camper site"
}
}
]
@ -97,7 +101,8 @@
"ru": "площадки для кемпинга",
"ja": "キャンプサイト",
"fr": "campings",
"zh_Hant": "露營地"
"zh_Hant": "露營地",
"nl": "camperplaatsen"
},
"tagRenderings": [
"images",
@ -108,7 +113,8 @@
"ru": "Это место называется {name}",
"ja": "この場所は {name} と呼ばれています",
"fr": "Cet endroit s'appelle {nom}",
"zh_Hant": "這個地方叫做 {name}"
"zh_Hant": "這個地方叫做 {name}",
"nl": "Deze plaats heet {name}"
},
"question": {
"en": "What is this place called?",
@ -117,7 +123,8 @@
"it": "Come viene chiamato questo luogo?",
"ja": "ここは何というところですか?",
"fr": "Comment s'appelle cet endroit ?",
"zh_Hant": "這個地方叫做什麼?"
"zh_Hant": "這個地方叫做什麼?",
"nl": "Wat is de naam van deze plaats?"
},
"freeform": {
"key": "name"
@ -130,7 +137,8 @@
"ru": "Взимается ли в этом месте плата?",
"ja": "ここは有料ですか?",
"fr": "Cet endroit est-il payant ?",
"zh_Hant": "這個地方收費嗎?"
"zh_Hant": "這個地方收費嗎?",
"nl": "Moet men betalen om deze camperplaats te gebruiken?"
},
"mappings": [
{
@ -144,7 +152,8 @@
"it": "Devi pagare per usarlo",
"ru": "За использование нужно платить",
"ja": "使用料を支払う必要がある",
"zh_Hant": "你要付費才能使用"
"zh_Hant": "你要付費才能使用",
"nl": "Gebruik is betalend"
}
},
{
@ -162,7 +171,8 @@
"ja": "無料で使用可能",
"fr": "Peut être utilisé gratuitement",
"nb_NO": "Kan brukes gratis",
"zh_Hant": "可以免費使用"
"zh_Hant": "可以免費使用",
"nl": "Kan gratis gebruikt worden"
}
},
{
@ -179,7 +189,8 @@
"ru": "Это место взимает {charge}",
"ja": "この場所は{charge} が必要",
"nb_NO": "Dette stedet tar {charge}",
"zh_Hant": "這個地方收費 {charge}"
"zh_Hant": "這個地方收費 {charge}",
"nl": "Deze plaats vraagt {charge}"
},
"question": {
"en": "How much does this place charge?",
@ -188,7 +199,8 @@
"ja": "ここはいくらかかりますか?",
"fr": "Combien coûte cet endroit ?",
"nb_NO": "pø",
"zh_Hant": "這個地方收多少費用?"
"zh_Hant": "這個地方收多少費用?",
"nl": "Hoeveel kost deze plaats?"
},
"freeform": {
"key": "charge"
@ -774,7 +786,8 @@
"question": {
"en": "Who can use this dump station?",
"ja": "このゴミ捨て場は誰が使えるんですか?",
"it": "Chi può utilizzare questo luogo di sversamento?"
"it": "Chi può utilizzare questo luogo di sversamento?",
"ru": "Кто может использовать эту станцию утилизации?"
},
"mappings": [
{

View file

@ -6,7 +6,8 @@
"ru": "Зарядные станции",
"ja": "充電ステーション",
"zh_Hant": "充電站",
"it": "Stazioni di ricarica"
"it": "Stazioni di ricarica",
"nl": "Oplaadpunten"
},
"shortDescription": {
"en": "A worldwide map of charging stations",
@ -29,6 +30,7 @@
"ja",
"zh_Hant",
"it",
"nl",
"nb_NO"
],
"maintainer": "",
@ -48,7 +50,8 @@
"ja": "充電ステーション",
"zh_Hant": "充電站",
"nb_NO": "Ladestasjoner",
"it": "Stazioni di ricarica"
"it": "Stazioni di ricarica",
"nl": "Oplaadpunten"
},
"minzoom": 10,
"source": {
@ -65,7 +68,8 @@
"ja": "充電ステーション",
"zh_Hant": "充電站",
"nb_NO": "Ladestasjon",
"it": "Stazione di ricarica"
"it": "Stazione di ricarica",
"nl": "Oplaadpunt"
}
},
"description": {
@ -74,7 +78,8 @@
"ja": "充電ステーション",
"zh_Hant": "充電站",
"nb_NO": "En ladestasjon",
"it": "Una stazione di ricarica"
"it": "Una stazione di ricarica",
"nl": "Een oplaadpunt"
},
"tagRenderings": [
"images",

View file

@ -547,7 +547,12 @@
}
},
{
"if": "climbing=site",
"if": {
"or": [
"climbing=site",
"climbing=area"
]
},
"then": {
"en": "Climbing site",
"nl": "Klimsite"
@ -727,7 +732,7 @@
"_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'])",
"_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"
]
},
{
@ -1400,8 +1405,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_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_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock",
"_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id",
"_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock",
"_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:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"

View file

@ -0,0 +1,12 @@
[
{
"authors": [
"Iconathon"
],
"path": "wind_turbine.svg",
"license": "CC0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Wind_Turbine_(2076)_-_The_Noun_Project.svg"
]
}
]

View file

@ -0,0 +1,204 @@
{
"id": "openwindpowermap",
"title": {
"en": "OpenWindPowerMap"
},
"maintainer": "Seppe Santens",
"icon": "./assets/themes/openwindpowermap/wind_turbine.svg",
"description": {
"en": "A map for showing and editing wind turbines."
},
"language": [
"en",
"nl"
],
"version": "2021-06-18",
"startLat": 50.52,
"startLon": 4.643,
"startZoom": 8,
"clustering": {
"maxZoom": 8
},
"layers": [
{
"id": "windturbine",
"name": {
"en": "wind turbine"
},
"source": {
"osmTags": "generator:source=wind"
},
"minzoom": 10,
"wayHandling": 1,
"title": {
"render": {
"en": "wind turbine"
},
"mappings": [
{
"if": "name~*",
"then": {
"en": "{name}"
}
}
]
},
"icon": "./assets/themes/openwindpowermap/wind_turbine.svg",
"iconSize": "40, 40, bottom",
"label": {
"mappings": [
{
"if": "generator:output:electricity~^[0-9]+.*[W]$",
"then": "<div style='background-color: rgba(0,0,0,0.3); color: white; font-size: 8px; padding: 0.25em; border-radius:0.5em'>{generator:output:electricity}</div>"
}
]
},
"tagRenderings": [
{
"render": {
"en": "The power output of this wind turbine is {generator:output:electricity}."
},
"question": {
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
},
"freeform": {
"key": "generator:output:electricity",
"type": "pfloat"
}
},
{
"render": {
"en": "This wind turbine is operated by {operator}."
},
"question": {
"en": "Who operates this wind turbine?"
},
"freeform": {
"key": "operator"
}
},
{
"render": {
"en": "The total height (including rotor radius) of this wind turbine is {height} metres."
},
"question": {
"en": "What is the total height of this wind turbine (including rotor radius), in metres?"
},
"freeform": {
"key": "height",
"type": "pfloat"
}
},
{
"render": {
"en": "The rotor diameter of this wind turbine is {rotor:diameter} metres."
},
"question": {
"en": "What is the rotor diameter of this wind turbine, in metres?"
},
"freeform": {
"key": "rotor:diameter",
"type": "float"
}
},
{
"render": {
"en": "This wind turbine went into operation on/in {start_date}."
},
"question": {
"en": "When did this wind turbine go into operation?"
},
"freeform": {
"key": "start_date",
"type": "date"
}
},
"images"
],
"presets": [
{
"tags": [
"power=generator",
"generator:source=wind"
],
"title": {
"en": "wind turbine"
}
}
]
}
],
"units": [
{
"appliesToKey": [
"generator:output:electricity"
],
"applicableUnits": [
{
"canonicalDenomination": "MW",
"alternativeDenomination": [
"megawatts",
"megawatt"
],
"human": {
"en": " megawatts",
"nl": " megawatt"
}
},
{
"canonicalDenomination": "kW",
"alternativeDenomination": [
"kilowatts",
"kilowatt"
],
"human": {
"en": " kilowatts",
"nl": " kilowatt"
}
},
{
"canonicalDenomination": "W",
"alternativeDenomination": [
"watts",
"watt"
],
"human": {
"en": " watts",
"nl": " watt"
}
},
{
"canonicalDenomination": "GW",
"alternativeDenomination": [
"gigawatts",
"gigawatt"
],
"human": {
"en": " gigawatts",
"nl": " gigawatt"
}
}
],
"eraseInvalidValues": true
},
{
"appliesToKey": [
"height",
"rotor:diameter"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": " meter",
"nl": " meter"
}
}
]
}
],
"defaultBackgroundId": "CartoDB.Voyager"
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 58.391 100" enable-background="new 0 0 58.391 100" xml:space="preserve"><circle cx="38.721" cy="33.621" r="4.148"></circle><path d="M45.875,29.436C57.268,8.877,59.217,0.677,58.122,0.045c-1.091-0.631-7.217,5.15-19.313,25.282
C41.822,25.359,44.447,27.003,45.875,29.436z"></path><path d="M30.424,33.621c0-1.486,0.397-2.879,1.083-4.087C8.059,29.949,0,32.359,0,33.621c0,1.261,8.058,3.672,31.505,4.086
C30.821,36.5,30.424,35.108,30.424,33.621z"></path><path d="M45.884,37.794c-1.424,2.436-4.046,4.083-7.061,4.119C50.911,62.021,57.03,67.798,58.124,67.167
C59.217,66.535,57.271,58.34,45.884,37.794z"></path><path d="M34.573,42.889v1.795V100h8.297V56.07C40.49,52.552,37.745,48.22,34.573,42.889z"></path></svg>

After

Width:  |  Height:  |  Size: 965 B

View file

@ -23,6 +23,7 @@
"ja",
"zh_Hant",
"ru",
"nl",
"ca",
"id"
],
@ -41,7 +42,8 @@
"en": "Shop",
"fr": "Magasin",
"ru": "Магазин",
"ja": "店"
"ja": "店",
"nl": "Winkel"
},
"minzoom": 16,
"source": {
@ -56,7 +58,8 @@
"en": "Shop",
"fr": "Magasin",
"ru": "Магазин",
"ja": "店"
"ja": "店",
"nl": "Winkel"
},
"mappings": [
{
@ -90,7 +93,8 @@
"description": {
"en": "A shop",
"fr": "Un magasin",
"ja": "ショップ"
"ja": "ショップ",
"nl": "Een winkel"
},
"tagRenderings": [
"images",
@ -99,7 +103,8 @@
"en": "What is the name of this shop?",
"fr": "Qu'est-ce que le nom de ce magasin?",
"ru": "Как называется магазин?",
"ja": "このお店の名前は何ですか?"
"ja": "このお店の名前は何ですか?",
"nl": "Wat is de naam van deze winkel?"
},
"render": "This shop is called <i>{name}</i>",
"freeform": {
@ -143,7 +148,8 @@
"en": "Supermarket",
"fr": "Supermarché",
"ru": "Супермаркет",
"ja": "スーパーマーケット"
"ja": "スーパーマーケット",
"nl": "Supermarkt"
}
},
{
@ -169,7 +175,8 @@
"en": "Hairdresser",
"fr": "Coiffeur",
"ru": "Парикмахерская",
"ja": "理容師"
"ja": "理容師",
"nl": "Kapper"
}
},
{
@ -181,7 +188,8 @@
"then": {
"en": "Bakery",
"fr": "Boulangerie",
"ja": "ベーカリー"
"ja": "ベーカリー",
"nl": "Bakkerij"
}
},
{
@ -223,7 +231,8 @@
"question": {
"en": "What is the phone number?",
"fr": "Quel est le numéro de téléphone ?",
"ja": "電話番号は何番ですか?"
"ja": "電話番号は何番ですか?",
"nl": "Wat is het telefoonnummer?"
},
"freeform": {
"key": "phone",
@ -242,7 +251,8 @@
"question": {
"en": "What is the website of this shop?",
"fr": "Quel est le site internet de ce magasin ?",
"ja": "このお店のホームページは何ですか?"
"ja": "このお店のホームページは何ですか?",
"nl": "Wat is de website van deze winkel?"
},
"freeform": {
"key": "website",
@ -277,7 +287,8 @@
"question": {
"en": "What are the opening hours of this shop?",
"fr": "Quels sont les horaires d'ouverture de ce magasin ?",
"ja": "この店の営業時間は何時から何時までですか?"
"ja": "この店の営業時間は何時から何時までですか?",
"nl": "Wat zijn de openingsuren van deze winkel?"
},
"freeform": {
"key": "opening_hours",
@ -316,13 +327,15 @@
"en": "Shop",
"fr": "Magasin",
"ru": "Магазин",
"ja": "店"
"ja": "店",
"nl": "Winkel"
},
"description": {
"en": "Add a new shop",
"fr": "Ajouter un nouveau magasin",
"ru": "Добавить новый магазин",
"ja": "新しい店を追加する"
"ja": "新しい店を追加する",
"nl": "Voeg een nieuwe winkel toe"
}
}
],

View file

@ -82,6 +82,10 @@ html, body {
box-sizing: initial !important;
}
.leaflet-control-attribution {
display: block ruby;
}
svg, img {
box-sizing: content-box;
width: 100%;
@ -101,6 +105,10 @@ a {
width: min-content;
}
.w-16-imp {
width: 4rem !important;
}
.space-between{
justify-content: space-between;
}

View file

@ -149,6 +149,10 @@
"zoomInToSeeThisLayer": "Zoom in to see this layer",
"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": {
"abbreviations": {
"monday": "Mon",

1
langs/eo.json Normal file
View file

@ -0,0 +1 @@
{}

43
langs/fi.json Normal file
View file

@ -0,0 +1,43 @@
{
"general": {
"opening_hours": {
"ph_open": "avattu",
"ph_closed": "suljettu",
"ph_not_known": " "
},
"weekdays": {
"sunday": "Sunnuntai",
"saturday": "Lauantai",
"friday": "Perjantai",
"thursday": "Torstai",
"wednesday": "Keskiviikko",
"tuesday": "Tiistai",
"monday": "Maanantai",
"abbreviations": {
"sunday": "Su",
"saturday": "La",
"friday": "Pe",
"thursday": "To",
"wednesday": "Ke",
"tuesday": "Ti",
"monday": "Ma"
}
},
"backgroundMap": "Taustakartta",
"pickLanguage": "Valitse kieli: ",
"number": "numero",
"cancel": "Peruuta",
"save": "Tallenna",
"search": {
"searching": "Etsitään…"
}
},
"centerMessage": {
"ready": "Valmis!"
},
"image": {
"doDelete": "Poista kuva",
"dontDelete": "Peruuta",
"addPicture": "Lisää kuva"
}
}

1
langs/layers/eo.json Normal file
View file

@ -0,0 +1 @@
{}

103
langs/layers/fi.json Normal file
View file

@ -0,0 +1,103 @@
{
"bench": {
"name": "Penkit",
"title": {
"render": "Penkki"
},
"tagRenderings": {
"1": {
"render": "Selkänoja",
"mappings": {
"0": {
"then": "Selkänoja: kyllä"
},
"1": {
"then": "Selkänoja: ei"
}
}
},
"3": {
"render": "Materiaali: {material}",
"mappings": {
"0": {
"then": "Materiaali: puu"
},
"2": {
"then": "Materiaali: kivi"
},
"3": {
"then": "Materiaali: betoni"
},
"4": {
"then": "Materiaali: muovi"
},
"5": {
"then": "Materiaali: teräs"
}
}
},
"5": {
"render": "Väri: {colour}",
"mappings": {
"0": {
"then": "Väri: ruskea"
},
"1": {
"then": "Väri: vihreä"
},
"2": {
"then": "Väri: harmaa"
},
"3": {
"then": "Väri: valkoinen"
},
"4": {
"then": "Väri: punainen"
},
"5": {
"then": "Väri: musta"
},
"6": {
"then": "Väri: sininen"
},
"7": {
"then": "Väri: keltainen"
}
}
}
},
"presets": {
"0": {
"title": "Penkki",
"description": "Lisää uusi penkki"
}
}
},
"bench_at_pt": {
"title": {
"render": "Penkki"
},
"tagRenderings": {
"1": {
"render": "{name}"
}
}
},
"bike_parking": {
"tagRenderings": {
"5": {
"render": "{access}"
}
}
},
"bike_repair_station": {
"icon": {
"render": "./assets/layers/bike_repair_station/repair_station.svg"
},
"presets": {
"0": {
"title": "Pyöräpumppu"
}
}
}
}

View file

@ -130,6 +130,9 @@
"mappings": {
"0": {
"then": "Прокат велосипедов бесплатен"
},
"1": {
"then": "Прокат велосипеда стоит €20/год и €20 залог"
}
}
},
@ -199,7 +202,37 @@
"render": "Это велосипедное кафе называется {name}"
},
"2": {
"question": "Есть ли в этом велосипедном кафе велосипедный насос для всеобщего использования?"
"question": "Есть ли в этом велосипедном кафе велосипедный насос для всеобщего использования?",
"mappings": {
"0": {
"then": "В этом велосипедном кафе есть велосипедный насос для всеобщего использования"
},
"1": {
"then": "В этом велосипедном кафе нет велосипедного насоса для всеобщего использования"
}
}
},
"3": {
"question": "Есть ли здесь инструменты для починки вашего велосипеда?",
"mappings": {
"0": {
"then": "В этом велосипедном кафе есть инструменты для починки своего велосипеда"
},
"1": {
"then": "В этом велосипедном кафе нет инструментов для починки своего велосипеда"
}
}
},
"4": {
"question": "Есть ли услуги ремонта велосипедов в этом велосипедном кафе?",
"mappings": {
"0": {
"then": "В этом велосипедном кафе есть услуги ремонта велосипедов"
},
"1": {
"then": "В этом велосипедном кафе нет услуг ремонта велосипедов"
}
}
},
"5": {
"question": "Какой сайт у {name}?"
@ -209,6 +242,9 @@
},
"7": {
"question": "Какой адрес электронной почты у {name}?"
},
"8": {
"question": "Каков режим работы этого велосипедного кафе?"
}
},
"presets": {
@ -217,10 +253,14 @@
}
}
},
"bike_monitoring_station": {
"name": "Станции мониторинга"
},
"bike_parking": {
"tagRenderings": {
"1": {
"question": "К какому типу относится эта велопарковка?"
"question": "К какому типу относится эта велопарковка?",
"render": "Это велопарковка типа {bicycle_parking}"
},
"2": {
"mappings": {
@ -235,7 +275,21 @@
}
}
},
"3": {
"mappings": {
"0": {
"then": "Это крытая парковка (есть крыша/навес)"
},
"1": {
"then": "Это открытая парковка"
}
}
},
"4": {
"render": "Место для {capacity} велосипеда(ов)"
},
"5": {
"question": "Кто может пользоваться этой велопарковкой?",
"render": "{access}"
}
}
@ -252,6 +306,14 @@
}
},
"tagRenderings": {
"3": {
"question": "Когда работает эта точка обслуживания велосипедов?",
"mappings": {
"0": {
"then": "Всегда открыто"
}
}
},
"6": {
"question": "Велосипедный насос все еще работает?",
"mappings": {
@ -309,7 +371,9 @@
}
},
"bike_shop": {
"name": "Обслуживание велосипедов/магазин",
"title": {
"render": "Обслуживание велосипедов/магазин",
"mappings": {
"0": {
"then": "Магазин спортивного инвентаря <i>{name}</i>"
@ -328,7 +392,8 @@
"description": "Магазин, специализирующийся на продаже велосипедов или сопутствующих товаров",
"tagRenderings": {
"2": {
"question": "Как называется магазин велосипедов?"
"question": "Как называется магазин велосипедов?",
"render": "Этот магазин велосипедов называется {name}"
},
"3": {
"question": "Какой сайт у {name}?"
@ -340,6 +405,7 @@
"question": "Какой адрес электронной почты у {name}?"
},
"9": {
"question": "Продаются ли велосипеды в этом магазине?",
"mappings": {
"0": {
"then": "В этом магазине продаются велосипеды"
@ -360,6 +426,9 @@
},
"2": {
"then": "Этот магазин ремонтирует только велосипеды, купленные здесь"
},
"3": {
"then": "В этом магазине обслуживают велосипеды определённого бренда"
}
}
},
@ -388,8 +457,35 @@
}
}
},
"13": {
"question": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?",
"mappings": {
"0": {
"then": "В этом магазине есть велосипедный насос для всеобщего пользования"
},
"1": {
"then": "В этом магазине нет велосипедного насоса для всеобщего пользования"
}
}
},
"14": {
"question": "Есть ли здесь инструменты для починки собственного велосипеда?",
"mappings": {
"2": {
"then": "Инструменты для починки доступны только при покупке/аренде велосипеда в магазине"
}
}
},
"15": {
"question": "Здесь моют велосипеды?"
"question": "Здесь моют велосипеды?",
"mappings": {
"0": {
"then": "В этом магазине оказываются услуги мойки/чистки велосипедов"
},
"2": {
"then": "В этом магазине нет услуг мойки/чистки велосипедов"
}
}
}
}
},
@ -424,6 +520,9 @@
"then": "Проверено сегодня!"
}
}
},
"15": {
"render": "Дополнительная информация для экспертов OpenStreetMap: {fixme}"
}
}
},
@ -443,11 +542,17 @@
},
"ghost_bike": {
"tagRenderings": {
"2": {
"render": "В знак памяти о {name}"
},
"3": {
"render": "<a href='{source}' target='_blank'>Доступна более подробная информация</a>"
},
"4": {
"render": "<i>{inscription}</i>"
},
"5": {
"render": "Установлен {start_date}"
}
}
},
@ -498,8 +603,11 @@
"title": {
"render": "Стол для пикника"
},
"description": "Слой, отображающий столы для пикника",
"tagRenderings": {
"0": {
"question": "Из чего изготовлен этот стол для пикника?",
"render": "Этот стол для пикника сделан из {material}",
"mappings": {
"0": {
"then": "Это деревянный стол для пикника"
@ -537,6 +645,9 @@
"1": {
"then": "Поверхность - <b>песок</b>"
},
"2": {
"then": "Покрытие из <b>щепы</b>"
},
"3": {
"then": "Поверхность - <b>брусчатка</b>"
},
@ -548,8 +659,23 @@
}
}
},
"2": {
"question": "Эта игровая площадка освещается ночью?",
"mappings": {
"0": {
"then": "Эта детская площадка освещается ночью"
},
"1": {
"then": "Эта детская площадка не освещается ночью"
}
}
},
"3": {
"render": "Доступно для детей старше {min_age} лет"
"render": "Доступно для детей старше {min_age} лет",
"question": "С какого возраста доступна эта детская площадка?"
},
"4": {
"render": "Доступно детям до {max_age}"
},
"6": {
"mappings": {
@ -564,8 +690,26 @@
"8": {
"render": "<a href='tel:{phone}'>{phone}</a>"
},
"10": {
"9": {
"question": "Доступна ли детская площадка пользователям кресел-колясок?",
"mappings": {
"0": {
"then": "Полностью доступна пользователям кресел-колясок"
},
"1": {
"then": "Частично доступна пользователям кресел-колясок"
},
"2": {
"then": "Недоступна пользователям кресел-колясок"
}
}
},
"10": {
"question": "Когда открыта эта игровая площадка?",
"mappings": {
"0": {
"then": "Открыто от рассвета до заката"
},
"1": {
"then": "Всегда доступен"
},
@ -583,6 +727,7 @@
},
"public_bookcase": {
"name": "Книжные шкафы",
"description": "Уличный шкаф с книгами, доступными для всех",
"title": {
"render": "Книжный шкаф",
"mappings": {
@ -599,7 +744,7 @@
"tagRenderings": {
"2": {
"render": "Название книжного шкафа — {name}",
"question": "Как называется общественный книжный шкаф?",
"question": "Как называется этот общественный книжный шкаф?",
"mappings": {
"0": {
"then": "У этого книжного шкафа нет названия"
@ -607,20 +752,38 @@
}
},
"3": {
"render": "{capacity} книг помещается в этот книжный шкаф",
"question": "Сколько книг помещается в этом общественном книжном шкафу?"
},
"4": {
"question": "Какие книги можно найти в этом общественном книжном шкафу?",
"mappings": {
"0": {
"then": "В основном детские книги"
},
"1": {
"then": "В основном книги для взрослых"
},
"2": {
"then": "Книги и для детей, и для взрослых"
}
}
},
"6": {
"question": "Имеется ли свободный доступ к этому общественному книжному шкафу?",
"mappings": {
"0": {
"then": "Свободный доступ"
}
}
},
"10": {
"question": "Когда был установлен этот общественный книжный шкаф?",
"render": "Установлен {start_date}"
},
"11": {
"render": "Более подробная информация <a href='{website}' target='_blank'>на сайте</a>"
"render": "Более подробная информация <a href='{website}' target='_blank'>на сайте</a>",
"question": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?"
}
}
},
@ -656,15 +819,32 @@
"title": {
"render": "Спортивная площадка"
},
"description": "Спортивная площадка",
"tagRenderings": {
"1": {
"mappings": {
"0": {
"then": "Здесь можно играть в баскетбол"
},
"1": {
"then": "Здесь можно играть в футбол"
},
"2": {
"then": "Это стол для пинг-понга"
},
"3": {
"then": "Здесь можно играть в теннис"
},
"4": {
"then": "Здесь можно играть в корфбол"
},
"5": {
"then": "Здесь можно играть в баскетбол"
}
}
},
"2": {
"question": "Какое покрытие на этой спортивной площадке?",
"render": "Поверхность - <b>{surface}</b>",
"mappings": {
"0": {
@ -684,7 +864,36 @@
}
}
},
"3": {
"question": "Есть ли свободный доступ к этой спортивной площадке?",
"mappings": {
"0": {
"then": "Свободный доступ"
},
"1": {
"then": "Ограниченный доступ (напр., только по записи, в определённые часы, ...)"
},
"2": {
"then": "Доступ только членам клуба"
}
}
},
"4": {
"question": "Нужна ли предварительная запись для доступа на эту спортивную площадку?",
"mappings": {
"1": {
"then": "Желательна предварительная запись для доступа на эту спортивную площадку"
},
"2": {
"then": "Предварительная запись для доступа на эту спортивную площадку возможна, но не обязательна"
},
"3": {
"then": "Невозможна предварительная запись"
}
}
},
"7": {
"question": "В какое время доступна эта площадка?",
"mappings": {
"1": {
"then": "Всегда доступен"
@ -693,6 +902,9 @@
}
},
"presets": {
"0": {
"title": "Стол для настольного тенниса"
},
"1": {
"title": "Спортивная площадка"
}
@ -705,11 +917,28 @@
},
"tagRenderings": {
"1": {
"question": "Какая это камера?",
"mappings": {
"1": {
"then": "Камера с поворотным механизмом"
},
"2": {
"then": "Панорамная камера"
}
}
},
"5": {
"mappings": {
"1": {
"then": "Эта камера расположена снаружи"
},
"2": {
"then": "Возможно, эта камера расположена снаружи"
}
}
},
"8": {
"question": "Как расположена эта камера?"
}
}
},
@ -720,12 +949,20 @@
},
"presets": {
"0": {
"title": "Туалет"
"title": "Туалет",
"description": "Туалет или комната отдыха со свободным доступом"
},
"1": {
"title": "Туалет с доступом для пользователей кресел-колясок"
}
},
"tagRenderings": {
"1": {
"question": "Есть ли свободный доступ к этим туалетам?",
"mappings": {
"0": {
"then": "Свободный доступ"
},
"2": {
"then": "Недоступно"
}
@ -737,6 +974,20 @@
"then": "Это платные туалеты"
}
}
},
"3": {
"question": "Сколько стоит посещение туалета?",
"render": "Стоимость {charge}"
},
"4": {
"mappings": {
"1": {
"then": "Недоступно пользователям кресел-колясок"
}
}
},
"5": {
"question": "Какие это туалеты?"
}
}
},
@ -759,13 +1010,44 @@
}
}
},
"4": {
"question": "Это дерево вечнозелёное или листопадное?",
"mappings": {
"0": {
"then": "Листопадное: у дерева опадают листья в определённое время года."
},
"1": {
"then": "Вечнозелёное."
}
}
},
"5": {
"render": "Название: {name}"
"render": "Название: {name}",
"question": "Есть ли у этого дерева название?",
"mappings": {
"0": {
"then": "У этого дерева нет названия."
}
}
},
"7": {
"render": "<img src=\"./assets/layers/tree_node/Onroerend_Erfgoed_logo_without_text.svg\" style=\"width:0.85em;height:1em;vertical-align:middle\" alt=\"\"/> Onroerend Erfgoed ID: <a href=\"https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}\">{ref:OnroerendErfgoed}</a>"
},
"8": {
"render": "<img src=\"./assets/svg/wikidata.svg\" style=\"width:1em;height:0.56em;vertical-align:middle\" alt=\"\"/> Wikidata: <a href=\"http://www.wikidata.org/entity/{wikidata}\">{wikidata}</a>"
}
},
"presets": {
"0": {
"title": "Лиственное дерево"
},
"1": {
"title": "Хвойное дерево",
"description": "Дерево с хвоей (иглами), например, сосна или ель."
},
"2": {
"title": "Дерево"
"title": "Дерево",
"description": "Если вы не уверены в том, лиственное это дерево или хвойное."
}
}
},

View file

@ -98,7 +98,7 @@
"fsGeolocation": "Включить кнопку \"найди меня\" (только в мобильной версии)",
"fsSearch": "Включить строку поиска",
"fsUserbadge": "Включить кнопку входа в систему",
"fsWelcomeMessage": "Показать всплывающее окно с приветствием и соответсвующие вкладки",
"fsWelcomeMessage": "Показать всплывающее окно с приветствием и соответствующие вкладки",
"fsLayers": "Включить выбор слоя карты",
"fsAddNew": "Включить кнопку \"добавить новую точку интереса\"",
"fsLayerControlToggle": "Открыть панель выбора слоя",
@ -106,7 +106,7 @@
"editThisTheme": "Редактировать эту тему",
"thanksForSharing": "Спасибо, что поделились!",
"copiedToClipboard": "Ссылка скопирована в буфер обмена",
"embedIntro": "<h3>Встроить на свой сайт</h3>Пожалуйста, вставьте эту карту на свой сайт.<br>Мы призываем вас сделать это - вам даже не нужно спрашивать разрешения.<br>Она бесплатна и всегда будет бесплатной. Чем больше людей пользуются ею, тем более ценной она становится.",
"embedIntro": "<h3>Встроить на свой сайт</h3>Пожалуйста, вставьте эту карту на свой сайт.<br>Мы призываем вас сделать это - вам даже не нужно спрашивать разрешения.<br>Карта бесплатна и всегда будет бесплатной. Чем больше людей пользуются ею, тем более ценной она становится.",
"addToHomeScreen": "<h3>Добавить на домашний экран</h3>Вы можете легко добавить этот сайт на домашний экран вашего смартфона. Для этого нажмите кнопку \"Добавить на главный экран\" в строке URL.",
"intro": "<h3>Поделиться этой картой</h3> Поделитесь этой картой, скопировав ссылку ниже и отправив её друзьям и близким:"
},
@ -140,12 +140,12 @@
"doDelete": "Удалить изображение",
"dontDelete": "Отмена",
"uploadDone": "<span class=\"thanks\">Ваше изображение добавлено. Спасибо за помощь!</span>",
"respectPrivacy": "Не фотографируйте людей и номерные знаки. Не загружайте снимки Google Maps, Google Streetview и иные источники с закрытой лицензией.",
"respectPrivacy": "Не фотографируйте людей и номерные знаки. Не загружайте снимки Google Maps, Google Street View и иные источники с закрытой лицензией.",
"uploadFailed": "Не удалось загрузить изображение. Проверьте, есть ли у вас доступ в Интернет и разрешены ли сторонние API? Браузеры Brave и UMatrix могут блокировать их.",
"ccb": "под лицензией CC-BY",
"ccbs": "под лицензией CC-BY-SA",
"cco": "в открытом доступе",
"willBePublished": "Ваше изображение будет опубликоавано: ",
"willBePublished": "Ваше изображение будет опубликовано: ",
"pleaseLogin": "Пожалуйста, войдите в систему, чтобы добавить изображение",
"uploadingMultiple": "Загружаем {count} изображений…",
"uploadingPicture": "Загружаем изображение…",

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -71,7 +71,7 @@
"render": "Erstellt von {artist_name}"
},
"3": {
"question": "Auf welcher Website gibt es mehr Informationen über dieses Kunstwerk?",
"question": "Gibt es eine Website mit weiteren Informationen über dieses Kunstwerk?",
"render": "Weitere Informationen auf <a href='{website}' target='_blank'>dieser Webseite</a>"
},
"4": {
@ -87,6 +87,9 @@
"shortDescription": "Eine Karte aller Sitzbänke",
"description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Einzeln stehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Account können Sie neue Bänke eintragen oder Detailinformationen existierender Bänke bearbeiten."
},
"bicyclelib": {
"title": "Fahrradbibliothek"
},
"bookcases": {
"title": "Öffentliche Bücherschränke Karte",
"description": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen."

View file

@ -1084,6 +1084,75 @@
"shortDescription": "This theme shows all (touristic) maps that OpenStreetMap knows of",
"description": "On this map you can find all maps OpenStreetMap knows - typically a big map on an information board showing the area, city or region, e.g. a tourist map on the back of a billboard, a map of a nature reserve, a map of cycling networks in the region, ...) <br/><br/>If a map is missing, you can easily map this map on OpenStreetMap."
},
"openwindpowermap": {
"title": "OpenWindPowerMap",
"description": "A map for showing and editing wind turbines.",
"layers": {
"0": {
"name": "wind turbine",
"title": {
"render": "wind turbine",
"mappings": {
"0": {
"then": "{name}"
}
}
},
"tagRenderings": {
"0": {
"render": "The power output of this wind turbine is {generator:output:electricity}.",
"question": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
},
"1": {
"render": "This wind turbine is operated by {operator}.",
"question": "Who operates this wind turbine?"
},
"2": {
"render": "The total height (including rotor radius) of this wind turbine is {height} metres.",
"question": "What is the total height of this wind turbine (including rotor radius), in metres?"
},
"3": {
"render": "The rotor diameter of this wind turbine is {rotor:diameter} metres.",
"question": "What is the rotor diameter of this wind turbine, in metres?"
},
"4": {
"render": "This wind turbine went into operation on/in {start_date}.",
"question": "When did this wind turbine go into operation?"
}
},
"presets": {
"0": {
"title": "wind turbine"
}
}
}
},
"units": {
"0": {
"applicableUnits": {
"0": {
"human": " megawatts"
},
"1": {
"human": " kilowatts"
},
"2": {
"human": " watts"
},
"3": {
"human": " gigawatts"
}
}
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
}
}
},
"personal": {
"title": "Personal theme",
"description": "Create a personal theme based on all the available layers of all themes"

1
langs/themes/eo.json Normal file
View file

@ -0,0 +1 @@
{}

1
langs/themes/fi.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -240,7 +240,55 @@
},
"campersite": {
"title": "Kampeersite",
"shortDescription": "Vind locaties waar je de nacht kan doorbrengen met je mobilehome"
"shortDescription": "Vind locaties waar je de nacht kan doorbrengen met je mobilehome",
"description": "Deze website verzamelt en toont alle officiële plaatsen waar een camper mag overnachten en afvalwater kan lozen. Ook jij kan extra gegevens toevoegen, zoals welke services er geboden worden en hoeveel dit kot, ook afbeeldingen en reviews kan je toevoegen. De data wordt op OpenStreetMap opgeslaan en is dus altijd gratis te hergebruiken, ook door andere applicaties.",
"layers": {
"0": {
"name": "Camperplaatsen",
"title": {
"render": "Camperplaats {name}",
"mappings": {
"0": {
"then": "Camper site"
}
}
},
"description": "camperplaatsen",
"tagRenderings": {
"1": {
"render": "Deze plaats heet {name}",
"question": "Wat is de naam van deze plaats?"
},
"2": {
"question": "Moet men betalen om deze camperplaats te gebruiken?",
"mappings": {
"0": {
"then": "Gebruik is betalend"
},
"1": {
"then": "Kan gratis gebruikt worden"
}
}
},
"3": {
"render": "Deze plaats vraagt {charge}",
"question": "Hoeveel kost deze plaats?"
}
}
}
}
},
"charging_stations": {
"title": "Oplaadpunten",
"layers": {
"0": {
"name": "Oplaadpunten",
"title": {
"render": "Oplaadpunt"
},
"description": "Een oplaadpunt"
}
}
},
"climbing": {
"title": "Open Klimkaart",
@ -854,6 +902,33 @@
"shortDescription": "Deze kaart bevat informatie voor natuurliefhebbers",
"description": "Op deze kaart vind je informatie voor natuurliefhebbers, zoals info over het natuurgebied waar je inzit, vogelkijkhutten, informatieborden, ..."
},
"openwindpowermap": {
"units": {
"0": {
"applicableUnits": {
"0": {
"human": " megawatt"
},
"1": {
"human": " kilowatt"
},
"2": {
"human": " watt"
},
"3": {
"human": " gigawatt"
}
}
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
}
}
},
"personal": {
"title": "Persoonlijk thema",
"description": "Stel je eigen thema samen door lagen te combineren van alle andere themas"
@ -868,6 +943,50 @@
"shortDescription": "Een kaart met speeltuinen",
"description": "Op deze kaart vind je speeltuinen en kan je zelf meer informatie en foto's toevoegen"
},
"shops": {
"layers": {
"0": {
"name": "Winkel",
"title": {
"render": "Winkel"
},
"description": "Een winkel",
"tagRenderings": {
"1": {
"question": "Wat is de naam van deze winkel?"
},
"2": {
"mappings": {
"1": {
"then": "Supermarkt"
},
"3": {
"then": "Kapper"
},
"4": {
"then": "Bakkerij"
}
}
},
"3": {
"question": "Wat is het telefoonnummer?"
},
"4": {
"question": "Wat is de website van deze winkel?"
},
"6": {
"question": "Wat zijn de openingsuren van deze winkel?"
}
},
"presets": {
"0": {
"title": "Winkel",
"description": "Voeg een nieuwe winkel toe"
}
}
}
}
},
"speelplekken": {
"title": "Welkom bij de groendoener!",
"shortDescription": "Speelplekken in de Antwerpse Zuidrand",

View file

@ -276,6 +276,9 @@
"then": "Здесь нельзя утилизировать отходы химических туалетов"
}
}
},
"6": {
"question": "Кто может использовать эту станцию утилизации?"
}
}
}

View file

@ -8,7 +8,7 @@
"main": "index.js",
"scripts": {
"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",
"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",

View file

@ -1,7 +1,7 @@
/**
* 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
import {Overpass} from "../Logic/Osm/Overpass";
@ -17,6 +17,7 @@ import MetaTagging from "../Logic/MetaTagging";
import LayerConfig from "../Customizations/JSON/LayerConfig";
import {GeoOperations} from "../Logic/GeoOperations";
import {UIEventSource} from "../Logic/UIEventSource";
import {TileRange} from "../Models/TileRange";
function createOverpassObject(theme: LayoutConfig) {

View file

@ -4,7 +4,6 @@ import LayerConfig from "../Customizations/JSON/LayerConfig";
import * as licenses from "../assets/generated/license_info.json"
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson";
import {Translation} from "../UI/i18n/Translation";
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
import AllKnownLayers from "../Customizations/AllKnownLayers";
@ -77,63 +76,6 @@ class LayerOverviewUtils {
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[]) {
const lt = this.loadThemesAndLayers();
@ -156,7 +98,6 @@ class LayerOverviewUtils {
}
let themeErrorCount = []
let missingTranslations = []
for (const themeFile of themeFiles) {
if (typeof themeFile.language === "string") {
themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings")
@ -165,10 +106,6 @@ class LayerOverviewUtils {
if (typeof layer === "string") {
if (!knownLayerIds.has(layer)) {
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) {
@ -186,7 +123,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 => l.builtin === undefined)
missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id))
try {
const theme = new LayoutConfig(themeFile, true, "test")
@ -198,11 +134,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) {
console.log("All good!")

31
test.ts
View file

@ -7,6 +7,9 @@ import {UIEventSource} from "./Logic/UIEventSource";
import {Tag} from "./Logic/Tags/Tag";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {Translation} from "./UI/i18n/Translation";
import LocationInput from "./UI/Input/LocationInput";
import Loc from "./Models/Loc";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
/*import ValidatedTextField from "./UI/Input/ValidatedTextField";
import Combine from "./UI/Base/Combine";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
@ -148,19 +151,15 @@ function TestMiniMap() {
featureSource.ping()
}
//*/
QueryParameters.GetQueryParameter("test", "true").setData("true")
State.state= new State(undefined)
const id = "node/5414688303"
State.state.allElements.addElementById(id, new UIEventSource<any>({id: id}))
new Combine([
new DeleteWizard(id, {
noDeleteOptions: [
{
if:[ new Tag("access","private")],
then: new Translation({
en: "Very private! Delete now or me send lawfull lawyer"
})
}
]
}),
]).AttachTo("maindiv")
const li = new LocationInput({
preferCategory:"photo",
centerLocation:
new UIEventSource<Loc>({
lat: 51.21576, lon: 3.22001, zoom: 19
})
})
li.SetStyle("height: 20rem")
.AttachTo("maindiv")
new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv")

View file

@ -10,7 +10,7 @@ export default class T {
test();
} catch (e) {
this.failures.push(name);
console.warn("Failed test: ", name, "because", e);
console.warn(`>>> Failed test in ${this.name}: ${name}because${e}`);
}
}
if (this.failures.length == 0) {

View file

@ -1,10 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-no-circular-imports"
],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}