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