forked from MapComplete/MapComplete
		
	Merge branch 'project/natuurpunt' of https://github.com/pietervdvn/MapComplete into project/natuurpunt
This commit is contained in:
		
						commit
						4d0f0e55f2
					
				
					 81 changed files with 18928 additions and 2269 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 BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import Combine from "../../UI/Base/Combine"; | ||||
| import {FixedUiElement} from "../../UI/Base/FixedUiElement"; | ||||
| 
 | ||||
| export class Unit { | ||||
|     public readonly appliesToKeys: Set<string>; | ||||
|  | @ -81,7 +82,10 @@ export class Unit { | |||
|             return undefined; | ||||
|         } | ||||
|         const [stripped, denom] = this.findDenomination(value) | ||||
|         const human = denom.human | ||||
|         const human = denom?.human | ||||
|         if(human === undefined){ | ||||
|             return new FixedUiElement(stripped ?? value); | ||||
|         } | ||||
| 
 | ||||
|         const elems = denom.prefix ? [human, stripped] : [stripped, human]; | ||||
|         return new Combine(elems) | ||||
|  | @ -152,7 +156,7 @@ export class Denomination { | |||
|         if (stripped === null) { | ||||
|             return null; | ||||
|         } | ||||
|         return stripped + " " + this.canonical.trim() | ||||
|         return (stripped + " " + this.canonical.trim()).trim(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -218,6 +218,16 @@ export interface LayerConfigJson { | |||
|          * (The first sentence is until the first '.'-character in the description) | ||||
|          */ | ||||
|         description?: string | any, | ||||
| 
 | ||||
|         /** | ||||
|          * If set, the user will prompted to confirm the location before actually adding the data. | ||||
|          * THis will be with a 'drag crosshair'-method. | ||||
|          *  | ||||
|          * If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category. | ||||
|          */ | ||||
|         preciseInput?: true | { | ||||
|             preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | ||||
|         } | ||||
|     }[], | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ export default class LayoutConfig { | |||
|     public readonly enableGeolocation: boolean; | ||||
|     public readonly enableBackgroundLayerSelection: boolean; | ||||
|     public readonly enableShowAllQuestions: boolean; | ||||
|     public readonly enableExportButton: boolean; | ||||
|     public readonly customCss?: string; | ||||
|     /* | ||||
|     How long is the cache valid, in seconds? | ||||
|  | @ -152,6 +153,7 @@ export default class LayoutConfig { | |||
|         this.enableAddNewPoints = json.enableAddNewPoints ?? true; | ||||
|         this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; | ||||
|         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; | ||||
|         this.enableExportButton = json.enableExportButton ?? false; | ||||
|         this.customCss = json.customCss; | ||||
|         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson"; | |||
|  * General remark: a type (string | any) indicates either a fixed or a translatable string. | ||||
|  */ | ||||
| export interface LayoutConfigJson { | ||||
|     | ||||
|     /** | ||||
|      * The id of this layout. | ||||
|      * | ||||
|  | @ -225,6 +226,10 @@ export interface LayoutConfigJson { | |||
|      * | ||||
|      * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. | ||||
|      * This is handled by defining units. | ||||
|      *  | ||||
|      * # Rendering | ||||
|      *  | ||||
|      * To render a value with long (human) denomination, use {canonical(key)} | ||||
|      * | ||||
|      * # Usage | ||||
|      * | ||||
|  | @ -331,4 +336,5 @@ export interface LayoutConfigJson { | |||
|     enableGeolocation?: boolean; | ||||
|     enableBackgroundLayerSelection?: boolean; | ||||
|     enableShowAllQuestions?: boolean; | ||||
|     enableExportButton?: boolean; | ||||
| } | ||||
|  |  | |||
|  | @ -26,6 +26,9 @@ export default class TagRenderingConfig { | |||
|         readonly key: string, | ||||
|         readonly type: string, | ||||
|         readonly addExtraTags: TagsFilter[]; | ||||
|         readonly inline: boolean, | ||||
|         readonly default?: string, | ||||
|         readonly helperArgs?: (string | number | boolean)[] | ||||
|     }; | ||||
| 
 | ||||
|     readonly multiAnswer: boolean; | ||||
|  | @ -73,7 +76,9 @@ export default class TagRenderingConfig { | |||
|                 type: json.freeform.type ?? "string", | ||||
|                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => | ||||
|                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], | ||||
| 
 | ||||
|                 inline: json.freeform.inline ?? false, | ||||
|                 default: json.freeform.default, | ||||
|                 helperArgs: json.freeform.helperArgs | ||||
| 
 | ||||
|             } | ||||
|             if (json.freeform["extraTags"] !== undefined) { | ||||
|  | @ -332,20 +337,20 @@ export default class TagRenderingConfig { | |||
|      * Note: this might be hidden by conditions | ||||
|      */ | ||||
|     public hasMinimap(): boolean { | ||||
|         const translations : Translation[]= Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); | ||||
|         const translations: Translation[] = Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); | ||||
|         for (const translation of translations) { | ||||
|             for (const key in translation.translations) { | ||||
|                 if(!translation.translations.hasOwnProperty(key)){ | ||||
|                 if (!translation.translations.hasOwnProperty(key)) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const template = translation.translations[key] | ||||
|                 const parts = SubstitutedTranslation.ExtractSpecialComponents(template) | ||||
|                 const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap") | ||||
|                 if(hasMiniMap){ | ||||
|                 const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") | ||||
|                 if (hasMiniMap) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     }  | ||||
|     } | ||||
| } | ||||
|  | @ -30,6 +30,7 @@ export interface TagRenderingConfigJson { | |||
|      * Allow freeform text input from the user | ||||
|      */ | ||||
|     freeform?: { | ||||
|      | ||||
|         /** | ||||
|          * If this key is present, then 'render' is used to display the value. | ||||
|          * 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 | ||||
|          */ | ||||
|         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. | ||||
|          * Useful to add a 'fixme=freeform textfield used - to be checked' | ||||
|          **/ | ||||
|         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 | ||||
|  ----------- | ||||
|   | ||||
|  **Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio, open everything in a 'new WSL Window'. | ||||
|  **Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio Code you can use a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the Devcontainer (see more details later). | ||||
|   | ||||
|  To develop and build MapComplete, yo | ||||
|  To develop and build MapComplete, you | ||||
|   | ||||
| 0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15 | ||||
| 0. Make a fork and clone the repository. | ||||
|  | @ -29,6 +29,30 @@ | |||
| 4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html | ||||
| 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. | ||||
| 
 | ||||
|  Development using Windows | ||||
|  ------------------------ | ||||
| 
 | ||||
|  For Windows you can use the devcontainer, or the WSL subsystem. | ||||
| 
 | ||||
|  To use the devcontainer in Visual Studio Code: | ||||
| 
 | ||||
| 0. Make sure you have installed the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and it's dependencies. | ||||
| 1. Make a fork and clone the repository. | ||||
| 2. After cloning, Visual Studio Code will ask you if you want to use the devcontainer. | ||||
| 3. Then you can either clone it again in a volume (for better performance), or open the current folder in a container. | ||||
| 4. By now, you should be able to run `npm run start` to host a local testversion at http://localhost:1234/index.html | ||||
| 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. | ||||
| 
 | ||||
|  To use the WSL in Visual Studio Code: | ||||
| 
 | ||||
| 0. Make sure you have installed the [Remote - WSL]() extension and it's dependencies. | ||||
| 1. Open a remote WSL window using the button in the bottom left. | ||||
| 2. Make a fork and clone the repository. | ||||
| 3. Install `npm` using `sudo apt install npm`. | ||||
| 4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too | ||||
| 5. Run `npm run start` to host a local testversion at http://localhost:1234/index.html | ||||
| 6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. | ||||
| 
 | ||||
| 
 | ||||
|  Automatic deployment | ||||
|  -------------------- | ||||
|  |  | |||
|  | @ -20,126 +20,158 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are | |||
| Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. | ||||
| 
 | ||||
| 
 | ||||
|  layer-control-toggle  | ||||
| ---------------------- | ||||
| 
 | ||||
|  Whether or not the layer control is shown The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  tab  | ||||
| ----- | ||||
| 
 | ||||
|  The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_ | ||||
| 
 | ||||
| 
 | ||||
|  z  | ||||
| --- | ||||
| 
 | ||||
|  The initial/current zoom level The default value is _0_ | ||||
| 
 | ||||
| 
 | ||||
|  lat  | ||||
| ----- | ||||
| 
 | ||||
|  The initial/current latitude The default value is _0_ | ||||
| 
 | ||||
| 
 | ||||
|  lon  | ||||
| ----- | ||||
| 
 | ||||
|  The initial/current longitude of the app The default value is _0_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-userbadge  | ||||
| -------------- | ||||
| 
 | ||||
|  Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-search  | ||||
| ----------- | ||||
| 
 | ||||
|  Disables/Enables the search bar The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-layers  | ||||
| ----------- | ||||
| 
 | ||||
|  Disables/Enables the layer control The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-add-new  | ||||
| ------------ | ||||
| 
 | ||||
|  Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-welcome-message  | ||||
| -------------------- | ||||
| 
 | ||||
|  Disables/enables the help menu or welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-iframe  | ||||
| ----------- | ||||
| 
 | ||||
|  Disables/Enables the iframe-popup The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-more-quests  | ||||
| ---------------- | ||||
| 
 | ||||
|  Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-share-screen  | ||||
| ----------------- | ||||
| 
 | ||||
|  Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-geolocation  | ||||
| ---------------- | ||||
| 
 | ||||
|  Disables/Enables the geolocation button The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
|  fs-all-questions  | ||||
| ------------------ | ||||
| 
 | ||||
|  Always show all questions The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  test  | ||||
| ------ | ||||
| 
 | ||||
|  If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  debug  | ||||
| ------- | ||||
| 
 | ||||
|  If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  backend  | ||||
| backend | ||||
| --------- | ||||
| 
 | ||||
|  The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ | ||||
| The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ | ||||
| 
 | ||||
| 
 | ||||
|  custom-css  | ||||
| test | ||||
| ------ | ||||
| 
 | ||||
| If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| layout | ||||
| -------- | ||||
| 
 | ||||
| The layout to load into MapComplete The default value is __ | ||||
| 
 | ||||
| 
 | ||||
| userlayout | ||||
| ------------ | ||||
| 
 | ||||
|  If specified, the custom css from the given link will be loaded additionaly The default value is __ | ||||
| If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: | ||||
| 
 | ||||
| - The hash of the URL contains a base64-encoded .json-file containing the theme definition | ||||
| - The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator | ||||
| - The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
|  background  | ||||
| layer-control-toggle | ||||
| ---------------------- | ||||
| 
 | ||||
| Whether or not the layer control is shown The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| tab | ||||
| ----- | ||||
| 
 | ||||
| The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_ | ||||
| 
 | ||||
| 
 | ||||
| z | ||||
| --- | ||||
| 
 | ||||
| The initial/current zoom level The default value is _14_ | ||||
| 
 | ||||
| 
 | ||||
| lat | ||||
| ----- | ||||
| 
 | ||||
| The initial/current latitude The default value is _51.2095_ | ||||
| 
 | ||||
| 
 | ||||
| lon | ||||
| ----- | ||||
| 
 | ||||
| The initial/current longitude of the app The default value is _3.2228_ | ||||
| 
 | ||||
| 
 | ||||
| fs-userbadge | ||||
| -------------- | ||||
| 
 | ||||
| Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-search | ||||
| ----------- | ||||
| 
 | ||||
| Disables/Enables the search bar The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-layers | ||||
| ----------- | ||||
| 
 | ||||
| Disables/Enables the layer control The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-add-new | ||||
| ------------ | ||||
| 
 | ||||
|  The id of the background layer to start with The default value is _osm_ | ||||
| Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-welcome-message | ||||
| -------------------- | ||||
| 
 | ||||
| Disables/enables the help menu or welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-iframe | ||||
| ----------- | ||||
| 
 | ||||
| Disables/Enables the iframe-popup The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| fs-more-quests | ||||
| ---------------- | ||||
| 
 | ||||
| Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-share-screen | ||||
| ----------------- | ||||
| 
 | ||||
| Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-geolocation | ||||
| ---------------- | ||||
| 
 | ||||
| Disables/Enables the geolocation button The default value is _true_ | ||||
| 
 | ||||
| 
 | ||||
| fs-all-questions | ||||
| ------------------ | ||||
| 
 | ||||
| Always show all questions The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| fs-export | ||||
| ----------- | ||||
| 
 | ||||
| If set, enables the 'download'-button to download everything as geojson The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| fake-user | ||||
| ----------- | ||||
| 
 | ||||
| If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| debug | ||||
| ------- | ||||
| 
 | ||||
| If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ | ||||
| 
 | ||||
| 
 | ||||
| custom-css | ||||
| ------------ | ||||
| 
 | ||||
| If specified, the custom css from the given link will be loaded additionaly The default value is __ | ||||
| 
 | ||||
| 
 | ||||
| background | ||||
| ------------ | ||||
| 
 | ||||
| The id of the background layer to start with The default value is _osm_ | ||||
| 
 | ||||
| 
 | ||||
| oauth_token | ||||
| ------------- | ||||
| 
 | ||||
| Used to complete the login No default value set | ||||
|  layer-<layer-id>  | ||||
| ------------------ | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1071
									
								
								InitUiElements.ts
									
										
									
									
									
								
							
							
						
						
									
										1071
									
								
								InitUiElements.ts
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1,11 +1,12 @@ | |||
| import * as editorlayerindex from "../../assets/editor-layer-index.json" | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import * as L from "leaflet"; | ||||
| import {TileLayer} from "leaflet"; | ||||
| import * as X from "leaflet-providers"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import {TileLayer} from "leaflet"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| 
 | ||||
| /** | ||||
|  * Calculates which layers are available at the current location | ||||
|  | @ -24,45 +25,87 @@ export default class AvailableBaseLayers { | |||
|                 false, false), | ||||
|             feature: null, | ||||
|             max_zoom: 19, | ||||
|             min_zoom: 0 | ||||
|             min_zoom: 0, | ||||
|             isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
 | ||||
|             category: "osmbasedmap" | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); | ||||
|     public availableEditorLayers: UIEventSource<BaseLayer[]>; | ||||
| 
 | ||||
|     constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) { | ||||
|         const self = this; | ||||
|         this.availableEditorLayers = | ||||
|             location.map( | ||||
|                 (currentLocation) => { | ||||
|     public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||
|         const source = location.map( | ||||
|             (currentLocation) => { | ||||
| 
 | ||||
|                     if (currentLocation === undefined) { | ||||
|                         return AvailableBaseLayers.layerOverview; | ||||
|                     } | ||||
|                 if (currentLocation === undefined) { | ||||
|                     return AvailableBaseLayers.layerOverview; | ||||
|                 } | ||||
| 
 | ||||
|                     const currentLayers = self.availableEditorLayers?.data; | ||||
|                     const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||
|                 const currentLayers = source?.data; // A bit unorthodox - I know
 | ||||
|                 const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||
| 
 | ||||
|                     if (currentLayers === undefined) { | ||||
|                 if (currentLayers === undefined) { | ||||
|                     return newLayers; | ||||
|                 } | ||||
|                 if (newLayers.length !== currentLayers.length) { | ||||
|                     return newLayers; | ||||
|                 } | ||||
|                 for (let i = 0; i < newLayers.length; i++) { | ||||
|                     if (newLayers[i].name !== currentLayers[i].name) { | ||||
|                         return newLayers; | ||||
|                     } | ||||
|                     if (newLayers.length !== currentLayers.length) { | ||||
|                         return newLayers; | ||||
|                     } | ||||
|                     for (let i = 0; i < newLayers.length; i++) { | ||||
|                         if (newLayers[i].name !== currentLayers[i].name) { | ||||
|                             return newLayers; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     return currentLayers; | ||||
|                 }); | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 return currentLayers; | ||||
|             }); | ||||
|         return source; | ||||
|     } | ||||
| 
 | ||||
|     private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||
|     public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||
|         return AvailableBaseLayers.AvailableLayersAt(location).map(available => { | ||||
|             // First float all 'best layers' to the top
 | ||||
|             available.sort((a, b) => { | ||||
|                     if (a.isBest && b.isBest) { | ||||
|                         return 0; | ||||
|                     } | ||||
|                     if (!a.isBest) { | ||||
|                         return 1 | ||||
|                     } | ||||
| 
 | ||||
|                     return -1; | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|             if (preferedCategory.data === undefined) { | ||||
|                 return available[0] | ||||
|             } | ||||
| 
 | ||||
|             let prefered: string [] | ||||
|             if (typeof preferedCategory.data === "string") { | ||||
|                 prefered = [preferedCategory.data] | ||||
|             } else { | ||||
|                 prefered = preferedCategory.data; | ||||
|             } | ||||
| 
 | ||||
|             prefered.reverse(); | ||||
|             for (const category of prefered) { | ||||
|                 //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
 | ||||
|                 available.sort((a, b) => { | ||||
|                         if (a.category === category && b.category === category) { | ||||
|                             return 0; | ||||
|                         } | ||||
|                         if (a.category !== category) { | ||||
|                             return 1 | ||||
|                         } | ||||
| 
 | ||||
|                         return -1; | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|             return available[0] | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||
|         const availableLayers = [AvailableBaseLayers.osmCarto] | ||||
|         const globalLayers = []; | ||||
|         for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { | ||||
|  | @ -140,7 +183,9 @@ export default class AvailableBaseLayers { | |||
|                 min_zoom: props.min_zoom ?? 1, | ||||
|                 name: props.name, | ||||
|                 layer: leafletLayer, | ||||
|                 feature: layer | ||||
|                 feature: layer, | ||||
|                 isBest: props.best ?? false, | ||||
|                 category: props.category | ||||
|             }); | ||||
|         } | ||||
|         return layers; | ||||
|  | @ -152,15 +197,16 @@ export default class AvailableBaseLayers { | |||
|         function l(id: string, name: string): BaseLayer { | ||||
|             try { | ||||
|                 const layer: any = () => L.tileLayer.provider(id, undefined); | ||||
|                 const baseLayer: BaseLayer = { | ||||
|                 return { | ||||
|                     feature: null, | ||||
|                     id: id, | ||||
|                     name: name, | ||||
|                     layer: layer, | ||||
|                     min_zoom: layer.minzoom, | ||||
|                     max_zoom: layer.maxzoom | ||||
|                     max_zoom: layer.maxzoom, | ||||
|                     category: "osmbasedmap", | ||||
|                     isBest: false | ||||
|                 } | ||||
|                 return baseLayer | ||||
|             } catch (e) { | ||||
|                 console.error("Could not find provided layer", name, e); | ||||
|                 return null; | ||||
|  |  | |||
|  | @ -1,265 +1,271 @@ | |||
| import * as L from "leaflet"; | ||||
| import { UIEventSource } from "../UIEventSource"; | ||||
| import { Utils } from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Svg from "../../Svg"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement"; | ||||
| import { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement"; | ||||
| import {VariableUiElement} from "../../UI/Base/VariableUIElement"; | ||||
| import {CenterFlexedElement} from "../../UI/Base/CenterFlexedElement"; | ||||
| 
 | ||||
| export default class GeoLocationHandler extends VariableUiElement { | ||||
|   /** | ||||
|    * Wether or not the geolocation is active, aka the user requested the current location | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _isActive: UIEventSource<boolean>; | ||||
|     /** | ||||
|      * Wether or not the geolocation is active, aka the user requested the current location | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _isActive: UIEventSource<boolean>; | ||||
| 
 | ||||
|   /** | ||||
|    * The callback over the permission API | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _permission: UIEventSource<string>; | ||||
|   /*** | ||||
|    * The marker on the map, in order to update it | ||||
|    * @private | ||||
|    */ | ||||
|   private _marker: L.Marker; | ||||
|   /** | ||||
|    * Literally: _currentGPSLocation.data != undefined | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _hasLocation: UIEventSource<boolean>; | ||||
|   private readonly _currentGPSLocation: UIEventSource<{ | ||||
|     latlng: any; | ||||
|     accuracy: number; | ||||
|   }>; | ||||
|   /** | ||||
|    * Kept in order to update the marker | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _leafletMap: UIEventSource<L.Map>; | ||||
|   /** | ||||
|    * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||
|    * @private | ||||
|    */ | ||||
|   private _lastUserRequest: Date; | ||||
|   /** | ||||
|    * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||
|    * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. | ||||
|    * | ||||
|    * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. | ||||
|    * If the user denies the geolocation this time, we unset this flag | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _previousLocationGrant: UIEventSource<string>; | ||||
|   private readonly _layoutToUse: UIEventSource<LayoutConfig>; | ||||
| 
 | ||||
|   constructor( | ||||
|     currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, | ||||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     layoutToUse: UIEventSource<LayoutConfig> | ||||
|   ) { | ||||
|     const hasLocation = currentGPSLocation.map( | ||||
|       (location) => location !== undefined | ||||
|     ); | ||||
|     const previousLocationGrant = LocalStorageSource.Get( | ||||
|       "geolocation-permissions" | ||||
|     ); | ||||
|     const isActive = new UIEventSource<boolean>(false); | ||||
|     /** | ||||
|      * 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>; | ||||
| 
 | ||||
|     super( | ||||
|       hasLocation.map( | ||||
|         (hasLocation) => { | ||||
|           if (hasLocation) { | ||||
|             return new CenterFlexedElement( | ||||
|               Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||
|             ); // crosshair_blue_ui()
 | ||||
|           } | ||||
|           if (isActive.data) { | ||||
|             return new CenterFlexedElement( | ||||
|               Img.AsImageElement(Svg.location, "", "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] | ||||
|       ) | ||||
|     ); | ||||
|     this._isActive = isActive; | ||||
|     this._permission = new UIEventSource<string>(""); | ||||
|     this._previousLocationGrant = previousLocationGrant; | ||||
|     this._currentGPSLocation = currentGPSLocation; | ||||
|     this._leafletMap = leafletMap; | ||||
|     this._layoutToUse = layoutToUse; | ||||
|     this._hasLocation = hasLocation; | ||||
|     const self = this; | ||||
|     /** | ||||
|      * The callback over the permission API | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _permission: UIEventSource<string>; | ||||
| 
 | ||||
|     const currentPointer = this._isActive.map( | ||||
|       (isActive) => { | ||||
|         if (isActive && !self._hasLocation.data) { | ||||
|           return "cursor-wait"; | ||||
|         } | ||||
|         return "cursor-pointer"; | ||||
|       }, | ||||
|       [this._hasLocation] | ||||
|     ); | ||||
|     currentPointer.addCallbackAndRun((pointerClass) => { | ||||
|       self.SetClass(pointerClass); | ||||
|     }); | ||||
|     /*** | ||||
|      * The marker on the map, in order to update it | ||||
|      * @private | ||||
|      */ | ||||
|     private _marker: L.Marker; | ||||
|     /** | ||||
|      * Literally: _currentGPSLocation.data != undefined | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _hasLocation: UIEventSource<boolean>; | ||||
|     private readonly _currentGPSLocation: UIEventSource<{ | ||||
|         latlng: any; | ||||
|         accuracy: number; | ||||
|     }>; | ||||
|     /** | ||||
|      * Kept in order to update the marker | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _leafletMap: UIEventSource<L.Map>; | ||||
| 
 | ||||
|     this.onClick(() => self.init(true)); | ||||
|     this.init(false); | ||||
|   } | ||||
| 
 | ||||
|   private init(askPermission: boolean) { | ||||
|     const self = this; | ||||
|     const map = this._leafletMap.data; | ||||
|     /** | ||||
|      * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||
|      * @private | ||||
|      */ | ||||
|     private _lastUserRequest: Date; | ||||
| 
 | ||||
|     this._currentGPSLocation.addCallback((location) => { | ||||
|       self._previousLocationGrant.setData("granted"); | ||||
| 
 | ||||
|       const timeSinceRequest = | ||||
|         (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||
|       if (timeSinceRequest < 30) { | ||||
|         self.MoveToCurrentLoction(16); | ||||
|       } | ||||
|     /** | ||||
|      * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||
|      * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. | ||||
|      * | ||||
|      * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. | ||||
|      * If the user denies the geolocation this time, we unset this flag | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _previousLocationGrant: UIEventSource<string>; | ||||
|     private readonly _layoutToUse: UIEventSource<LayoutConfig>; | ||||
| 
 | ||||
|       let color = "#1111cc"; | ||||
|       try { | ||||
|         color = getComputedStyle(document.body).getPropertyValue( | ||||
|           "--catch-detail-color" | ||||
|         ); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|       const icon = L.icon({ | ||||
|         iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), | ||||
|         iconSize: [40, 40], // size of the icon
 | ||||
|         iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||
|       }); | ||||
| 
 | ||||
|       const newMarker = L.marker(location.latlng, { icon: icon }); | ||||
|       newMarker.addTo(map); | ||||
| 
 | ||||
|       if (self._marker !== undefined) { | ||||
|         map.removeLayer(self._marker); | ||||
|       } | ||||
|       self._marker = newMarker; | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|       navigator?.permissions | ||||
|         ?.query({ name: "geolocation" }) | ||||
|         ?.then(function (status) { | ||||
|           console.log("Geolocation is already", status); | ||||
|           if (status.state === "granted") { | ||||
|             self.StartGeolocating(false); | ||||
|           } | ||||
|           self._permission.setData(status.state); | ||||
|           status.onchange = function () { | ||||
|             self._permission.setData(status.state); | ||||
|           }; | ||||
|         }); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|     } | ||||
|     if (askPermission) { | ||||
|       self.StartGeolocating(true); | ||||
|     } else if (this._previousLocationGrant.data === "granted") { | ||||
|       this._previousLocationGrant.setData(""); | ||||
|       self.StartGeolocating(false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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) { | ||||
|     const location = this._currentGPSLocation.data; | ||||
|     this._lastUserRequest = undefined; | ||||
| 
 | ||||
|     if ( | ||||
|       this._currentGPSLocation.data.latlng[0] === 0 && | ||||
|       this._currentGPSLocation.data.latlng[1] === 0 | ||||
|     constructor( | ||||
|         currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, | ||||
|         leafletMap: UIEventSource<L.Map>, | ||||
|         layoutToUse: UIEventSource<LayoutConfig> | ||||
|     ) { | ||||
|       console.debug("Not moving to GPS-location: it is null island"); | ||||
|       return; | ||||
|         const hasLocation = currentGPSLocation.map( | ||||
|             (location) => location !== undefined | ||||
|         ); | ||||
|         const previousLocationGrant = LocalStorageSource.Get( | ||||
|             "geolocation-permissions" | ||||
|         ); | ||||
|         const isActive = new UIEventSource<boolean>(false); | ||||
|         const isLocked = new UIEventSource<boolean>(false); | ||||
|         super( | ||||
|             hasLocation.map( | ||||
|                 (hasLocationData) => { | ||||
|                     let icon: string; | ||||
| 
 | ||||
|                     if (isLocked.data) { | ||||
|                         icon = Svg.crosshair_locked; | ||||
|                     } else if (hasLocationData) { | ||||
|                         icon = Svg.crosshair_blue; | ||||
|                     } else if (isActive.data) { | ||||
|                         icon = Svg.crosshair_blue_center; | ||||
|                     } else { | ||||
|                         icon = Svg.crosshair; | ||||
|                     } | ||||
| 
 | ||||
|                     return new CenterFlexedElement( | ||||
|                         Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem") | ||||
|                     ); | ||||
| 
 | ||||
|                 }, | ||||
|                 [isActive, isLocked] | ||||
|             ) | ||||
|         ); | ||||
|         this._isActive = isActive; | ||||
|         this._isLocked = isLocked; | ||||
|         this._permission = new UIEventSource<string>(""); | ||||
|         this._previousLocationGrant = previousLocationGrant; | ||||
|         this._currentGPSLocation = currentGPSLocation; | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._layoutToUse = layoutToUse; | ||||
|         this._hasLocation = hasLocation; | ||||
|         const self = this; | ||||
| 
 | ||||
|         const currentPointer = this._isActive.map( | ||||
|             (isActive) => { | ||||
|                 if (isActive && !self._hasLocation.data) { | ||||
|                     return "cursor-wait"; | ||||
|                 } | ||||
|                 return "cursor-pointer"; | ||||
|             }, | ||||
|             [this._hasLocation] | ||||
|         ); | ||||
|         currentPointer.addCallbackAndRun((pointerClass) => { | ||||
|             self.SetClass(pointerClass); | ||||
|         }); | ||||
| 
 | ||||
|         this.onClick(() => { | ||||
|             if (self._hasLocation.data) { | ||||
|                 self._isLocked.setData(!self._isLocked.data); | ||||
|             } | ||||
|             self.init(true); | ||||
|         }); | ||||
|         this.init(false); | ||||
| 
 | ||||
| 
 | ||||
|         this._currentGPSLocation.addCallback((location) => { | ||||
|             self._previousLocationGrant.setData("granted"); | ||||
| 
 | ||||
|             const timeSinceRequest = | ||||
|                 (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||
|             if (timeSinceRequest < 30) { | ||||
|                 self.MoveToCurrentLoction(16); | ||||
|             } else if (self._isLocked.data) { | ||||
|                 self.MoveToCurrentLoction(); | ||||
|             } | ||||
| 
 | ||||
|             let color = "#1111cc"; | ||||
|             try { | ||||
|                 color = getComputedStyle(document.body).getPropertyValue( | ||||
|                     "--catch-detail-color" | ||||
|                 ); | ||||
|             } catch (e) { | ||||
|                 console.error(e); | ||||
|             } | ||||
|             const icon = L.icon({ | ||||
|                 iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), | ||||
|                 iconSize: [40, 40], // size of the icon
 | ||||
|                 iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||
|             }); | ||||
| 
 | ||||
|             const map = self._leafletMap.data; | ||||
| 
 | ||||
|             const newMarker = L.marker(location.latlng, {icon: icon}); | ||||
|             newMarker.addTo(map); | ||||
| 
 | ||||
|             if (self._marker !== undefined) { | ||||
|                 map.removeLayer(self._marker); | ||||
|             } | ||||
|             self._marker = newMarker; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // We check that the GPS location is not out of bounds
 | ||||
|     const b = this._layoutToUse.data.lockLocation; | ||||
|     let inRange = true; | ||||
|     if (b) { | ||||
|       if (b !== true) { | ||||
|         // B is an array with our locklocation
 | ||||
|         inRange = | ||||
|           b[0][0] <= location.latlng[0] && | ||||
|           location.latlng[0] <= b[1][0] && | ||||
|           b[0][1] <= location.latlng[1] && | ||||
|           location.latlng[1] <= b[1][1]; | ||||
|       } | ||||
|     } | ||||
|     if (!inRange) { | ||||
|       console.log( | ||||
|         "Not zooming to GPS location: out of bounds", | ||||
|         b, | ||||
|         location.latlng | ||||
|       ); | ||||
|     } else { | ||||
|       this._leafletMap.data.setView(location.latlng, targetZoom); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private StartGeolocating(zoomToGPS = true) { | ||||
|     const self = this; | ||||
|     console.log("Starting geolocation"); | ||||
| 
 | ||||
|     this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); | ||||
|     if (self._permission.data === "denied") { | ||||
|       self._previousLocationGrant.setData(""); | ||||
|       return ""; | ||||
|     } | ||||
|     if (this._currentGPSLocation.data !== undefined) { | ||||
|       this.MoveToCurrentLoction(16); | ||||
|     } | ||||
| 
 | ||||
|     console.log("Searching location using GPS"); | ||||
|     this.locate(); | ||||
| 
 | ||||
|     if (!self._isActive.data) { | ||||
|       self._isActive.setData(true); | ||||
|       Utils.DoEvery(60000, () => { | ||||
|         if (document.visibilityState !== "visible") { | ||||
|           console.log("Not starting gps: document not visible"); | ||||
|           return; | ||||
|     private init(askPermission: boolean) { | ||||
|         const self = this; | ||||
|         if (self._isActive.data) { | ||||
|             self.MoveToCurrentLoction(16); | ||||
|             return; | ||||
|         } | ||||
|         try { | ||||
|             navigator?.permissions | ||||
|                 ?.query({name: "geolocation"}) | ||||
|                 ?.then(function (status) { | ||||
|                     console.log("Geolocation is already", status); | ||||
|                     if (status.state === "granted") { | ||||
|                         self.StartGeolocating(false); | ||||
|                     } | ||||
|                     self._permission.setData(status.state); | ||||
|                     status.onchange = function () { | ||||
|                         self._permission.setData(status.state); | ||||
|                     }; | ||||
|                 }); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|         } | ||||
|         if (askPermission) { | ||||
|             self.StartGeolocating(true); | ||||
|         } else if (this._previousLocationGrant.data === "granted") { | ||||
|             this._previousLocationGrant.setData(""); | ||||
|             self.StartGeolocating(false); | ||||
|         } | ||||
|         this.locate(); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     private MoveToCurrentLoction(targetZoom = 16) { | ||||
|         const location = this._currentGPSLocation.data; | ||||
|         this._lastUserRequest = undefined; | ||||
| 
 | ||||
|         if ( | ||||
|             this._currentGPSLocation.data.latlng[0] === 0 && | ||||
|             this._currentGPSLocation.data.latlng[1] === 0 | ||||
|         ) { | ||||
|             console.debug("Not moving to GPS-location: it is null island"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // We check that the GPS location is not out of bounds
 | ||||
|         const b = this._layoutToUse.data.lockLocation; | ||||
|         let inRange = true; | ||||
|         if (b) { | ||||
|             if (b !== true) { | ||||
|                 // B is an array with our locklocation
 | ||||
|                 inRange = | ||||
|                     b[0][0] <= location.latlng[0] && | ||||
|                     location.latlng[0] <= b[1][0] && | ||||
|                     b[0][1] <= location.latlng[1] && | ||||
|                     location.latlng[1] <= b[1][1]; | ||||
|             } | ||||
|         } | ||||
|         if (!inRange) { | ||||
|             console.log( | ||||
|                 "Not zooming to GPS location: out of bounds", | ||||
|                 b, | ||||
|                 location.latlng | ||||
|             ); | ||||
|         } else { | ||||
|             this._leafletMap.data.setView(location.latlng, targetZoom); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private StartGeolocating(zoomToGPS = true) { | ||||
|         const self = this; | ||||
|         console.log("Starting geolocation"); | ||||
| 
 | ||||
|         this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); | ||||
|         if (self._permission.data === "denied") { | ||||
|             self._previousLocationGrant.setData(""); | ||||
|             return ""; | ||||
|         } | ||||
|         if (this._currentGPSLocation.data !== undefined) { | ||||
|             this.MoveToCurrentLoction(16); | ||||
|         } | ||||
| 
 | ||||
|         console.log("Searching location using GPS"); | ||||
| 
 | ||||
|         if (self._isActive.data) { | ||||
|             return; | ||||
|         } | ||||
|         self._isActive.setData(true); | ||||
|         navigator.geolocation.watchPosition( | ||||
|             function (position) { | ||||
|                 self._currentGPSLocation.setData({ | ||||
|                     latlng: [position.coords.latitude, position.coords.longitude], | ||||
|                     accuracy: position.coords.accuracy, | ||||
|                 }); | ||||
|             }, | ||||
|             function () { | ||||
|                 console.warn("Could not get location with navigator.geolocation"); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -47,7 +47,12 @@ export default class StrayClickHandler { | |||
|                     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.bindPopup(popup); | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ import RegisteringFeatureSource from "./RegisteringFeatureSource"; | |||
| 
 | ||||
| export default class FeaturePipeline implements FeatureSource { | ||||
| 
 | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> ; | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
| 
 | ||||
|     public readonly name = "FeaturePipeline" | ||||
| 
 | ||||
|  | @ -29,7 +29,7 @@ export default class FeaturePipeline implements FeatureSource { | |||
|                 selectedElement: UIEventSource<any>) { | ||||
| 
 | ||||
|         const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|          | ||||
| 
 | ||||
|         // first we metatag, then we save to get the metatags into storage too
 | ||||
|         // Note that we need to register before we do metatagging (as it expects the event sources)
 | ||||
| 
 | ||||
|  | @ -46,8 +46,11 @@ export default class FeaturePipeline implements FeatureSource { | |||
|         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||
|             .ConstructMultiSource(flayers.data, locationControl) | ||||
|             .map(geojsonSource => { | ||||
|                 let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); | ||||
|                 if(!geojsonSource.isOsmCache){ | ||||
|                 let source = new RegisteringFeatureSource( | ||||
|                     new FeatureDuplicatorPerLayer(flayers, | ||||
|                             geojsonSource | ||||
|                     )); | ||||
|                 if (!geojsonSource.isOsmCache) { | ||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||
|                 } | ||||
|                 return source | ||||
|  |  | |||
|  | @ -1,9 +1,45 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: UIEventSource<{feature: any, freshness: Date}[]>; | ||||
|     features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|     /** | ||||
|      * Mainly used for debuging | ||||
|      */ | ||||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| export class FeatureSourceUtils { | ||||
| 
 | ||||
|     /** | ||||
|      * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) | ||||
|      * @param featurePipeline The FeaturePipeline you want to export | ||||
|      * @param options The options object | ||||
|      * @param options.metadata True if you want to include the MapComplete metadata, false otherwise | ||||
|      */ | ||||
|     public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { | ||||
|         let defaults = { | ||||
|             metadata: false, | ||||
|         } | ||||
|         options = Utils.setDefaults(options, defaults); | ||||
| 
 | ||||
|         // Select all features, ignore the freshness and other data
 | ||||
|         let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature); | ||||
| 
 | ||||
|         if (!options.metadata) { | ||||
|             for (let i = 0; i < featureList.length; i++) { | ||||
|                 let feature = featureList[i]; | ||||
|                 for (let property in feature.properties) { | ||||
|                     if (property[0] == "_") { | ||||
|                         delete featureList[i]["properties"][property]; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return {type: "FeatureCollection", features: featureList} | ||||
| 
 | ||||
|    | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource { | |||
| 
 | ||||
|                 let freshness: Date = time; | ||||
|                 if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||
|                     freshness = new Date(feature["_last_edit:timestamp"]) | ||||
|                     freshness = new Date(feature.properties["_last_edit:timestamp"]) | ||||
|                 } | ||||
| 
 | ||||
|                 newFeatures.push({feature: feature, freshness: freshness}) | ||||
|  |  | |||
|  | @ -6,11 +6,14 @@ export class GeoOperations { | |||
|         return turf.area(feature); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a GeoJSon feature to a point feature | ||||
|      * @param feature | ||||
|      */ | ||||
|     static centerpoint(feature: any) { | ||||
|         const newFeature = turf.center(feature); | ||||
|         newFeature.properties = feature.properties; | ||||
|         newFeature.id = feature.id; | ||||
| 
 | ||||
|         return newFeature; | ||||
|     } | ||||
| 
 | ||||
|  | @ -273,6 +276,14 @@ export class GeoOperations { | |||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|     /** | ||||
|      * Generates the closest point on a way from a given point | ||||
|      * @param way The road on which you want to find a point | ||||
|      * @param point Point defined as [lon, lat] | ||||
|      */ | ||||
|     public static nearestPoint(way, point: [number, number]){ | ||||
|         return turf.nearestPointOnLine(way, point, {units: "kilometers"}); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,31 +6,38 @@ import Constants from "../../Models/Constants"; | |||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "./OsmConnection"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes implements FeatureSource{ | ||||
| export class Changes implements FeatureSource { | ||||
| 
 | ||||
|      | ||||
| 
 | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * The newly created points, as a FeatureSource | ||||
|      */ | ||||
|     public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||
|      | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
|     /** | ||||
|      * All the pending changes | ||||
|      */ | ||||
|     public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =  | ||||
|         new UIEventSource<{elementId: string; key: string; value: string}[]>([]); | ||||
|     public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) | ||||
| 
 | ||||
|     /** | ||||
|      * All the pending new objects to upload | ||||
|      */ | ||||
|     private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) | ||||
| 
 | ||||
|     private readonly isUploading = new UIEventSource(false); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a change to the pending changes | ||||
|      */ | ||||
|     private static checkChange(kv: {k: string, v: string}): { k: string, v: string } { | ||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { | ||||
|         const key = kv.k; | ||||
|         const value = kv.v; | ||||
|         if (key === undefined || key === null) { | ||||
|  | @ -49,8 +56,7 @@ export class Changes implements FeatureSource{ | |||
|         return {k: key.trim(), v: value.trim()}; | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|      | ||||
| 
 | ||||
|     addTag(elementId: string, tagsFilter: TagsFilter, | ||||
|            tags?: UIEventSource<any>) { | ||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||
|  | @ -59,7 +65,7 @@ export class Changes implements FeatureSource{ | |||
|         if (changes.length == 0) { | ||||
|             return; | ||||
|         } | ||||
|        | ||||
| 
 | ||||
|         for (const change of changes) { | ||||
|             if (elementTags[change.k] !== change.v) { | ||||
|                 elementTags[change.k] = change.v; | ||||
|  | @ -76,16 +82,16 @@ export class Changes implements FeatureSource{ | |||
|      * Uploads all the pending changes in one go. | ||||
|      * Triggered by the 'PendingChangeUploader'-actor in Actors | ||||
|      */ | ||||
|     public flushChanges(flushreason: string = undefined){ | ||||
|         if(this.pending.data.length === 0){ | ||||
|     public flushChanges(flushreason: string = undefined) { | ||||
|         if (this.pending.data.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|         if(flushreason !== undefined){ | ||||
|         if (flushreason !== undefined) { | ||||
|             console.log(flushreason) | ||||
|         } | ||||
|         this.uploadAll([], this.pending.data); | ||||
|         this.pending.setData([]); | ||||
|         this.uploadAll(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new node element at the given lat/long. | ||||
|      * An internal OsmObject is created to upload later on, a geojson represention is returned. | ||||
|  | @ -93,12 +99,12 @@ export class Changes implements FeatureSource{ | |||
|      */ | ||||
|     public createElement(basicTags: Tag[], lat: number, lon: number) { | ||||
|         console.log("Creating a new element with ", basicTags) | ||||
|         const osmNode = new OsmNode(Changes._nextId); | ||||
|         const newId = Changes._nextId; | ||||
|         Changes._nextId--; | ||||
| 
 | ||||
|         const id = "node/" + osmNode.id; | ||||
|         osmNode.lat = lat; | ||||
|         osmNode.lon = lon; | ||||
|         const id = "node/" + newId; | ||||
| 
 | ||||
| 
 | ||||
|         const properties = {id: id}; | ||||
| 
 | ||||
|         const geojson = { | ||||
|  | @ -118,35 +124,49 @@ export class Changes implements FeatureSource{ | |||
|         // The tags are not yet written into the OsmObject, but this is applied onto a 
 | ||||
|         const changes = []; | ||||
|         for (const kv of basicTags) { | ||||
|             properties[kv.key] = kv.value; | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use a regex in a preset" | ||||
|             } | ||||
|             properties[kv.key] = kv.value; | ||||
|             changes.push({elementId: id, key: kv.key, value: kv.value}) | ||||
|         } | ||||
|         | ||||
| 
 | ||||
|         console.log("New feature added and pinged") | ||||
|         this.features.data.push({feature:geojson, freshness: new Date()}); | ||||
|         this.features.data.push({feature: geojson, freshness: new Date()}); | ||||
|         this.features.ping(); | ||||
|          | ||||
| 
 | ||||
|         State.state.allElements.addOrGetElement(geojson).ping(); | ||||
| 
 | ||||
|         this.uploadAll([osmNode], changes); | ||||
|         if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) { | ||||
|             properties["_backend"] = State.state.osmConnection.userDetails.data.backend | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.newObjects.data.push({id: newId, lat: lat, lon: lon}) | ||||
|         this.pending.data.push(...changes) | ||||
|         this.pending.ping(); | ||||
|         this.newObjects.ping(); | ||||
|         return geojson; | ||||
|     } | ||||
| 
 | ||||
|     private uploadChangesWithLatestVersions( | ||||
|         knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { | ||||
|         knownElements: OsmObject[]) { | ||||
|         const knownById = new Map<string, OsmObject>(); | ||||
|          | ||||
|         knownElements.forEach(knownElement => { | ||||
|             knownById.set(knownElement.type + "/" + knownElement.id, knownElement) | ||||
|         }) | ||||
|          | ||||
|           | ||||
| 
 | ||||
|         const newElements: OsmNode [] = this.newObjects.data.map(spec => { | ||||
|             const newElement = new OsmNode(spec.id); | ||||
|             newElement.lat = spec.lat; | ||||
|             newElement.lon = spec.lon; | ||||
|             return newElement | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
 | ||||
|         // We apply the changes on them
 | ||||
|         for (const change of pending) { | ||||
|         for (const change of this.pending.data) { | ||||
|             if (parseInt(change.elementId.split("/")[1]) < 0) { | ||||
|                 // This is a new element - we should apply this on one of the new elements
 | ||||
|                 for (const newElement of newElements) { | ||||
|  | @ -168,9 +188,17 @@ export class Changes implements FeatureSource{ | |||
|             } | ||||
|         } | ||||
|         if (changedElements.length == 0 && newElements.length == 0) { | ||||
|             console.log("No changes in any object"); | ||||
|             console.log("No changes in any object - clearing"); | ||||
|             this.pending.setData([]) | ||||
|             this.newObjects.setData([]) | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
| 
 | ||||
|         if (this.isUploading.data) { | ||||
|             return; | ||||
|         } | ||||
|         this.isUploading.setData(true) | ||||
| 
 | ||||
|         console.log("Beginning upload..."); | ||||
|         // At last, we build the changeset and upload
 | ||||
|  | @ -213,17 +241,22 @@ export class Changes implements FeatureSource{ | |||
|                 changes += "</osmChange>"; | ||||
| 
 | ||||
|                 return changes; | ||||
|             }); | ||||
|             }, | ||||
|             () => { | ||||
|                 console.log("Upload successfull!") | ||||
|                 self.newObjects.setData([]) | ||||
|                 self.pending.setData([]); | ||||
|                 self.isUploading.setData(false) | ||||
|             }, | ||||
|             () => self.isUploading.setData(false) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     private uploadAll( | ||||
|         newElements: OsmObject[], | ||||
|         pending: { elementId: string; key: string; value: string }[] | ||||
|     ) { | ||||
|     private uploadAll() { | ||||
|         const self = this; | ||||
| 
 | ||||
| 
 | ||||
|         const pending = this.pending.data; | ||||
|         let neededIds: string[] = []; | ||||
|         for (const change of pending) { | ||||
|             const id = change.elementId; | ||||
|  | @ -236,8 +269,7 @@ export class Changes implements FeatureSource{ | |||
| 
 | ||||
|         neededIds = Utils.Dedup(neededIds); | ||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||
|             console.log("KnownElements:", knownElements) | ||||
|             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) | ||||
|             self.uploadChangesWithLatestVersions(knownElements) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ export class ChangesetHandler { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { | ||||
|     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void { | ||||
|         const nodes = response.getElementsByTagName("node"); | ||||
|         // @ts-ignore
 | ||||
|         for (const node of nodes) { | ||||
|  | @ -69,7 +69,9 @@ export class ChangesetHandler { | |||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         whenDone: (csId: string) => void, | ||||
|         onFail: () => void) { | ||||
| 
 | ||||
|         if (this.userDetails.data.csCount == 0) { | ||||
|             // The user became a contributor!
 | ||||
|  | @ -80,6 +82,7 @@ export class ChangesetHandler { | |||
|         if (this._dryRun) { | ||||
|             const changesetXML = generateChangeXML("123456"); | ||||
|             console.log(changesetXML); | ||||
|             whenDone("123456") | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -93,12 +96,14 @@ export class ChangesetHandler { | |||
|                 console.log(changeset); | ||||
|                 self.AddChange(csId, changeset, | ||||
|                     allElements, | ||||
|                     () => { | ||||
|                     }, | ||||
|                     whenDone, | ||||
|                     (e) => { | ||||
|                         console.error("UPLOADING FAILED!", e) | ||||
|                         onFail() | ||||
|                     } | ||||
|                 ) | ||||
|             }, { | ||||
|                 onFail: onFail | ||||
|             }) | ||||
|         } else { | ||||
|             // There still exists an open changeset (or at least we hope so)
 | ||||
|  | @ -107,15 +112,13 @@ export class ChangesetHandler { | |||
|                 csId, | ||||
|                 generateChangeXML(csId), | ||||
|                 allElements, | ||||
|                 () => { | ||||
|                 }, | ||||
|                 whenDone, | ||||
|                 (e) => { | ||||
|                     console.warn("Could not upload, changeset is probably closed: ", e); | ||||
|                     // Mark the CS as closed...
 | ||||
|                     this.currentChangeset.setData(""); | ||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist  
 | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML); | ||||
| 
 | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|  | @ -161,18 +164,22 @@ export class ChangesetHandler { | |||
|         const self = this; | ||||
|         this.OpenChangeset(layout, (csId: string) => { | ||||
| 
 | ||||
|             // The cs is open - let us actually upload!
 | ||||
|             const changes = generateChangeXML(csId) | ||||
|                 // The cs is open - let us actually upload!
 | ||||
|                 const changes = generateChangeXML(csId) | ||||
| 
 | ||||
|             self.AddChange(csId, changes, allElements, (csId) => { | ||||
|                 console.log("Successfully deleted ", object.id) | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }, (csId) => { | ||||
|                 alert("Deletion failed... Should not happend") | ||||
|                 // FAILED
 | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }) | ||||
|         }, true, reason) | ||||
|                 self.AddChange(csId, changes, allElements, (csId) => { | ||||
|                     console.log("Successfully deleted ", object.id) | ||||
|                     self.CloseChangeset(csId, continuation) | ||||
|                 }, (csId) => { | ||||
|                     alert("Deletion failed... Should not happend") | ||||
|                     // FAILED
 | ||||
|                     self.CloseChangeset(csId, continuation) | ||||
|                 }) | ||||
|             }, { | ||||
|                 isDeletionCS: true, | ||||
|                 deletionReason: reason | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { | ||||
|  | @ -204,15 +211,20 @@ export class ChangesetHandler { | |||
|     private OpenChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         continuation: (changesetId: string) => void, | ||||
|         isDeletionCS: boolean = false, | ||||
|         deletionReason: string = undefined) { | ||||
| 
 | ||||
|         options?: { | ||||
|             isDeletionCS?: boolean, | ||||
|             deletionReason?: string, | ||||
|             onFail?: () => void | ||||
|         } | ||||
|     ) { | ||||
|         options = options ?? {} | ||||
|         options.isDeletionCS = options.isDeletionCS ?? false | ||||
|         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; | ||||
|         let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|         if (isDeletionCS) { | ||||
|         if (options.isDeletionCS) { | ||||
|             comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|             if (deletionReason) { | ||||
|                 comment += ": " + deletionReason; | ||||
|             if (options.deletionReason) { | ||||
|                 comment += ": " + options.deletionReason; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -221,7 +233,7 @@ export class ChangesetHandler { | |||
|         const metadata = [ | ||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||
|             ["comment", comment], | ||||
|             ["deletion", isDeletionCS ? "yes" : undefined], | ||||
|             ["deletion", options.isDeletionCS ? "yes" : undefined], | ||||
|             ["theme", layout.id], | ||||
|             ["language", Locale.language.data], | ||||
|             ["host", window.location.host], | ||||
|  | @ -244,7 +256,9 @@ export class ChangesetHandler { | |||
|         }, function (err, response) { | ||||
|             if (response === undefined) { | ||||
|                 console.log("err", err); | ||||
|                 alert("Could not upload change (opening failed). Please file a bug report") | ||||
|                 if(options.onFail){ | ||||
|                     options.onFail() | ||||
|                 } | ||||
|                 return; | ||||
|             } else { | ||||
|                 continuation(response); | ||||
|  | @ -265,7 +279,7 @@ export class ChangesetHandler { | |||
|     private AddChange(changesetId: string, | ||||
|                       changesetXML: string, | ||||
|                       allElements: ElementStorage, | ||||
|                       continuation: ((changesetId: string, idMapping: any) => void), | ||||
|                       continuation: ((changesetId: string) => void), | ||||
|                       onFail: ((changesetId: string, reason: string) => void) = undefined) { | ||||
|         this.auth.xhr({ | ||||
|             method: 'POST', | ||||
|  | @ -280,9 +294,9 @@ export class ChangesetHandler { | |||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|             const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements); | ||||
|             ChangesetHandler.parseUploadChangesetResponse(response, allElements); | ||||
|             console.log("Uploaded changeset ", changesetId); | ||||
|             continuation(changesetId, mapping); | ||||
|             continuation(changesetId); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ export default class UserDetails { | |||
| 
 | ||||
| export class OsmConnection { | ||||
| 
 | ||||
|     public static readonly _oauth_configs = { | ||||
|     public static readonly oauth_configs = { | ||||
|         "osm": { | ||||
|             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', | ||||
|             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', | ||||
|  | @ -47,6 +47,7 @@ export class OsmConnection { | |||
|     public auth; | ||||
|     public userDetails: UIEventSource<UserDetails>; | ||||
|     public isLoggedIn: UIEventSource<boolean> | ||||
|     private fakeUser: boolean; | ||||
|     _dryRun: boolean; | ||||
|     public preferencesHandler: OsmPreferences; | ||||
|     public changesetHandler: ChangesetHandler; | ||||
|  | @ -59,20 +60,31 @@ export class OsmConnection { | |||
|         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
 | ||||
|                 layoutName: string, | ||||
|                 singlePage: boolean = true, | ||||
|                 osmConfiguration: "osm" | "osm-test" = 'osm' | ||||
|     ) { | ||||
|         this.fakeUser = fakeUser; | ||||
|         this._singlePage = singlePage; | ||||
|         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; | ||||
|         this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm; | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
| 
 | ||||
|         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; | ||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { | ||||
|             if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ | ||||
|  | @ -110,8 +122,10 @@ export class OsmConnection { | |||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         whenDone: (csId: string) => void, | ||||
|         onFail: () => {}) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); | ||||
|     } | ||||
| 
 | ||||
|     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|  | @ -136,6 +150,10 @@ export class OsmConnection { | |||
|     } | ||||
| 
 | ||||
|     public AttemptLogin() { | ||||
|         if(this.fakeUser){ | ||||
|             console.log("AttemptLogin called, but ignored as fakeUser is set") | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
|         console.log("Trying to log in..."); | ||||
|         this.updateAuthObject(); | ||||
|  |  | |||
|  | @ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource"; | |||
| 
 | ||||
| export abstract class OsmObject { | ||||
| 
 | ||||
|     protected static backendURL = "https://www.openstreetmap.org/" | ||||
|     private static defaultBackend = "https://www.openstreetmap.org/" | ||||
|     protected static backendURL = OsmObject.defaultBackend; | ||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); | ||||
|     private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); | ||||
|  | @ -37,15 +38,15 @@ export abstract class OsmObject { | |||
|     } | ||||
| 
 | ||||
|     static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> { | ||||
|         let src : UIEventSource<OsmObject>; | ||||
|         let src: UIEventSource<OsmObject>; | ||||
|         if (OsmObject.objectCache.has(id)) { | ||||
|             src = OsmObject.objectCache.get(id) | ||||
|             if(forceRefresh){ | ||||
|             if (forceRefresh) { | ||||
|                 src.setData(undefined) | ||||
|             }else{ | ||||
|             } else { | ||||
|                 return src; | ||||
|             } | ||||
|         }else{ | ||||
|         } else { | ||||
|             src = new UIEventSource<OsmObject>(undefined) | ||||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|  | @ -157,7 +158,7 @@ export abstract class OsmObject { | |||
|         const minlat = bounds[1][0] | ||||
|         const maxlat = bounds[0][0]; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` | ||||
|         Utils.downloadJson(url).then( data => { | ||||
|         Utils.downloadJson(url).then(data => { | ||||
|             const elements: any[] = data.elements; | ||||
|             const objects = OsmObject.ParseObjects(elements) | ||||
|             callback(objects); | ||||
|  | @ -291,6 +292,7 @@ export abstract class OsmObject { | |||
| 
 | ||||
|                 self.LoadData(element) | ||||
|                 self.SaveExtraData(element, nodes); | ||||
| 
 | ||||
|                 const meta = { | ||||
|                     "_last_edit:contributor": element.user, | ||||
|                     "_last_edit:contributor:uid": element.uid, | ||||
|  | @ -299,6 +301,11 @@ export abstract class OsmObject { | |||
|                     "_version_number": element.version | ||||
|                 } | ||||
| 
 | ||||
|                 if (OsmObject.backendURL !== OsmObject.defaultBackend) { | ||||
|                     self.tags["_backend"] = OsmObject.backendURL | ||||
|                     meta["_backend"] = OsmObject.backendURL; | ||||
|                 } | ||||
| 
 | ||||
|                 continuation(self, meta); | ||||
|             } | ||||
|         ); | ||||
|  |  | |||
|  | @ -83,7 +83,8 @@ export default class SimpleMetaTagger { | |||
| 
 | ||||
|         }, | ||||
|         (feature => { | ||||
|             const units = State.state.layoutToUse.data.units ?? []; | ||||
|             const units = State.state?.layoutToUse?.data?.units ?? []; | ||||
|             let rewritten = false; | ||||
|             for (const key in feature.properties) { | ||||
|                 if (!feature.properties.hasOwnProperty(key)) { | ||||
|                     continue; | ||||
|  | @ -95,16 +96,23 @@ export default class SimpleMetaTagger { | |||
|                     const value = feature.properties[key] | ||||
|                     const [, denomination] = unit.findDenomination(value) | ||||
|                     let canonical = denomination?.canonicalValue(value) ?? undefined; | ||||
|                     console.log("Rewritten ", key, " from", value, "into", canonical) | ||||
|                     if(canonical === undefined && !unit.eraseInvalid) { | ||||
|                     if(canonical === value){ | ||||
|                         break; | ||||
|                     } | ||||
|                      | ||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) | ||||
|                     if (canonical === undefined && !unit.eraseInvalid) { | ||||
|                         break; | ||||
|                     } | ||||
| 
 | ||||
|                     feature.properties[key] = canonical; | ||||
|                     rewritten = true; | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if(rewritten){ | ||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); | ||||
|             } | ||||
|         }) | ||||
|     ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource"; | |||
|  * UIEventsource-wrapper around localStorage | ||||
|  */ | ||||
| export class LocalStorageSource { | ||||
|      | ||||
|     static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{ | ||||
|         return LocalStorageSource.Get(key).map( | ||||
|             str => { | ||||
|                 if(str === undefined){ | ||||
|                     return defaultValue | ||||
|                 } | ||||
|                 try{ | ||||
|                     return JSON.parse(str) | ||||
|                 }catch{ | ||||
|                     return defaultValue | ||||
|                 } | ||||
|             }, [],  | ||||
|             value => JSON.stringify(value) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { | ||||
|         try { | ||||
|  |  | |||
|  | @ -7,4 +7,6 @@ export default interface BaseLayer { | |||
|     max_zoom: number, | ||||
|     min_zoom: number; | ||||
|     feature: any, | ||||
|     isBest?: boolean, | ||||
|     category?: "map" | "osmbasedmap" | "photo"  | "historicphoto" | string | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import { Utils } from "../Utils"; | |||
| 
 | ||||
| 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
 | ||||
|     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 | ||||
| } | ||||
							
								
								
									
										773
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										773
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -1,13 +1,13 @@ | |||
| import { Utils } from "./Utils"; | ||||
| import { ElementStorage } from "./Logic/ElementStorage"; | ||||
| import { Changes } from "./Logic/Osm/Changes"; | ||||
| import { OsmConnection } from "./Logic/Osm/OsmConnection"; | ||||
| import {Utils} from "./Utils"; | ||||
| import {ElementStorage} from "./Logic/ElementStorage"; | ||||
| import {Changes} from "./Logic/Osm/Changes"; | ||||
| import {OsmConnection} from "./Logic/Osm/OsmConnection"; | ||||
| import Locale from "./UI/i18n/Locale"; | ||||
| import { UIEventSource } from "./Logic/UIEventSource"; | ||||
| import { LocalStorageSource } from "./Logic/Web/LocalStorageSource"; | ||||
| import { QueryParameters } from "./Logic/Web/QueryParameters"; | ||||
| import {UIEventSource} from "./Logic/UIEventSource"; | ||||
| import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; | ||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||
| import LayoutConfig from "./Customizations/JSON/LayoutConfig"; | ||||
| import { MangroveIdentity } from "./Logic/Web/MangroveReviews"; | ||||
| import {MangroveIdentity} from "./Logic/Web/MangroveReviews"; | ||||
| import InstalledThemes from "./Logic/Actors/InstalledThemes"; | ||||
| import BaseLayer from "./Models/BaseLayer"; | ||||
| import Loc from "./Models/Loc"; | ||||
|  | @ -17,410 +17,423 @@ import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; | |||
| import LayerConfig from "./Customizations/JSON/LayerConfig"; | ||||
| import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||
| 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 FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  */ | ||||
| 
 | ||||
| export default class State { | ||||
|   // The singleton of the global state
 | ||||
|   public static state: State; | ||||
|     // The singleton of the global state
 | ||||
|     public static state: State; | ||||
| 
 | ||||
|   public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined); | ||||
|     public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined); | ||||
| 
 | ||||
|   /** | ||||
|     /** | ||||
|      The mapping from id -> UIEventSource<properties> | ||||
|      */ | ||||
|   public allElements: ElementStorage; | ||||
|   /** | ||||
|     public allElements: ElementStorage; | ||||
|     /** | ||||
|      THe change handler | ||||
|      */ | ||||
|   public changes: Changes; | ||||
|   /** | ||||
|     public changes: Changes; | ||||
|     /** | ||||
|      The leaflet instance of the big basemap | ||||
|      */ | ||||
|   public leafletMap = new UIEventSource<L.Map>(undefined); | ||||
|   /** | ||||
|    * Background layer id | ||||
|    */ | ||||
|   public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||
|   /** | ||||
|     public leafletMap = new UIEventSource<L.Map>(undefined); | ||||
|     /** | ||||
|      * Background layer id | ||||
|      */ | ||||
|     public availableBackgroundLayers: UIEventSource<BaseLayer[]>; | ||||
|     /** | ||||
|      The user credentials | ||||
|      */ | ||||
|   public osmConnection: OsmConnection; | ||||
|     public osmConnection: OsmConnection; | ||||
| 
 | ||||
|   public mangroveIdentity: MangroveIdentity; | ||||
|     public mangroveIdentity: MangroveIdentity; | ||||
| 
 | ||||
|   public favouriteLayers: UIEventSource<string[]>; | ||||
|     public favouriteLayers: UIEventSource<string[]>; | ||||
| 
 | ||||
|   public layerUpdater: OverpassFeatureSource; | ||||
|     public layerUpdater: OverpassFeatureSource; | ||||
| 
 | ||||
|   public osmApiFeatureSource: OsmApiFeatureSource; | ||||
|     public osmApiFeatureSource: OsmApiFeatureSource; | ||||
| 
 | ||||
|   public filteredLayers: UIEventSource< | ||||
|     { | ||||
|       readonly isDisplayed: UIEventSource<boolean>; | ||||
|       readonly layerDef: LayerConfig; | ||||
|     }[] | ||||
|   > = new UIEventSource< | ||||
|     { | ||||
|       readonly isDisplayed: UIEventSource<boolean>; | ||||
|       readonly layerDef: LayerConfig; | ||||
|     }[] | ||||
|   >([]); | ||||
|     public filteredLayers: UIEventSource<{ | ||||
|         readonly isDisplayed: UIEventSource<boolean>; | ||||
|         readonly layerDef: LayerConfig; | ||||
|     }[]> = new UIEventSource<{ | ||||
|         readonly isDisplayed: UIEventSource<boolean>; | ||||
|         readonly layerDef: LayerConfig; | ||||
|     }[]>([]); | ||||
| 
 | ||||
|   /** | ||||
|     /** | ||||
|      The latest element that was selected | ||||
|      */ | ||||
|   public readonly selectedElement = new UIEventSource<any>( | ||||
|     undefined, | ||||
|     "Selected element" | ||||
|   ); | ||||
| 
 | ||||
|   /** | ||||
|    * Keeps track of relations: which way is part of which other way? | ||||
|    * Set by the overpass-updater; used in the metatagging | ||||
|    */ | ||||
|   public readonly knownRelations = new UIEventSource< | ||||
|     Map<string, { role: string; relation: Relation }[]> | ||||
|   >(undefined, "Relation memberships"); | ||||
| 
 | ||||
|   public readonly featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchSearch: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchLayers: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchAddNew: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchIframe: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchMoreQuests: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchShareScreen: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchGeolocation: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchIsDebugging: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; | ||||
|   public readonly featureSwitchApiURL: UIEventSource<string>; | ||||
|   public readonly featureSwitchFilter: UIEventSource<boolean>; | ||||
| 
 | ||||
|   /** | ||||
|    * The map location: currently centered lat, lon and zoom | ||||
|    */ | ||||
|   public readonly locationControl = new UIEventSource<Loc>(undefined); | ||||
|   public backgroundLayer; | ||||
|   public readonly backgroundLayerId: UIEventSource<string>; | ||||
| 
 | ||||
|   /* Last location where a click was registered | ||||
|    */ | ||||
|   public readonly LastClickLocation: UIEventSource<{ | ||||
|     lat: number; | ||||
|     lon: number; | ||||
|   }> = new UIEventSource<{ lat: number; lon: number }>(undefined); | ||||
| 
 | ||||
|   /** | ||||
|    * The location as delivered by the GPS | ||||
|    */ | ||||
|   public currentGPSLocation: UIEventSource<{ | ||||
|     latlng: { lat: number; lng: number }; | ||||
|     accuracy: number; | ||||
|   }> = new UIEventSource<{ | ||||
|     latlng: { lat: number; lng: number }; | ||||
|     accuracy: number; | ||||
|   }>(undefined); | ||||
|   public layoutDefinition: string; | ||||
|   public installedThemes: UIEventSource< | ||||
|     { layout: LayoutConfig; definition: string }[] | ||||
|   >; | ||||
| 
 | ||||
|   public layerControlIsOpened: UIEventSource<boolean> = | ||||
|     QueryParameters.GetQueryParameter( | ||||
|       "layer-control-toggle", | ||||
|       "false", | ||||
|       "Whether or not the layer control is shown" | ||||
|     ).map<boolean>( | ||||
|       (str) => str !== "false", | ||||
|       [], | ||||
|       (b) => "" + b | ||||
|     ); | ||||
| 
 | ||||
|   public FilterIsOpened: UIEventSource<boolean> = | ||||
|     QueryParameters.GetQueryParameter( | ||||
|       "filter-toggle", | ||||
|       "false", | ||||
|       "Whether or not the filter is shown" | ||||
|     ).map<boolean>( | ||||
|       (str) => str !== "false", | ||||
|       [], | ||||
|       (b) => "" + b | ||||
|     ); | ||||
| 
 | ||||
|   public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter( | ||||
|     "tab", | ||||
|     "0", | ||||
|     `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` | ||||
|   ).map<number>( | ||||
|     (str) => (isNaN(Number(str)) ? 0 : Number(str)), | ||||
|     [], | ||||
|     (n) => "" + n | ||||
|   ); | ||||
| 
 | ||||
|   constructor(layoutToUse: LayoutConfig) { | ||||
|     const self = this; | ||||
| 
 | ||||
|     this.layoutToUse.setData(layoutToUse); | ||||
| 
 | ||||
|     // -- Location control initialization
 | ||||
|     { | ||||
|       const zoom = State.asFloat( | ||||
|         QueryParameters.GetQueryParameter( | ||||
|           "z", | ||||
|           "" + (layoutToUse?.startZoom ?? 1), | ||||
|           "The initial/current zoom level" | ||||
|         ).syncWith(LocalStorageSource.Get("zoom")) | ||||
|       ); | ||||
|       const lat = State.asFloat( | ||||
|         QueryParameters.GetQueryParameter( | ||||
|           "lat", | ||||
|           "" + (layoutToUse?.startLat ?? 0), | ||||
|           "The initial/current latitude" | ||||
|         ).syncWith(LocalStorageSource.Get("lat")) | ||||
|       ); | ||||
|       const lon = State.asFloat( | ||||
|         QueryParameters.GetQueryParameter( | ||||
|           "lon", | ||||
|           "" + (layoutToUse?.startLon ?? 0), | ||||
|           "The initial/current longitude of the app" | ||||
|         ).syncWith(LocalStorageSource.Get("lon")) | ||||
|       ); | ||||
| 
 | ||||
|       this.locationControl = new UIEventSource<Loc>({ | ||||
|         zoom: Utils.asFloat(zoom.data), | ||||
|         lat: Utils.asFloat(lat.data), | ||||
|         lon: Utils.asFloat(lon.data), | ||||
|       }).addCallback((latlonz) => { | ||||
|         zoom.setData(latlonz.zoom); | ||||
|         lat.setData(latlonz.lat); | ||||
|         lon.setData(latlonz.lon); | ||||
|       }); | ||||
| 
 | ||||
|       this.layoutToUse.addCallback((layoutToUse) => { | ||||
|         const lcd = self.locationControl.data; | ||||
|         lcd.zoom = lcd.zoom ?? layoutToUse?.startZoom; | ||||
|         lcd.lat = lcd.lat ?? layoutToUse?.startLat; | ||||
|         lcd.lon = lcd.lon ?? layoutToUse?.startLon; | ||||
|         self.locationControl.ping(); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // Helper function to initialize feature switches
 | ||||
|     function featSw( | ||||
|       key: string, | ||||
|       deflt: (layout: LayoutConfig) => boolean, | ||||
|       documentation: string | ||||
|     ): UIEventSource<boolean> { | ||||
|       const queryParameterSource = QueryParameters.GetQueryParameter( | ||||
|         key, | ||||
|     public readonly selectedElement = new UIEventSource<any>( | ||||
|         undefined, | ||||
|         documentation | ||||
|       ); | ||||
|       // I'm so sorry about someone trying to decipher this
 | ||||
| 
 | ||||
|       // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|       return UIEventSource.flatten( | ||||
|         self.layoutToUse.map((layout) => { | ||||
|           const defaultValue = deflt(layout); | ||||
|           const queryParam = QueryParameters.GetQueryParameter( | ||||
|             key, | ||||
|             "" + defaultValue, | ||||
|             documentation | ||||
|           ); | ||||
|           return queryParam.map((str) => | ||||
|             str === undefined ? defaultValue : str !== "false" | ||||
|           ); | ||||
|         }), | ||||
|         [queryParameterSource] | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // Feature switch initialization - not as a function as the UIEventSources are readonly
 | ||||
|     { | ||||
|       this.featureSwitchUserbadge = featSw( | ||||
|         "fs-userbadge", | ||||
|         (layoutToUse) => layoutToUse?.enableUserBadge ?? true, | ||||
|         "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." | ||||
|       ); | ||||
|       this.featureSwitchSearch = featSw( | ||||
|         "fs-search", | ||||
|         (layoutToUse) => layoutToUse?.enableSearch ?? true, | ||||
|         "Disables/Enables the search bar" | ||||
|       ); | ||||
|       this.featureSwitchLayers = featSw( | ||||
|         "fs-layers", | ||||
|         (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|         "Disables/Enables the layer control" | ||||
|       ); | ||||
|       this.featureSwitchFilter = featSw( | ||||
|         "fs-filter", | ||||
|         (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|         "Disables/Enables the filter" | ||||
|       ); | ||||
|       this.featureSwitchAddNew = featSw( | ||||
|         "fs-add-new", | ||||
|         (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, | ||||
|         "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" | ||||
|       ); | ||||
|       this.featureSwitchWelcomeMessage = featSw( | ||||
|         "fs-welcome-message", | ||||
|         () => true, | ||||
|         "Disables/enables the help menu or welcome message" | ||||
|       ); | ||||
|       this.featureSwitchIframe = featSw( | ||||
|         "fs-iframe", | ||||
|         () => false, | ||||
|         "Disables/Enables the iframe-popup" | ||||
|       ); | ||||
|       this.featureSwitchMoreQuests = featSw( | ||||
|         "fs-more-quests", | ||||
|         (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, | ||||
|         "Disables/Enables the 'More Quests'-tab in the welcome message" | ||||
|       ); | ||||
|       this.featureSwitchShareScreen = featSw( | ||||
|         "fs-share-screen", | ||||
|         (layoutToUse) => layoutToUse?.enableShareScreen ?? true, | ||||
|         "Disables/Enables the 'Share-screen'-tab in the welcome message" | ||||
|       ); | ||||
|       this.featureSwitchGeolocation = featSw( | ||||
|         "fs-geolocation", | ||||
|         (layoutToUse) => layoutToUse?.enableGeolocation ?? true, | ||||
|         "Disables/Enables the geolocation button" | ||||
|       ); | ||||
|       this.featureSwitchShowAllQuestions = featSw( | ||||
|         "fs-all-questions", | ||||
|         (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, | ||||
|         "Always show all questions" | ||||
|       ); | ||||
| 
 | ||||
|       this.featureSwitchIsTesting = QueryParameters.GetQueryParameter( | ||||
|         "test", | ||||
|         "false", | ||||
|         "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org" | ||||
|       ).map( | ||||
|         (str) => str === "true", | ||||
|         [], | ||||
|         (b) => "" + b | ||||
|       ); | ||||
| 
 | ||||
|       this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter( | ||||
|         "debug", | ||||
|         "false", | ||||
|         "If true, shows some extra debugging help such as all the available tags on every object" | ||||
|       ).map( | ||||
|         (str) => str === "true", | ||||
|         [], | ||||
|         (b) => "" + b | ||||
|       ); | ||||
| 
 | ||||
|       this.featureSwitchApiURL = QueryParameters.GetQueryParameter( | ||||
|         "backend", | ||||
|         "osm", | ||||
|         "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" | ||||
|       ); | ||||
|     } | ||||
|     { | ||||
|       // Some other feature switches
 | ||||
|       const customCssQP = QueryParameters.GetQueryParameter( | ||||
|         "custom-css", | ||||
|         "", | ||||
|         "If specified, the custom css from the given link will be loaded additionaly" | ||||
|       ); | ||||
|       if (customCssQP.data !== undefined && customCssQP.data !== "") { | ||||
|         Utils.LoadCustomCss(customCssQP.data); | ||||
|       } | ||||
| 
 | ||||
|       this.backgroundLayerId = QueryParameters.GetQueryParameter( | ||||
|         "background", | ||||
|         layoutToUse?.defaultBackgroundId ?? "osm", | ||||
|         "The id of the background layer to start with" | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (Utils.runningFromConsole) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.osmConnection = new OsmConnection( | ||||
|       this.featureSwitchIsTesting.data, | ||||
|       QueryParameters.GetQueryParameter( | ||||
|         "oauth_token", | ||||
|         undefined, | ||||
|         "Used to complete the login" | ||||
|       ), | ||||
|       layoutToUse?.id, | ||||
|       true, | ||||
|       // @ts-ignore
 | ||||
|       this.featureSwitchApiURL.data | ||||
|         "Selected element" | ||||
|     ); | ||||
| 
 | ||||
|     this.allElements = new ElementStorage(); | ||||
|     this.changes = new Changes(); | ||||
|     this.osmApiFeatureSource = new OsmApiFeatureSource(); | ||||
|     /** | ||||
|      * Keeps track of relations: which way is part of which other way? | ||||
|      * Set by the overpass-updater; used in the metatagging | ||||
|      */ | ||||
|     public readonly knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships"); | ||||
| 
 | ||||
|     new PendingChangesUploader(this.changes, this.selectedElement); | ||||
|     public readonly featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchSearch: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchLayers: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchAddNew: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIframe: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchMoreQuests: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShareScreen: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchGeolocation: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchIsDebugging: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchApiURL: UIEventSource<string>; | ||||
|     public readonly featureSwitchFilter: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchEnableExport: UIEventSource<boolean>; | ||||
|     public readonly featureSwitchFakeUser: UIEventSource<boolean>; | ||||
| 
 | ||||
|     this.mangroveIdentity = new MangroveIdentity( | ||||
|       this.osmConnection.GetLongPreference("identity", "mangrove") | ||||
|     ); | ||||
| 
 | ||||
|     this.installedThemes = new InstalledThemes( | ||||
|       this.osmConnection | ||||
|     ).installedThemes; | ||||
|     public featurePipeline: FeaturePipeline; | ||||
| 
 | ||||
|     // Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
 | ||||
|     this.favouriteLayers = LocalStorageSource.Get("favouriteLayers") | ||||
|       .syncWith(this.osmConnection.GetLongPreference("favouriteLayers")) | ||||
|       .map( | ||||
|         (str) => Utils.Dedup(str?.split(";")) ?? [], | ||||
| 
 | ||||
|     /** | ||||
|      * The map location: currently centered lat, lon and zoom | ||||
|      */ | ||||
|     public readonly locationControl = new UIEventSource<Loc>(undefined); | ||||
|     public backgroundLayer; | ||||
|     public readonly backgroundLayerId: UIEventSource<string>; | ||||
| 
 | ||||
|     /* Last location where a click was registered | ||||
|      */ | ||||
|     public readonly LastClickLocation: UIEventSource<{ | ||||
|         lat: number; | ||||
|         lon: number; | ||||
|     }> = new UIEventSource<{ lat: number; lon: number }>(undefined); | ||||
| 
 | ||||
|     /** | ||||
|      * The location as delivered by the GPS | ||||
|      */ | ||||
|     public currentGPSLocation: UIEventSource<{ | ||||
|         latlng: { lat: number; lng: number }; | ||||
|         accuracy: number; | ||||
|     }> = new UIEventSource<{ | ||||
|         latlng: { lat: number; lng: number }; | ||||
|         accuracy: number; | ||||
|     }>(undefined); | ||||
|     public layoutDefinition: string; | ||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||
| 
 | ||||
|     public layerControlIsOpened: UIEventSource<boolean> = | ||||
|         QueryParameters.GetQueryParameter( | ||||
|             "layer-control-toggle", | ||||
|             "false", | ||||
|             "Whether or not the layer control is shown" | ||||
|         ).map<boolean>( | ||||
|             (str) => str !== "false", | ||||
|             [], | ||||
|             (b) => "" + b | ||||
|         ); | ||||
| 
 | ||||
|     public FilterIsOpened: UIEventSource<boolean> = | ||||
|         QueryParameters.GetQueryParameter( | ||||
|             "filter-toggle", | ||||
|             "false", | ||||
|             "Whether or not the filter is shown" | ||||
|         ).map<boolean>( | ||||
|             (str) => str !== "false", | ||||
|             [], | ||||
|             (b) => "" + b | ||||
|         ); | ||||
| 
 | ||||
|     public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter( | ||||
|         "tab", | ||||
|         "0", | ||||
|         `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` | ||||
|     ).map<number>( | ||||
|         (str) => (isNaN(Number(str)) ? 0 : Number(str)), | ||||
|         [], | ||||
|         (layers) => Utils.Dedup(layers)?.join(";") | ||||
|       ); | ||||
| 
 | ||||
|     Locale.language.syncWith(this.osmConnection.GetPreference("language")); | ||||
| 
 | ||||
|     Locale.language | ||||
|       .addCallback((currentLanguage) => { | ||||
|         const layoutToUse = self.layoutToUse.data; | ||||
|         if (layoutToUse === undefined) { | ||||
|           return; | ||||
|         } | ||||
|         if (this.layoutToUse.data.language.indexOf(currentLanguage) < 0) { | ||||
|           console.log( | ||||
|             "Resetting language to", | ||||
|             layoutToUse.language[0], | ||||
|             "as", | ||||
|             currentLanguage, | ||||
|             " is unsupported" | ||||
|           ); | ||||
|           // The current language is not supported -> switch to a supported one
 | ||||
|           Locale.language.setData(layoutToUse.language[0]); | ||||
|         } | ||||
|       }) | ||||
|       .ping(); | ||||
| 
 | ||||
|     new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements); | ||||
|   } | ||||
| 
 | ||||
|   private static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|     return source.map( | ||||
|       (str) => { | ||||
|         let parsed = parseFloat(str); | ||||
|         return isNaN(parsed) ? undefined : parsed; | ||||
|       }, | ||||
|       [], | ||||
|       (fl) => { | ||||
|         if (fl === undefined || isNaN(fl)) { | ||||
|           return undefined; | ||||
|         } | ||||
|         return ("" + fl).substr(0, 8); | ||||
|       } | ||||
|         (n) => "" + n | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|     constructor(layoutToUse: LayoutConfig) { | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.layoutToUse.setData(layoutToUse); | ||||
| 
 | ||||
|         // -- Location control initialization
 | ||||
|         { | ||||
|             const zoom = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "z", | ||||
|                     "" + (layoutToUse?.startZoom ?? 1), | ||||
|                     "The initial/current zoom level" | ||||
|                 ).syncWith(LocalStorageSource.Get("zoom")) | ||||
|             ); | ||||
|             const lat = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "lat", | ||||
|                     "" + (layoutToUse?.startLat ?? 0), | ||||
|                     "The initial/current latitude" | ||||
|                 ).syncWith(LocalStorageSource.Get("lat")) | ||||
|             ); | ||||
|             const lon = State.asFloat( | ||||
|                 QueryParameters.GetQueryParameter( | ||||
|                     "lon", | ||||
|                     "" + (layoutToUse?.startLon ?? 0), | ||||
|                     "The initial/current longitude of the app" | ||||
|                 ).syncWith(LocalStorageSource.Get("lon")) | ||||
|             ); | ||||
| 
 | ||||
|             this.locationControl = new UIEventSource<Loc>({ | ||||
|                 zoom: Utils.asFloat(zoom.data), | ||||
|                 lat: Utils.asFloat(lat.data), | ||||
|                 lon: Utils.asFloat(lon.data), | ||||
|             }).addCallback((latlonz) => { | ||||
|                 zoom.setData(latlonz.zoom); | ||||
|                 lat.setData(latlonz.lat); | ||||
|                 lon.setData(latlonz.lon); | ||||
|             }); | ||||
| 
 | ||||
|             this.layoutToUse.addCallback((layoutToUse) => { | ||||
|                 const lcd = self.locationControl.data; | ||||
|                 lcd.zoom = lcd.zoom ?? layoutToUse?.startZoom; | ||||
|                 lcd.lat = lcd.lat ?? layoutToUse?.startLat; | ||||
|                 lcd.lon = lcd.lon ?? layoutToUse?.startLon; | ||||
|                 self.locationControl.ping(); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         // Helper function to initialize feature switches
 | ||||
|         function featSw( | ||||
|             key: string, | ||||
|             deflt: (layout: LayoutConfig) => boolean, | ||||
|             documentation: string | ||||
|         ): UIEventSource<boolean> { | ||||
|             const queryParameterSource = QueryParameters.GetQueryParameter( | ||||
|                 key, | ||||
|                 undefined, | ||||
|                 documentation | ||||
|             ); | ||||
|             // I'm so sorry about someone trying to decipher this
 | ||||
| 
 | ||||
|             // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
 | ||||
|             return UIEventSource.flatten( | ||||
|                 self.layoutToUse.map((layout) => { | ||||
|                     const defaultValue = deflt(layout); | ||||
|                     const queryParam = QueryParameters.GetQueryParameter( | ||||
|                         key, | ||||
|                         "" + defaultValue, | ||||
|                         documentation | ||||
|                     ); | ||||
|                     return queryParam.map((str) => | ||||
|                         str === undefined ? defaultValue : str !== "false" | ||||
|                     ); | ||||
|                 }), | ||||
|                 [queryParameterSource] | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         // Feature switch initialization - not as a function as the UIEventSources are readonly
 | ||||
|         { | ||||
|             this.featureSwitchUserbadge = featSw( | ||||
|                 "fs-userbadge", | ||||
|                 (layoutToUse) => layoutToUse?.enableUserBadge ?? true, | ||||
|                 "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." | ||||
|             ); | ||||
|             this.featureSwitchSearch = featSw( | ||||
|                 "fs-search", | ||||
|                 (layoutToUse) => layoutToUse?.enableSearch ?? true, | ||||
|                 "Disables/Enables the search bar" | ||||
|             ); | ||||
|             this.featureSwitchLayers = featSw( | ||||
|                 "fs-layers", | ||||
|                 (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|                 "Disables/Enables the layer control" | ||||
|             ); | ||||
|             this.featureSwitchFilter = featSw( | ||||
|                 "fs-filter", | ||||
|                 (layoutToUse) => layoutToUse?.enableLayers ?? true, | ||||
|                 "Disables/Enables the filter" | ||||
|             ); | ||||
|             this.featureSwitchAddNew = featSw( | ||||
|                 "fs-add-new", | ||||
|                 (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, | ||||
|                 "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" | ||||
|             ); | ||||
|             this.featureSwitchWelcomeMessage = featSw( | ||||
|                 "fs-welcome-message", | ||||
|                 () => true, | ||||
|                 "Disables/enables the help menu or welcome message" | ||||
|             ); | ||||
|             this.featureSwitchIframe = featSw( | ||||
|                 "fs-iframe", | ||||
|                 () => false, | ||||
|                 "Disables/Enables the iframe-popup" | ||||
|             ); | ||||
|             this.featureSwitchMoreQuests = featSw( | ||||
|                 "fs-more-quests", | ||||
|                 (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, | ||||
|                 "Disables/Enables the 'More Quests'-tab in the welcome message" | ||||
|             ); | ||||
|             this.featureSwitchShareScreen = featSw( | ||||
|                 "fs-share-screen", | ||||
|                 (layoutToUse) => layoutToUse?.enableShareScreen ?? true, | ||||
|                 "Disables/Enables the 'Share-screen'-tab in the welcome message" | ||||
|             ); | ||||
|             this.featureSwitchGeolocation = featSw( | ||||
|                 "fs-geolocation", | ||||
|                 (layoutToUse) => layoutToUse?.enableGeolocation ?? true, | ||||
|                 "Disables/Enables the geolocation button" | ||||
|             ); | ||||
|             this.featureSwitchShowAllQuestions = featSw( | ||||
|                 "fs-all-questions", | ||||
|                 (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, | ||||
|                 "Always show all questions" | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchIsTesting = QueryParameters.GetQueryParameter( | ||||
|                 "test", | ||||
|                 "false", | ||||
|                 "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org" | ||||
|             ).map( | ||||
|                 (str) => str === "true", | ||||
|                 [], | ||||
|                 (b) => "" + b | ||||
|             ); | ||||
| 
 | ||||
|             this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter( | ||||
|                 "debug", | ||||
|                 "false", | ||||
|                 "If true, shows some extra debugging help such as all the available tags on every object" | ||||
|             ).map( | ||||
|                 (str) => str === "true", | ||||
|                 [], | ||||
|                 (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( | ||||
|                 "backend", | ||||
|                 "osm", | ||||
|                 "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
 | ||||
|             const customCssQP = QueryParameters.GetQueryParameter( | ||||
|                 "custom-css", | ||||
|                 "", | ||||
|                 "If specified, the custom css from the given link will be loaded additionaly" | ||||
|             ); | ||||
|             if (customCssQP.data !== undefined && customCssQP.data !== "") { | ||||
|                 Utils.LoadCustomCss(customCssQP.data); | ||||
|             } | ||||
| 
 | ||||
|             this.backgroundLayerId = QueryParameters.GetQueryParameter( | ||||
|                 "background", | ||||
|                 layoutToUse?.defaultBackgroundId ?? "osm", | ||||
|                 "The id of the background layer to start with" | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.osmConnection = new OsmConnection( | ||||
|             this.featureSwitchIsTesting.data, | ||||
|             this.featureSwitchFakeUser.data, | ||||
|             QueryParameters.GetQueryParameter( | ||||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|             layoutToUse?.id, | ||||
|             true, | ||||
|             // @ts-ignore
 | ||||
|             this.featureSwitchApiURL.data | ||||
|         ); | ||||
| 
 | ||||
|         this.allElements = new ElementStorage(); | ||||
|         this.changes = new Changes(); | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource(); | ||||
| 
 | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement); | ||||
| 
 | ||||
|         this.mangroveIdentity = new MangroveIdentity( | ||||
|             this.osmConnection.GetLongPreference("identity", "mangrove") | ||||
|         ); | ||||
| 
 | ||||
|         this.installedThemes = new InstalledThemes( | ||||
|             this.osmConnection | ||||
|         ).installedThemes; | ||||
| 
 | ||||
|         // Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
 | ||||
|         this.favouriteLayers = LocalStorageSource.Get("favouriteLayers") | ||||
|             .syncWith(this.osmConnection.GetLongPreference("favouriteLayers")) | ||||
|             .map( | ||||
|                 (str) => Utils.Dedup(str?.split(";")) ?? [], | ||||
|                 [], | ||||
|                 (layers) => Utils.Dedup(layers)?.join(";") | ||||
|             ); | ||||
| 
 | ||||
|         Locale.language.syncWith(this.osmConnection.GetPreference("language")); | ||||
| 
 | ||||
|         Locale.language | ||||
|             .addCallback((currentLanguage) => { | ||||
|                 const layoutToUse = self.layoutToUse.data; | ||||
|                 if (layoutToUse === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|                 if (this.layoutToUse.data.language.indexOf(currentLanguage) < 0) { | ||||
|                     console.log( | ||||
|                         "Resetting language to", | ||||
|                         layoutToUse.language[0], | ||||
|                         "as", | ||||
|                         currentLanguage, | ||||
|                         " is unsupported" | ||||
|                     ); | ||||
|                     // The current language is not supported -> switch to a supported one
 | ||||
|                     Locale.language.setData(layoutToUse.language[0]); | ||||
|                 } | ||||
|             }) | ||||
|             .ping(); | ||||
| 
 | ||||
|         new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements); | ||||
|     } | ||||
| 
 | ||||
|     private static asFloat(source: UIEventSource<string>): UIEventSource<number> { | ||||
|         return source.map( | ||||
|             (str) => { | ||||
|                 let parsed = parseFloat(str); | ||||
|                 return isNaN(parsed) ? undefined : parsed; | ||||
|             }, | ||||
|             [], | ||||
|             (fl) => { | ||||
|                 if (fl === undefined || isNaN(fl)) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 return ("" + fl).substr(0, 8); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										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 AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import {Map} from "leaflet"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| export default class Minimap extends BaseUIElement { | ||||
| 
 | ||||
|  | @ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement { | |||
|     private readonly _location: UIEventSource<Loc>; | ||||
|     private _isInited = false; | ||||
|     private _allowMoving: boolean; | ||||
|     private readonly _leafletoptions: any; | ||||
| 
 | ||||
|     constructor(options?: { | ||||
|                     background?: UIEventSource<BaseLayer>, | ||||
|                     location?: UIEventSource<Loc>, | ||||
|                     allowMoving?: boolean | ||||
|                     allowMoving?: boolean, | ||||
|                     leafletOptions?: any | ||||
|                 } | ||||
|     ) { | ||||
|         super() | ||||
|  | @ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement { | |||
|         this._location = options?.location ?? new UIEventSource<Loc>(undefined) | ||||
|         this._id = "minimap" + Minimap._nextId; | ||||
|         this._allowMoving = options.allowMoving ?? true; | ||||
|         this._leafletoptions = options.leafletOptions ?? {} | ||||
|         Minimap._nextId++ | ||||
| 
 | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         const div = document.createElement("div") | ||||
|         div.id = this._id; | ||||
|  | @ -44,7 +48,6 @@ export default class Minimap extends BaseUIElement { | |||
|         const self = this; | ||||
|         // @ts-ignore
 | ||||
|         const resizeObserver = new ResizeObserver(_ => { | ||||
|             console.log("Change in size detected!") | ||||
|             self.InitMap(); | ||||
|             self.leafletMap?.data?.invalidateSize() | ||||
|         }); | ||||
|  | @ -72,8 +75,8 @@ export default class Minimap extends BaseUIElement { | |||
|         const location = this._location; | ||||
| 
 | ||||
|         let currentLayer = this._background.data.layer() | ||||
|         const map = L.map(this._id, { | ||||
|             center: [location.data?.lat ?? 0, location.data?.lon ?? 0], | ||||
|         const options = { | ||||
|             center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0], | ||||
|             zoom: location.data?.zoom ?? 2, | ||||
|             layers: [currentLayer], | ||||
|             zoomControl: false, | ||||
|  | @ -82,8 +85,14 @@ export default class Minimap extends BaseUIElement { | |||
|             scrollWheelZoom: this._allowMoving, | ||||
|             doubleClickZoom: 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( | ||||
|             [[-100, -200], [100, 200]] | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| 
 | ||||
| export class Basemap { | ||||
| 
 | ||||
|  | @ -35,9 +36,8 @@ export class Basemap { | |||
|         ); | ||||
| 
 | ||||
|         this.map.attributionControl.setPrefix( | ||||
|             "<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>"); | ||||
|             "<span id='leaflet-attribution'>A</span>"); | ||||
| 
 | ||||
|         extraAttribution.AttachTo('leaflet-attribution') | ||||
|         const self = this; | ||||
| 
 | ||||
|         currentLayer.addCallbackAndRun(layer => { | ||||
|  | @ -77,6 +77,7 @@ export class Basemap { | |||
|             lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); | ||||
|         }); | ||||
| 
 | ||||
|         extraAttribution.AttachTo('leaflet-attribution') | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 LayerSelection from "./LayerSelection"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import {ExportDataButton} from "./ExportDataButton"; | ||||
| 
 | ||||
| export default class LayerControlPanel extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen { | |||
|         super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown); | ||||
|     } | ||||
| 
 | ||||
|     private static GenTitle():BaseUIElement { | ||||
|     private static GenTitle(): BaseUIElement { | ||||
|         return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2") | ||||
|     } | ||||
| 
 | ||||
|     private static GeneratePanel() : BaseUIElement { | ||||
|         let layerControlPanel: BaseUIElement = new FixedUiElement(""); | ||||
|     private static GeneratePanel(): BaseUIElement { | ||||
|         const elements: BaseUIElement[] = [] | ||||
| 
 | ||||
|         if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { | ||||
|             layerControlPanel = new BackgroundSelector(); | ||||
|             layerControlPanel.SetStyle("margin:1em"); | ||||
|             layerControlPanel.onClick(() => { | ||||
|             const backgroundSelector = new BackgroundSelector(); | ||||
|             backgroundSelector.SetStyle("margin:1em"); | ||||
|             backgroundSelector.onClick(() => { | ||||
|             }); | ||||
|             elements.push(backgroundSelector) | ||||
|         } | ||||
| 
 | ||||
|         if (State.state.filteredLayers.data.length > 1) { | ||||
|             const layerSelection = new LayerSelection(State.state.filteredLayers); | ||||
|             layerSelection.onClick(() => { | ||||
|             }); | ||||
|             layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]); | ||||
|         } | ||||
|         elements.push(new Toggle( | ||||
|             new LayerSelection(State.state.filteredLayers), | ||||
|             undefined, | ||||
|             State.state.filteredLayers.map(layers => layers.length > 1) | ||||
|         )) | ||||
| 
 | ||||
|         return layerControlPanel; | ||||
|         elements.push(new Toggle( | ||||
|             new ExportDataButton(), | ||||
|             undefined, | ||||
|             State.state.featureSwitchEnableExport | ||||
|         )) | ||||
| 
 | ||||
|         return new Combine(elements).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -74,7 +74,6 @@ export default class LayerSelection extends Combine { | |||
|             ); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         super(checkboxes) | ||||
|         this.SetStyle("display:flex;flex-direction:column;") | ||||
| 
 | ||||
|  |  | |||
|  | @ -62,6 +62,10 @@ export default class MoreScreen extends Combine { | |||
|         let officialThemes = AllKnownLayouts.layoutsList | ||||
| 
 | ||||
|         let buttons = officialThemes.map((layout) => { | ||||
|             if(layout === undefined){ | ||||
|                 console.trace("Layout is undefined") | ||||
|                 return undefined | ||||
|             } | ||||
|             const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); | ||||
|             if(layout.id === personal.id){ | ||||
|                 return new VariableUiElement( | ||||
|  |  | |||
|  | @ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | |||
| import Toggle from "../Input/Toggle"; | ||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||
| import {Translation} from "../i18n/Translation"; | ||||
| import LocationInput from "../Input/LocationInput"; | ||||
| import {InputElement} from "../Input/InputElement"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| 
 | ||||
| /* | ||||
| * The SimpleAddUI is a single panel, which can have multiple states: | ||||
|  | @ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation"; | |||
| * - A 'read your unread messages before adding a point' | ||||
|  */ | ||||
| 
 | ||||
| /*private*/ | ||||
| interface PresetInfo { | ||||
|     description: string | Translation, | ||||
|     name: string | BaseUIElement, | ||||
|     icon: BaseUIElement, | ||||
|     icon: () => BaseUIElement, | ||||
|     tags: Tag[], | ||||
|     layerToAddTo: { | ||||
|         layerDef: LayerConfig, | ||||
|         isDisplayed: UIEventSource<boolean> | ||||
|     }, | ||||
|     preciseInput?: { | ||||
|         preferredBackground?: string | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle { | |||
|             new SubtleButton(Svg.envelope_ui(), | ||||
|                 Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}) | ||||
|         ]); | ||||
|          | ||||
|          | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||
|          | ||||
|         function createNewPoint(tags: any[]){ | ||||
|            const loc = State.state.LastClickLocation.data; | ||||
|             let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); | ||||
| 
 | ||||
|         function createNewPoint(tags: any[], location: { lat: number, lon: number }) { | ||||
|             let feature = State.state.changes.createElement(tags, location.lat, location.lon); | ||||
|             State.state.selectedElement.setData(feature); | ||||
|         } | ||||
|          | ||||
| 
 | ||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) | ||||
| 
 | ||||
|         const addUi = new VariableUiElement( | ||||
|  | @ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle { | |||
|                         return presetsOverview | ||||
|                     } | ||||
|                     return SimpleAddUI.CreateConfirmButton(preset, | ||||
|                         tags => { | ||||
|                             createNewPoint(tags) | ||||
|                         (tags, location) => { | ||||
|                             createNewPoint(tags, location) | ||||
|                             selectedPreset.setData(undefined) | ||||
|                         }, () => { | ||||
|                             selectedPreset.setData(undefined) | ||||
|  | @ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle { | |||
|                         addUi, | ||||
|                         State.state.layerUpdater.runningQuery | ||||
|                     ), | ||||
|                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert")                    , | ||||
|                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), | ||||
|                     State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) | ||||
|                 ), | ||||
|                 readYourMessages, | ||||
|  | @ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     private static CreateConfirmButton(preset: PresetInfo, | ||||
|                                        confirm: (tags: any[]) => void,  | ||||
|                                        confirm: (tags: any[], location: { lat: number, lon: number }) => void, | ||||
|                                        cancel: () => void): BaseUIElement { | ||||
| 
 | ||||
|         let location = State.state.LastClickLocation; | ||||
|         let preciseInput: InputElement<Loc> = undefined | ||||
|         if (preset.preciseInput !== undefined) { | ||||
|             const locationSrc = new UIEventSource({ | ||||
|                 lat: location.data.lat, | ||||
|                 lon: location.data.lon, | ||||
|                 zoom: 19 | ||||
|             }); | ||||
|              | ||||
|             let backgroundLayer = undefined; | ||||
|             if(preset.preciseInput.preferredBackground){ | ||||
|                backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) | ||||
|             } | ||||
|              | ||||
|             preciseInput = new LocationInput({ | ||||
|                 mapBackground: backgroundLayer, | ||||
|                 centerLocation:locationSrc | ||||
|                      | ||||
|             }) | ||||
|             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") | ||||
|         } | ||||
| 
 | ||||
|         const confirmButton = new SubtleButton(preset.icon, | ||||
| 
 | ||||
|         let confirmButton: BaseUIElement = new SubtleButton(preset.icon(), | ||||
|             new Combine([ | ||||
|                 Translations.t.general.add.addNew.Subs({category: preset.name}), | ||||
|                 Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") | ||||
|             ]).SetClass("flex flex-col") | ||||
|         ).SetClass("font-bold break-words") | ||||
|             .onClick(() => confirm(preset.tags)); | ||||
|             .onClick(() => { | ||||
|                 confirm(preset.tags, (preciseInput?.GetValue() ?? location).data); | ||||
|             }); | ||||
|          | ||||
|         if (preciseInput !== undefined) { | ||||
|             confirmButton = new Combine([preciseInput, confirmButton]) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const openLayerControl =   | ||||
|         const openLayerControl = | ||||
|             new SubtleButton( | ||||
|                 Svg.layers_ui(), | ||||
|                 new Combine([ | ||||
|  | @ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle { | |||
|                     Translations.t.general.add.openLayerControl | ||||
|                 ]) | ||||
|             ) | ||||
|             | ||||
|             .onClick(() => State.state.layerControlIsOpened.setData(true)) | ||||
|          | ||||
| 
 | ||||
|                 .onClick(() => State.state.layerControlIsOpened.setData(true)) | ||||
| 
 | ||||
|         const openLayerOrConfirm = new Toggle( | ||||
|             confirmButton, | ||||
|             openLayerControl, | ||||
|  | @ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|         const cancelButton = new SubtleButton(Svg.close_ui(), | ||||
|             Translations.t.general.cancel | ||||
|         ).onClick(cancel        ) | ||||
|         ).onClick(cancel) | ||||
| 
 | ||||
|         return new Combine([ | ||||
|             Translations.t.general.add.confirmIntro.Subs({title: preset.name}), | ||||
|             State.state.osmConnection.userDetails.data.dryRun ?  | ||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined           ,  | ||||
|             State.state.osmConnection.userDetails.data.dryRun ? | ||||
|                 Translations.t.general.testing.Clone().SetClass("alert") : undefined, | ||||
|             openLayerOrConfirm, | ||||
|             cancelButton, | ||||
|             preset.description, | ||||
|  | @ -180,11 +212,11 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private static CreatePresetSelectButton(preset: PresetInfo){ | ||||
|     private static CreatePresetSelectButton(preset: PresetInfo) { | ||||
| 
 | ||||
|         const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false); | ||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false); | ||||
|         return new SubtleButton( | ||||
|             preset.icon, | ||||
|             preset.icon(), | ||||
|             new Combine([ | ||||
|                 Translations.t.general.add.addNew.Subs({ | ||||
|                     category: preset.name | ||||
|  | @ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle { | |||
|             ]).SetClass("flex flex-col") | ||||
|         ) | ||||
|     } | ||||
|   | ||||
| /* | ||||
| * Generates the list with all the buttons.*/ | ||||
| 
 | ||||
|     /* | ||||
|     * Generates the list with all the buttons.*/ | ||||
|     private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { | ||||
|         const allButtons = []; | ||||
|         for (const layer of State.state.filteredLayers.data) { | ||||
|              | ||||
|             if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){ | ||||
| 
 | ||||
|             if (layer.isDisplayed.data === false && State.state.featureSwitchLayers) { | ||||
|                 continue; | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|             const presets = layer.layerDef.presets; | ||||
|             for (const preset of presets) { | ||||
| 
 | ||||
|                 const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||
|                 let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html | ||||
|                 let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html | ||||
|                     .SetClass("w-12 h-12 block relative"); | ||||
|                 const presetInfo: PresetInfo = { | ||||
|                     tags: preset.tags, | ||||
|                     layerToAddTo: layer, | ||||
|                     name: preset.title, | ||||
|                     description: preset.description, | ||||
|                     icon: icon | ||||
|                     icon: icon, | ||||
|                     preciseInput: preset.preciseInput | ||||
|                 } | ||||
| 
 | ||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); | ||||
|  |  | |||
|  | @ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> { | |||
|         }) | ||||
| 
 | ||||
|         this.RegisterTriggers(element) | ||||
|         element.style.overflow = "hidden" | ||||
| 
 | ||||
|         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") | ||||
|             block.appendChild(input) | ||||
|             block.appendChild(label) | ||||
|             block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1") | ||||
|             block.classList.add("flex","w-full","border", "rounded-3xl", "border-gray-400","m-1") | ||||
|             wrappers.push(block) | ||||
| 
 | ||||
|             form.appendChild(block) | ||||
|  |  | |||
|  | @ -36,11 +36,11 @@ export class TextField extends InputElement<string> { | |||
|         this.SetClass("form-text-field") | ||||
|         let inputEl: HTMLElement | ||||
|         if (options.htmlType === "area") { | ||||
|             this.SetClass("w-full box-border max-w-full") | ||||
|             const el = document.createElement("textarea") | ||||
|             el.placeholder = placeholder | ||||
|             el.rows = options.textAreaRows | ||||
|             el.cols = 50 | ||||
|             el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box" | ||||
|             inputEl = el; | ||||
|         } else { | ||||
|             const el = document.createElement("input") | ||||
|  |  | |||
|  | @ -13,6 +13,8 @@ import {Utils} from "../../Utils"; | |||
| import Loc from "../../Models/Loc"; | ||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import LengthInput from "./LengthInput"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| 
 | ||||
| interface TextFieldDef { | ||||
|     name: string, | ||||
|  | @ -21,14 +23,16 @@ interface TextFieldDef { | |||
|     reformat?: ((s: string, country?: () => string) => string), | ||||
|     inputHelper?: (value: UIEventSource<string>, options?: { | ||||
|         location: [number, number], | ||||
|         mapBackgroundLayer?: UIEventSource<any> | ||||
|         mapBackgroundLayer?: UIEventSource<any>, | ||||
|         args: (string | number | boolean)[] | ||||
|         feature?: any | ||||
|     }) => InputElement<string>, | ||||
| 
 | ||||
|     inputmode?: string | ||||
| } | ||||
| 
 | ||||
| export default class ValidatedTextField { | ||||
| 
 | ||||
|     public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any | ||||
| 
 | ||||
|     public static tpList: TextFieldDef[] = [ | ||||
|         ValidatedTextField.tp( | ||||
|  | @ -63,6 +67,83 @@ export default class ValidatedTextField { | |||
|                 return [year, month, day].join('-'); | ||||
|             }, | ||||
|             (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( | ||||
|             "wikidata", | ||||
|             "A wikidata identifier, e.g. Q42", | ||||
|  | @ -113,22 +194,6 @@ export default class ValidatedTextField { | |||
|             undefined, | ||||
|             undefined, | ||||
|             "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( | ||||
|             "float", | ||||
|             "A decimal", | ||||
|  | @ -222,6 +287,7 @@ export default class ValidatedTextField { | |||
|      * {string (typename) --> TextFieldDef} | ||||
|      */ | ||||
|     public static AllTypes = ValidatedTextField.allTypesDict(); | ||||
| 
 | ||||
|     public static InputForType(type: string, options?: { | ||||
|         placeholder?: string | BaseUIElement, | ||||
|         value?: UIEventSource<string>, | ||||
|  | @ -233,7 +299,9 @@ export default class ValidatedTextField { | |||
|         country?: () => string, | ||||
|         location?: [number /*lat*/, number /*lon*/], | ||||
|         mapBackgroundLayer?: UIEventSource<any>, | ||||
|         unit?: Unit | ||||
|         unit?: Unit, | ||||
|         args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
 | ||||
|         feature?: any | ||||
|     }): InputElement<string> { | ||||
|         options = options ?? {}; | ||||
|         options.placeholder = options.placeholder ?? type; | ||||
|  | @ -247,7 +315,7 @@ export default class ValidatedTextField { | |||
|                 if (str === undefined) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 if(options.unit) { | ||||
|                 if (options.unit) { | ||||
|                     str = options.unit.stripUnitParts(str) | ||||
|                 } | ||||
|                 return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); | ||||
|  | @ -268,7 +336,7 @@ export default class ValidatedTextField { | |||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if(options.unit) { | ||||
|         if (options.unit) { | ||||
|             // We need to apply a unit.
 | ||||
|             // This implies:
 | ||||
|             // We have to create a dropdown with applicable denominations, and fuse those values
 | ||||
|  | @ -282,23 +350,22 @@ export default class ValidatedTextField { | |||
|                 }) | ||||
|             ) | ||||
|             unitDropDown.GetValue().setData(unit.defaultDenom) | ||||
|             unitDropDown.SetStyle("width: min-content") | ||||
|             unitDropDown.SetClass("w-min") | ||||
| 
 | ||||
|             input = new CombinedInputElement( | ||||
|                 input, | ||||
|                 unitDropDown, | ||||
|                 // combine the value from the textfield and the dropdown into the resulting value that should go into OSM
 | ||||
|                 (text, denom) => denom?.canonicalValue(text, true) ?? undefined,  | ||||
|                 (text, denom) => denom?.canonicalValue(text, true) ?? undefined, | ||||
|                 (valueWithDenom: string) => { | ||||
|                     // Take the value from OSM and feed it into the textfield and the dropdown
 | ||||
|                     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
 | ||||
|                         return [undefined, undefined] | ||||
|                     } | ||||
|                     const [strippedText, denom] = withDenom | ||||
|                     if(strippedText === undefined){ | ||||
|                     if (strippedText === undefined) { | ||||
|                         return [undefined, undefined] | ||||
|                     } | ||||
|                     return [strippedText, denom] | ||||
|  | @ -306,18 +373,20 @@ export default class ValidatedTextField { | |||
|             ).SetClass("flex") | ||||
|         } | ||||
|         if (tp.inputHelper) { | ||||
|             const helper =  tp.inputHelper(input.GetValue(), { | ||||
|             const helper = tp.inputHelper(input.GetValue(), { | ||||
|                 location: options.location, | ||||
|                 mapBackgroundLayer: options.mapBackgroundLayer | ||||
| 
 | ||||
|                 mapBackgroundLayer: options.mapBackgroundLayer, | ||||
|                 args: options.args, | ||||
|                 feature: options.feature | ||||
|             }) | ||||
|             input = new CombinedInputElement(input, helper, | ||||
|                 (a, _) => a, // We can ignore b, as they are linked earlier
 | ||||
|                 a => [a, a] | ||||
|                 ); | ||||
|             ); | ||||
|         } | ||||
|         return input; | ||||
|     } | ||||
| 
 | ||||
|     public static HelpText(): string { | ||||
|         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 | ||||
|  | @ -329,7 +398,9 @@ export default class ValidatedTextField { | |||
|                       reformat?: ((s: string, country?: () => string) => string), | ||||
|                       inputHelper?: (value: UIEventSource<string>, options?: { | ||||
|                           location: [number, number], | ||||
|                           mapBackgroundLayer: UIEventSource<any> | ||||
|                           mapBackgroundLayer: UIEventSource<any>, | ||||
|                           args: string[], | ||||
|                           feature: any | ||||
|                       }) => InputElement<string>, | ||||
|                       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"); | ||||
|         const titleIcons = new Combine( | ||||
|             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") | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement { | |||
|             throw "Trying to generate a tagRenderingAnswer without configuration..." | ||||
|         } | ||||
|         super(tagsSource.map(tags => { | ||||
|             if(tags === undefined){ | ||||
|             if (tags === undefined) { | ||||
|                 return undefined; | ||||
|             } | ||||
|              | ||||
|             if(configuration.condition){ | ||||
|                 if(!configuration.condition.matchesProperties(tags)){ | ||||
| 
 | ||||
|             if (configuration.condition) { | ||||
|                 if (!configuration.condition.matchesProperties(tags)) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             const trs = Utils.NoNull(configuration.GetRenderValues(tags)); | ||||
|             if(trs.length === 0){ | ||||
|                 return  undefined; | ||||
|             } | ||||
|             | ||||
|            const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) | ||||
|            if(valuesToRender.length === 1){ | ||||
|               return valuesToRender[0]; | ||||
|            }else if(valuesToRender.length > 1){ | ||||
|                return new List(valuesToRender) | ||||
|            } | ||||
|            return undefined; | ||||
|                 }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) | ||||
| 
 | ||||
|         this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") | ||||
|             const trs = Utils.NoNull(configuration.GetRenderValues(tags)); | ||||
|             if (trs.length === 0) { | ||||
|                 return undefined; | ||||
|             } | ||||
| 
 | ||||
|             const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) | ||||
|             if (valuesToRender.length === 1) { | ||||
|                 return valuesToRender[0]; | ||||
|             } else if (valuesToRender.length > 1) { | ||||
|                 return new List(valuesToRender) | ||||
|             } | ||||
|             return undefined; | ||||
|         }).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) | ||||
| 
 | ||||
|         this.SetClass("flex items-center flex-row text-lg link-underline") | ||||
|         this.SetStyle("word-wrap: anywhere;"); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {DropDown} from "../Input/DropDown"; | ||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | ||||
| import InputElementWrapper from "../Input/InputElementWrapper"; | ||||
| 
 | ||||
| /** | ||||
|  * Shows the question element. | ||||
|  | @ -128,7 +129,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|             } | ||||
|             return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined:  m.ifnot)) | ||||
|         } | ||||
|         const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data); | ||||
|         const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource); | ||||
|         const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 | ||||
| 
 | ||||
|         if (mappings.length < 8 || configuration.multiAnswer || hasImages) { | ||||
|  | @ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|             (t0, t1) => t1.isEquivalent(t0)); | ||||
|     } | ||||
| 
 | ||||
|     private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> { | ||||
|     private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> { | ||||
|         const freeform = configuration.freeform; | ||||
|         if (freeform === undefined) { | ||||
|             return undefined; | ||||
|  | @ -328,20 +329,34 @@ export default class TagRenderingQuestion extends Combine { | |||
|             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), | ||||
|             country: () => tagsData._country, | ||||
|             location: [tagsData._lat, tagsData._lon], | ||||
|             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), | ||||
|             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) { | ||||
|                 try { | ||||
| 
 | ||||
|                     mp.fitBounds(geoLayer.getBounds()) | ||||
| 
 | ||||
|                     mp.fitBounds(geoLayer.getBounds(), {animate: false}) | ||||
|                 } catch (e) { | ||||
|                     console.error(e) | ||||
|                 } | ||||
|  | @ -148,7 +146,9 @@ export default class ShowDataLayer { | |||
|         const popup = L.popup({ | ||||
|             autoPan: true, | ||||
|             closeOnEscapeKey: true, | ||||
|             closeButton: false | ||||
|             closeButton: false, | ||||
|             autoPanPaddingTopLeft: [15,15], | ||||
|              | ||||
|         }, leafletLayer); | ||||
| 
 | ||||
|         leafletLayer.bindPopup(popup); | ||||
|  |  | |||
|  | @ -39,7 +39,8 @@ export default class SpecialVisualizations { | |||
|     static constructMiniMap: (options?: { | ||||
|         background?: UIEventSource<BaseLayer>, | ||||
|         location?: UIEventSource<Loc>, | ||||
|         allowMoving?: boolean | ||||
|         allowMoving?: boolean, | ||||
|         leafletOptions?: any | ||||
|     }) => BaseUIElement; | ||||
|     static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any; | ||||
|     public static specialVisualizations: SpecialVisualization[] = | ||||
|  | @ -369,7 +370,6 @@ export default class SpecialVisualizations { | |||
|                                 if (unit === undefined) { | ||||
|                                     return value; | ||||
|                                 } | ||||
| 
 | ||||
|                                 return unit.asHumanLongValue(value); | ||||
| 
 | ||||
|                             }, | ||||
|  | @ -379,6 +379,7 @@ export default class SpecialVisualizations { | |||
|             } | ||||
| 
 | ||||
|         ] | ||||
|      | ||||
|     static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); | ||||
|     private static GenHelpMessage() { | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio | |||
| import {Utils} from "../Utils"; | ||||
| import {VariableUiElement} from "./Base/VariableUIElement"; | ||||
| import Combine from "./Base/Combine"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
| 
 | ||||
| export class SubstitutedTranslation extends VariableUiElement { | ||||
| 
 | ||||
|     public constructor( | ||||
|         translation: Translation, | ||||
|         tagsSource: UIEventSource<any>) { | ||||
|         tagsSource: UIEventSource<any>, | ||||
|         mapping: Map<string, BaseUIElement> = undefined) { | ||||
| 
 | ||||
|         const extraMappings: SpecialVisualization[] = []; | ||||
| 
 | ||||
|         mapping?.forEach((value, key) => { | ||||
|             console.log("KV:", key, value) | ||||
|             extraMappings.push( | ||||
|                 { | ||||
|                     funcName: key, | ||||
|                     constr: (() => { | ||||
|                         return value | ||||
|                     }), | ||||
|                     docs: "Dynamically injected input element", | ||||
|                     args: [], | ||||
|                     example: "" | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         super( | ||||
|             Locale.language.map(language => { | ||||
|                 const txt = translation.textFor(language) | ||||
|                 let txt = translation.textFor(language); | ||||
|                 if (txt === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map( | ||||
|                 mapping?.forEach((_, key) => { | ||||
|                     txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) | ||||
|                 }) | ||||
| 
 | ||||
|                 return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( | ||||
|                     proto => { | ||||
|                         if (proto.fixed !== undefined) { | ||||
|                             return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); | ||||
|  | @ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|             }) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         this.SetClass("w-full") | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static ExtractSpecialComponents(template: string): { | ||||
|         fixed?: string, special?: { | ||||
|     public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): { | ||||
|         fixed?: string, | ||||
|         special?: { | ||||
|             func: SpecialVisualization, | ||||
|             args: string[], | ||||
|             style: string | ||||
|         } | ||||
|     }[] { | ||||
| 
 | ||||
|         for (const knownSpecial of SpecialVisualizations.specialVisualizations) { | ||||
|         if (extraMappings.length > 0) { | ||||
| 
 | ||||
|             console.log("Extra mappings are", extraMappings) | ||||
|         } | ||||
| 
 | ||||
|         for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { | ||||
| 
 | ||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||
|             const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); | ||||
|             if (matched != null) { | ||||
| 
 | ||||
|                 // We found a special component that should be brought to live
 | ||||
|                 const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]); | ||||
|                 const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings); | ||||
|                 const argument = matched[2].trim(); | ||||
|                 const style = matched[3]?.substring(1) ?? "" | ||||
|                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]); | ||||
|                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings); | ||||
|                 const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); | ||||
|                 if (argument.length > 0) { | ||||
|                     const realArgs = argument.split(",").map(str => str.trim()); | ||||
|  | @ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|                 } | ||||
| 
 | ||||
|                 let element; | ||||
|                 element =  {special:{ | ||||
|                     args: args, | ||||
|                     style: style, | ||||
|                     func: knownSpecial | ||||
|                 }} | ||||
|                 element = { | ||||
|                     special: { | ||||
|                         args: args, | ||||
|                         style: style, | ||||
|                         func: knownSpecial | ||||
|                     } | ||||
|                 } | ||||
|                 return [...partBefore, element, ...partAfter] | ||||
|             } | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										19
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -1,4 +1,5 @@ | |||
| import * as colors from "./assets/colors.json" | ||||
| import {TileRange} from "./Models/TileRange"; | ||||
| 
 | ||||
| export class Utils { | ||||
| 
 | ||||
|  | @ -134,7 +135,7 @@ export class Utils { | |||
|         } | ||||
|         return newArr; | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     public static MergeTags(a: any, b: any) { | ||||
|         const t = {}; | ||||
|         for (const k in a) { | ||||
|  | @ -450,14 +451,12 @@ export class Utils { | |||
|             b: parseInt(hex.substr(5, 2), 16), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static setDefaults(options, defaults){ | ||||
|         for (let key in defaults){ | ||||
|             if (!(key in options)) options[key] = defaults[key]; | ||||
|         } | ||||
|         return options; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface TileRange { | ||||
|     xstart: number, | ||||
|     ystart: number, | ||||
|     xend: number, | ||||
|     yend: number, | ||||
|     total: number, | ||||
|     zoomlevel: number | ||||
| 
 | ||||
| } | ||||
|  | @ -73,7 +73,10 @@ | |||
|       }, | ||||
|       "tags": [ | ||||
|         "amenity=public_bookcase" | ||||
|       ] | ||||
|       ], | ||||
|       "preciseInput": { | ||||
|         "preferredBackground": "photo" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "tagRenderings": [ | ||||
|  | @ -139,7 +142,8 @@ | |||
|       }, | ||||
|       "freeform": { | ||||
|         "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 | 
|  | @ -638,5 +638,611 @@ | |||
|     "path": "arrow-left-thin.svg", | ||||
|     "license": "CC0", | ||||
|     "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": { | ||||
|     "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>", | ||||
|     "condition": "wikipedia~*", | ||||
|     "condition": { | ||||
|       "or": [ | ||||
|         "wikipedia~*", | ||||
|         "wikidata~*" | ||||
|       ] | ||||
|     }, | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": { | ||||
|           "and": [ | ||||
|             "wikipedia=", | ||||
|             "wikidata~*" | ||||
|           ] | ||||
|         }, | ||||
|         "if": "wikipedia=", | ||||
|         "then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>" | ||||
|       } | ||||
|     ] | ||||
|  | @ -59,8 +59,12 @@ | |||
|     "render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>", | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": "id~=-", | ||||
|         "then": "<span class='alert'>Uploading...</alert>" | ||||
|         "if": "id~.*/-.*", | ||||
|         "then": "" | ||||
|       }, | ||||
|       { | ||||
|         "if": "_backend~*", | ||||
|         "then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>" | ||||
|       } | ||||
|     ], | ||||
|     "condition": "id~(node|way|relation)/[0-9]*" | ||||
|  |  | |||
|  | @ -736,7 +736,7 @@ | |||
|         "_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)", | ||||
|         "_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])", | ||||
|         "_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])", | ||||
|         "_contained_climbing_routes_count=JSON.parse(_contained_climbing_routes).length" | ||||
|         "_contained_climbing_routes_count=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').length" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|  | @ -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_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]", | ||||
|       "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'", | ||||
|       "_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock", | ||||
|       "_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id", | ||||
|       "_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock", | ||||
|       "_embedding_features_with_rock:id=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.id", | ||||
|       "_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access", | ||||
|       "_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']", | ||||
|       "_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id" | ||||
|  |  | |||
|  | @ -27,9 +27,8 @@ | |||
|   "defaultBackgroundId": "CartoDB.Positron", | ||||
|   "layers": [ | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "nature_reserve" | ||||
|       ], | ||||
|       "#": "Nature reserve with geometry, z>=13", | ||||
|       "builtin": "nature_reserve", | ||||
|       "override": { | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|  | @ -41,16 +40,32 @@ | |||
|           "geoJsonZoomLevel": 12, | ||||
|           "isOsmCache": true | ||||
|         }, | ||||
|         "minzoom": "10", | ||||
|         "minzoom": "13", | ||||
|         "icon": { | ||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "visitor_information_centre" | ||||
|       ], | ||||
|       "#": "Nature reserve overview from cache, points only, z < 13", | ||||
|       "builtin": "nature_reserve", | ||||
|       "override": { | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|             "+and": [ | ||||
|               "operator~.*[nN]atuurpunt.*" | ||||
|             ] | ||||
|           }, | ||||
|           "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson" | ||||
|         }, | ||||
|         "minzoom": "0", | ||||
|         "icon": { | ||||
|           "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": "visitor_information_centre", | ||||
|       "override": { | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|  | @ -69,9 +84,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "trail" | ||||
|       ], | ||||
|       "builtin": "trail", | ||||
|       "override": { | ||||
|         "source": { | ||||
|           "osmTags": { | ||||
|  | @ -100,9 +113,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "toilet" | ||||
|       ], | ||||
|       "builtin": "toilet", | ||||
|       "override": { | ||||
|         "minzoom": "15", | ||||
|         "source": { | ||||
|  | @ -126,9 +137,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "birdhide" | ||||
|       ], | ||||
|       "builtin": "birdhide", | ||||
|       "override": { | ||||
|         "minzoom": "15", | ||||
|         "source": { | ||||
|  | @ -154,9 +163,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "picnic_table" | ||||
|       ], | ||||
|       "builtin": "picnic_table", | ||||
|       "override": { | ||||
|         "minzoom": "16", | ||||
|         "source": { | ||||
|  | @ -170,9 +177,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "drinking_water" | ||||
|       ], | ||||
|       "builtin": "drinking_water", | ||||
|       "override": { | ||||
|         "minzoom": "16", | ||||
|         "source": { | ||||
|  | @ -186,9 +191,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "parking" | ||||
|       ], | ||||
|       "builtin": "parking", | ||||
|       "override": { | ||||
|         "minzoom": "16", | ||||
|         "icon": { | ||||
|  | @ -215,9 +218,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "information_board" | ||||
|       ], | ||||
|       "builtin": "information_board", | ||||
|       "override": { | ||||
|         "minzoom": "16", | ||||
|         "source": { | ||||
|  | @ -231,9 +232,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "bench" | ||||
|       ], | ||||
|       "builtin": "bench", | ||||
|       "override": { | ||||
|         "minzoom": "18", | ||||
|         "source": { | ||||
|  | @ -247,9 +246,7 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "builtin": [ | ||||
|         "watermill" | ||||
|       ], | ||||
|       "builtin": "watermill", | ||||
|       "override": { | ||||
|         "minzoom": "18", | ||||
|         "source": { | ||||
|  |  | |||
|  | @ -62,7 +62,8 @@ | |||
|             "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "generator:output:electricity" | ||||
|             "key": "generator:output:electricity", | ||||
|             "type": "pfloat" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|  | @ -85,7 +86,7 @@ | |||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "height", | ||||
|             "type": "float" | ||||
|             "type": "pfloat" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|  | @ -179,6 +180,24 @@ | |||
|         } | ||||
|       ], | ||||
|       "eraseInvalidValues": true | ||||
|     }, | ||||
|     { | ||||
|       "appliesToKey": [ | ||||
|         "height", | ||||
|         "rotor:diameter" | ||||
|       ], | ||||
|       "applicableUnits": [ | ||||
|         { | ||||
|           "canonicalDenomination": "m", | ||||
|           "alternativeDenomination": [ | ||||
|             "meter" | ||||
|           ], | ||||
|           "human": { | ||||
|             "en": " meter", | ||||
|             "nl": " meter" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "defaultBackgroundId": "CartoDB.Voyager" | ||||
|  |  | |||
|  | @ -105,11 +105,31 @@ | |||
|     { | ||||
|       "builtin": "slow_roads", | ||||
|       "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": [ | ||||
|           "_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': ''" | ||||
|         ], | ||||
|         "minzoom": 9, | ||||
|         "minzoom": 18, | ||||
|         "source": { | ||||
|           "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", | ||||
|  |  | |||
|  | @ -64,7 +64,13 @@ | |||
|       }, | ||||
|       "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:", | ||||
|  |  | |||
|  | @ -82,6 +82,10 @@ html, body { | |||
|     box-sizing: initial !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-control-attribution { | ||||
|     display: block ruby; | ||||
| } | ||||
| 
 | ||||
| svg, img { | ||||
|     box-sizing: content-box; | ||||
|     width: 100%; | ||||
|  | @ -101,6 +105,10 @@ a { | |||
|     width: min-content; | ||||
| } | ||||
| 
 | ||||
| .w-16-imp { | ||||
|     width: 4rem !important; | ||||
| } | ||||
| 
 | ||||
| .space-between{ | ||||
|     justify-content: space-between; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										3
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput"; | |||
| import SpecialVisualizations from "./UI/SpecialVisualizations"; | ||||
| import ShowDataLayer from "./UI/ShowDataLayer"; | ||||
| 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
 | ||||
| SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); | ||||
| DirectionInput.constructMinimap = options =>  new Minimap(options) | ||||
| ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)  | ||||
| SpecialVisualizations.constructMiniMap = options => new Minimap(options) | ||||
| SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, | ||||
|                                                  leafletMap: UIEventSource<L.Map>, | ||||
|  |  | |||
|  | @ -149,6 +149,10 @@ | |||
|       "zoomInToSeeThisLayer": "Zoom in to see this layer", | ||||
|       "title": "Select layers" | ||||
|     }, | ||||
|     "download": { | ||||
|       "downloadGeojson": "Download visible data as geojson", | ||||
|       "licenseInfo": "<h3>Copyright notice</h3>The provided is available under ODbL. Reusing this data is free for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b></li><li>Any change to this data must be republished under the same license</li></ul>. Please see the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details" | ||||
|     }, | ||||
|     "weekdays": { | ||||
|       "abbreviations": { | ||||
|         "monday": "Mon", | ||||
|  |  | |||
|  | @ -487,6 +487,11 @@ | |||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "presets": { | ||||
|             "0": { | ||||
|                 "title": "Обслуживание велосипедов/магазин" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "defibrillator": { | ||||
|  | @ -1064,6 +1069,7 @@ | |||
|             "1": { | ||||
|                 "question": "Вы хотите добавить описание?" | ||||
|             } | ||||
|         } | ||||
|         }, | ||||
|         "name": "Смотровая площадка" | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -122,8 +122,10 @@ | |||
|             "thanksForSharing": "Obrigado por compartilhar!", | ||||
|             "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.", | ||||
|             "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": { | ||||
|         "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!", | ||||
|         "name_required": "É necessário um nome para exibir e criar comentários", | ||||
|         "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": { | ||||
|         "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": { | ||||
|             "question": "Was sind die Öffnungszeiten von {name}?", | ||||
|             "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": { | ||||
|             "question": "Какое время работы у {name}?", | ||||
|             "render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}" | ||||
|         }, | ||||
|         "level": { | ||||
|             "mappings": { | ||||
|                 "2": { | ||||
|                     "then": "Расположено на первом этаже" | ||||
|                 }, | ||||
|                 "1": { | ||||
|                     "then": "Расположено на первом этаже" | ||||
|                 }, | ||||
|                 "0": { | ||||
|                     "then": "Расположено под землей" | ||||
|                 } | ||||
|             }, | ||||
|             "render": "Расположено на {level}ом этаже" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -1148,6 +1148,13 @@ | |||
|                         "human": " gigawatts" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "1": { | ||||
|                 "applicableUnits": { | ||||
|                     "0": { | ||||
|                         "human": " meter" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  |  | |||
|  | @ -956,6 +956,13 @@ | |||
|                         "human": " gigawatt" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "1": { | ||||
|                 "applicableUnits": { | ||||
|                     "0": { | ||||
|                         "human": " meter" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  |  | |||
							
								
								
									
										14790
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14790
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -8,7 +8,7 @@ | |||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||
|     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", | ||||
|     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", | ||||
|     "test": "ts-node test/TestAll.ts", | ||||
|     "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", | ||||
|     "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", | ||||
|  | @ -20,7 +20,7 @@ | |||
|     "generate:layouts": "ts-node scripts/generateLayouts.ts", | ||||
|     "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", | ||||
|     "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", | ||||
|     "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4", | ||||
|     "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre", | ||||
|     "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", | ||||
|     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", | ||||
|     "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement"; | |||
| 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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| /** | ||||
|  * Generates a collection of geojson files based on an overpass query for a given theme | ||||
|  */ | ||||
| import {TileRange, Utils} from "../Utils"; | ||||
| import {Utils} from "../Utils"; | ||||
| 
 | ||||
| Utils.runningFromConsole = true | ||||
| import {Overpass} from "../Logic/Osm/Overpass"; | ||||
|  | @ -17,6 +17,8 @@ import MetaTagging from "../Logic/MetaTagging"; | |||
| import LayerConfig from "../Customizations/JSON/LayerConfig"; | ||||
| import {GeoOperations} from "../Logic/GeoOperations"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import * as fs from "fs"; | ||||
| import {TileRange} from "../Models/TileRange"; | ||||
| 
 | ||||
| 
 | ||||
| function createOverpassObject(theme: LayoutConfig) { | ||||
|  | @ -139,7 +141,7 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { | |||
|     return allFeatures; | ||||
| } | ||||
| 
 | ||||
| async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { | ||||
| function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { | ||||
|     let processed = 0; | ||||
|     const layerIndex = theme.LayerIndex(); | ||||
|     for (let x = r.xstart; x <= r.xend; x++) { | ||||
|  | @ -211,8 +213,9 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, | |||
|     } | ||||
| } | ||||
| 
 | ||||
| async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | ||||
| function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { | ||||
|     const z = r.zoomlevel; | ||||
|     const generated = {} // layer --> x --> y[]
 | ||||
|     for (let x = r.xstart; x <= r.xend; x++) { | ||||
|         for (let y = r.ystart; y <= r.yend; y++) { | ||||
|             const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") | ||||
|  | @ -227,10 +230,8 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi | |||
|                     .filter(f => f._matching_layer_id === layer.id) | ||||
|                     .filter(f => { | ||||
|                         const isShown = layer.isShown.GetRenderValue(f.properties).txt | ||||
|                         if (isShown === "no") { | ||||
|                             return false; | ||||
|                         } | ||||
|                         return true; | ||||
|                         return isShown !== "no"; | ||||
| 
 | ||||
|                     }) | ||||
|                 const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); | ||||
|                 console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") | ||||
|  | @ -239,18 +240,66 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi | |||
|                     continue; | ||||
|                 } | ||||
|                 writeFileSync(new_path, JSON.stringify(geojson, null, " ")) | ||||
| 
 | ||||
|                 if (generated[layer.id] === undefined) { | ||||
|                     generated[layer.id] = {} | ||||
|                 } | ||||
|                 if (generated[layer.id][x] === undefined) { | ||||
|                     generated[layer.id][x] = [] | ||||
|                 } | ||||
|                 generated[layer.id][x].push(y) | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     for (const layer of theme.layers) { | ||||
|         const id = layer.id | ||||
|         const loaded = generated[id] | ||||
|         if(loaded === undefined){ | ||||
|             console.log("No features loaded for layer ",id) | ||||
|             continue; | ||||
|         } | ||||
|         writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) { | ||||
|     const allFeatures = [] | ||||
|     for (let x = r.xstart; x <= r.xend; x++) { | ||||
|         for (let y = r.ystart; y <= r.yend; y++) { | ||||
|             const read_path = geoJsonName(targetdir + "_" + layername, x, y, z); | ||||
|             if (!fs.existsSync(read_path)) { | ||||
|                 continue; | ||||
|             } | ||||
|             const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features | ||||
|             const pointsOnly = features.map(f => { | ||||
|                  | ||||
|                 f.properties["_last_edit:timestamp"] = "1970-01-01" | ||||
|                  | ||||
|                 if (f.geometry.type === "Point") { | ||||
|                     return f | ||||
|                 } else { | ||||
|                     return GeoOperations.centerpoint(f) | ||||
|                 } | ||||
| 
 | ||||
|             }) | ||||
|             allFeatures.push(...pointsOnly) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const geojson = { | ||||
|         "type": "FeatureCollection", | ||||
|         "features": allFeatures | ||||
|     } | ||||
|     writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " ")) | ||||
| } | ||||
| 
 | ||||
| async function main(args: string[]) { | ||||
| 
 | ||||
|     if (args.length == 0) { | ||||
|         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1") | ||||
|         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]") | ||||
|         return; | ||||
|     } | ||||
|     const themeName = args[0] | ||||
|  | @ -285,8 +334,18 @@ async function main(args: string[]) { | |||
|     } while (failed > 0) | ||||
| 
 | ||||
|     const extraFeatures = await downloadExtraData(theme); | ||||
|     await postProcess(targetdir, tileRange, theme, extraFeatures) | ||||
|     await splitPerLayer(targetdir, tileRange, theme) | ||||
|     postProcess(targetdir, tileRange, theme, extraFeatures) | ||||
|     splitPerLayer(targetdir, tileRange, theme) | ||||
| 
 | ||||
|     if (args[7] === "--generate-point-overview") { | ||||
|         const targetLayers = args[8].split(",") | ||||
|         for (const targetLayer of targetLayers) { | ||||
|             if (!theme.layers.some(l => l.id === targetLayer)) { | ||||
|                 throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",") | ||||
|             } | ||||
|             createOverview(targetdir, tileRange, zoomlevel, targetLayer) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import LayerConfig from "../Customizations/JSON/LayerConfig"; | |||
| import * as licenses from "../assets/generated/license_info.json" | ||||
| import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | ||||
| import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; | ||||
| import {Translation} from "../UI/i18n/Translation"; | ||||
| import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; | ||||
| import AllKnownLayers from "../Customizations/AllKnownLayers"; | ||||
| 
 | ||||
|  | @ -77,63 +76,6 @@ class LayerOverviewUtils { | |||
|         return errorCount | ||||
|     } | ||||
| 
 | ||||
|     validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) { | ||||
|         const missingTranlations = [] | ||||
|         const translations: { tr: Translation, context: string }[] = []; | ||||
|         const queue: { object: any, context: string }[] = [{object: object, context: context}] | ||||
| 
 | ||||
|         while (queue.length > 0) { | ||||
|             const item = queue.pop(); | ||||
|             const o = item.object | ||||
|             for (const key in o) { | ||||
|                 const v = o[key]; | ||||
|                 if (v === undefined) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (v instanceof Translation || v?.translations !== undefined) { | ||||
|                     translations.push({tr: v, context: item.context}); | ||||
|                 } else if ( | ||||
|                     ["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) { | ||||
|                     queue.push({object: v, context: item.context + "." + key}) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const missing = {} | ||||
|         const present = {} | ||||
|         for (const ln of expectedLanguages) { | ||||
|             missing[ln] = 0; | ||||
|             present[ln] = 0; | ||||
|             for (const translation of translations) { | ||||
|                 if (translation.tr.translations["*"] !== undefined) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 const txt = translation.tr.translations[ln]; | ||||
|                 const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0; | ||||
|                 if (isMissing) { | ||||
|                     missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`) | ||||
|                     missing[ln]++ | ||||
|                 } else { | ||||
|                     present[ln]++; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let message = `Translation completeness for ${context}` | ||||
|         let isComplete = true; | ||||
|         for (const ln of expectedLanguages) { | ||||
|             const amiss = missing[ln]; | ||||
|             const ok = present[ln]; | ||||
|             const total = amiss + ok; | ||||
|             message += ` ${ln}: ${ok}/${total}` | ||||
|             if (ok !== total) { | ||||
|                 isComplete = false; | ||||
|             } | ||||
|         } | ||||
|         return missingTranlations | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     main(args: string[]) { | ||||
| 
 | ||||
|         const lt = this.loadThemesAndLayers(); | ||||
|  | @ -160,7 +102,6 @@ class LayerOverviewUtils { | |||
|         } | ||||
| 
 | ||||
|         let themeErrorCount = [] | ||||
|         let missingTranslations = [] | ||||
|         for (const themeFile of themeFiles) { | ||||
|             if (typeof themeFile.language === "string") { | ||||
|                 themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") | ||||
|  | @ -169,10 +110,6 @@ class LayerOverviewUtils { | |||
|                 if (typeof layer === "string") { | ||||
|                     if (!knownLayerIds.has(layer)) { | ||||
|                         themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) | ||||
|                     } else { | ||||
|                         const layerConfig = knownLayerIds.get(layer); | ||||
|                         missingTranslations.push(...this.validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer)) | ||||
| 
 | ||||
|                     } | ||||
|                 } else if (layer.builtin !== undefined) { | ||||
|                     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 => l.builtin === undefined) | ||||
| 
 | ||||
|             missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id)) | ||||
| 
 | ||||
|             try { | ||||
|                 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) { | ||||
|             console.log("All good!") | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										35
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -7,6 +7,11 @@ import {UIEventSource} from "./Logic/UIEventSource"; | |||
| import {Tag} from "./Logic/Tags/Tag"; | ||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; | ||||
| import {Translation} from "./UI/i18n/Translation"; | ||||
| import LocationInput from "./UI/Input/LocationInput"; | ||||
| import Loc from "./Models/Loc"; | ||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||
| import LengthInput from "./UI/Input/LengthInput"; | ||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; | ||||
| import Combine from "./UI/Base/Combine"; | ||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||
|  | @ -148,19 +153,17 @@ function TestMiniMap() { | |||
|     featureSource.ping() | ||||
| } | ||||
| //*/
 | ||||
| QueryParameters.GetQueryParameter("test", "true").setData("true") | ||||
| State.state= new State(undefined) | ||||
| const id = "node/5414688303" | ||||
| State.state.allElements.addElementById(id, new UIEventSource<any>({id: id})) | ||||
| new Combine([ | ||||
|     new DeleteWizard(id, { | ||||
|         noDeleteOptions: [ | ||||
|             { | ||||
|                 if:[ new Tag("access","private")], | ||||
|                 then: new Translation({ | ||||
|                     en: "Very private! Delete now or me send lawfull lawyer" | ||||
|                 }) | ||||
|             } | ||||
|         ] | ||||
|     }), | ||||
| ]).AttachTo("maindiv") | ||||
| 
 | ||||
| const loc = new UIEventSource<Loc>({ | ||||
|     zoom: 24, | ||||
|     lat: 51.21043, | ||||
|     lon: 3.21389 | ||||
| }) | ||||
| const li = new LengthInput( | ||||
|     AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")), | ||||
|     loc | ||||
| ) | ||||
|     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", [ | ||||
|             ["login on dev", | ||||
|                 () => { | ||||
|                    const osmConn = new OsmConnection(false, | ||||
|                    const osmConn = new OsmConnection(false,false, | ||||
|                         new UIEventSource<string>(undefined), | ||||
|                         "Unit test", | ||||
|                         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