forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						330930d5d4
					
				
					 77 changed files with 2462 additions and 581 deletions
				
			
		
							
								
								
									
										16
									
								
								.devcontainer/Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.devcontainer/Dockerfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile | ||||
| 
 | ||||
| # [Choice] Node.js version: 16, 14, 12 | ||||
| ARG VARIANT="16-buster" | ||||
| FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} | ||||
| 
 | ||||
| # [Optional] Uncomment this section to install additional OS packages. | ||||
| # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ | ||||
| #     && apt-get -y install --no-install-recommends <your-package-list-here> | ||||
| 
 | ||||
| # [Optional] Uncomment if you want to install an additional version of node using nvm | ||||
| # ARG EXTRA_NODE_VERSION=10 | ||||
| # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" | ||||
| 
 | ||||
| # [Optional] Uncomment if you want to install more global node packages | ||||
| # RUN su node -c "npm install -g <your-package-list -here>" | ||||
							
								
								
									
										29
									
								
								.devcontainer/devcontainer.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.devcontainer/devcontainer.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: | ||||
| // https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node | ||||
| { | ||||
| 	"name": "MapComplete", | ||||
| 	"build": { | ||||
| 		"dockerfile": "Dockerfile", | ||||
| 		// Update 'VARIANT' to pick a Node version: 12, 14, 16 | ||||
| 		"args": {  | ||||
| 			"VARIANT": "16" | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	// Set *default* container specific settings.json values on container create. | ||||
| 	"settings": {}, | ||||
| 
 | ||||
| 
 | ||||
| 	// Add the IDs of extensions you want installed when the container is created. | ||||
| 	"extensions": [ | ||||
| 	], | ||||
| 
 | ||||
| 	// Use 'forwardPorts' to make a list of ports inside the container available locally. | ||||
| 	"forwardPorts": [1234], | ||||
| 
 | ||||
| 	// Use 'postCreateCommand' to run commands after the container is created. | ||||
| 	"postCreateCommand": "npm run init", | ||||
| 
 | ||||
| 	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. | ||||
| 	"remoteUser": "node" | ||||
| } | ||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|     "files.eol": "\n" | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ import UnitConfigJson from "./UnitConfigJson"; | |||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import 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(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -50,9 +50,10 @@ export default class LayerConfig { | |||
|     public readonly deletion: DeleteConfig | null; | ||||
| 
 | ||||
|     presets: { | ||||
|         title: Translation; | ||||
|         tags: Tag[]; | ||||
|         description?: Translation; | ||||
|         title: Translation, | ||||
|         tags: Tag[], | ||||
|         description?: Translation, | ||||
|         preciseInput?: { preferredBackground?: string } | ||||
|     }[]; | ||||
| 
 | ||||
|     tagRenderings: TagRenderingConfig[]; | ||||
|  | @ -144,14 +145,19 @@ export default class LayerConfig { | |||
|         this.minzoom = json.minzoom ?? 0; | ||||
|         this.maxzoom = json.maxzoom ?? 1000; | ||||
|         this.wayHandling = json.wayHandling ?? 0; | ||||
|         this.presets = (json.presets ?? []).map((pr, i) => ({ | ||||
|         this.presets = (json.presets ?? []).map((pr, i) => { | ||||
|             if (pr.preciseInput === true) { | ||||
|                 pr.preciseInput = { | ||||
|                     preferredBackground: undefined | ||||
|                 } | ||||
|             } | ||||
|             return { | ||||
|                 title: Translations.T(pr.title, `${context}.presets[${i}].title`), | ||||
|                 tags: pr.tags.map((t) => FromJSON.SimpleTag(t)), | ||||
|             description: Translations.T( | ||||
|                 pr.description, | ||||
|                 `${context}.presets[${i}].description` | ||||
|             ), | ||||
|         })); | ||||
|                 description: Translations.T(pr.description, `${context}.presets[${i}].description`), | ||||
|                 preciseInput: pr.preciseInput | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         /** Given a key, gets the corresponding property from the json (or the default if not found | ||||
|          * | ||||
|  |  | |||
|  | @ -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. | ||||
|      * | ||||
|  | @ -226,6 +227,10 @@ export interface LayoutConfigJson { | |||
|      * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. | ||||
|      * This is handled by defining units. | ||||
|      *  | ||||
|      * # Rendering | ||||
|      *  | ||||
|      * To render a value with long (human) denomination, use {canonical(key)} | ||||
|      * | ||||
|      * # Usage | ||||
|      * | ||||
|      * First of all, you define which keys have units applied, for example: | ||||
|  | @ -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,16 +337,16 @@ 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; | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
|  | @ -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>  | ||||
| ------------------ | ||||
| 
 | ||||
|  |  | |||
|  | @ -435,10 +435,7 @@ export class InitUiElements { | |||
|     } | ||||
| 
 | ||||
|     private static InitBaseMap() { | ||||
|         State.state.availableBackgroundLayers = new AvailableBaseLayers( | ||||
|             State.state.locationControl | ||||
|         ).availableEditorLayers; | ||||
| 
 | ||||
|         State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); | ||||
|         State.state.backgroundLayer = State.state.backgroundLayerId.map( | ||||
|             (selectedId: string) => { | ||||
|                 if (selectedId === undefined) { | ||||
|  | @ -545,6 +542,7 @@ export class InitUiElements { | |||
|             state.selectedElement | ||||
|         ); | ||||
| 
 | ||||
|         State.state.featurePipeline = source; | ||||
|         new ShowDataLayer( | ||||
|             source.features, | ||||
|             State.state.leafletMap, | ||||
|  |  | |||
|  | @ -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,25 +25,23 @@ 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( | ||||
|     public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||
|         const source = location.map( | ||||
|             (currentLocation) => { | ||||
| 
 | ||||
|                 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) { | ||||
|                     return newLayers; | ||||
|  | @ -58,11 +57,55 @@ export default class AvailableBaseLayers { | |||
| 
 | ||||
|                 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,6 +1,5 @@ | |||
| import * as L from "leaflet"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Svg from "../../Svg"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
|  | @ -15,11 +14,19 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|      */ | ||||
|     private readonly _isActive: UIEventSource<boolean>; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _isLocked: UIEventSource<boolean>; | ||||
| 
 | ||||
|     /** | ||||
|      * The callback over the permission API | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _permission: UIEventSource<string>; | ||||
| 
 | ||||
|     /*** | ||||
|      * The marker on the map, in order to update it | ||||
|      * @private | ||||
|  | @ -39,11 +46,15 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|      * @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. | ||||
|  | @ -67,28 +78,32 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|             "geolocation-permissions" | ||||
|         ); | ||||
|         const isActive = new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|         const isLocked = new UIEventSource<boolean>(false); | ||||
|         super( | ||||
|             hasLocation.map( | ||||
|                 (hasLocation) => { | ||||
|                     if (hasLocation) { | ||||
|                         return new CenterFlexedElement( | ||||
|                             Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||
|                         ); // crosshair_blue_ui()
 | ||||
|                 (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; | ||||
|                     } | ||||
|                     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
 | ||||
|                         Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem") | ||||
|                     ); | ||||
| 
 | ||||
|                 }, | ||||
|                 [isActive] | ||||
|                 [isActive, isLocked] | ||||
|             ) | ||||
|         ); | ||||
|         this._isActive = isActive; | ||||
|         this._isLocked = isLocked; | ||||
|         this._permission = new UIEventSource<string>(""); | ||||
|         this._previousLocationGrant = previousLocationGrant; | ||||
|         this._currentGPSLocation = currentGPSLocation; | ||||
|  | @ -110,13 +125,14 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|             self.SetClass(pointerClass); | ||||
|         }); | ||||
| 
 | ||||
|         this.onClick(() => self.init(true)); | ||||
|         this.init(false); | ||||
|         this.onClick(() => { | ||||
|             if (self._hasLocation.data) { | ||||
|                 self._isLocked.setData(!self._isLocked.data); | ||||
|             } | ||||
|             self.init(true); | ||||
|         }); | ||||
|         this.init(false); | ||||
| 
 | ||||
|     private init(askPermission: boolean) { | ||||
|         const self = this; | ||||
|         const map = this._leafletMap.data; | ||||
| 
 | ||||
|         this._currentGPSLocation.addCallback((location) => { | ||||
|             self._previousLocationGrant.setData("granted"); | ||||
|  | @ -125,6 +141,8 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|                 (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||
|             if (timeSinceRequest < 30) { | ||||
|                 self.MoveToCurrentLoction(16); | ||||
|             } else if (self._isLocked.data) { | ||||
|                 self.MoveToCurrentLoction(); | ||||
|             } | ||||
| 
 | ||||
|             let color = "#1111cc"; | ||||
|  | @ -141,6 +159,8 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|                 iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||
|             }); | ||||
| 
 | ||||
|             const map = self._leafletMap.data; | ||||
| 
 | ||||
|             const newMarker = L.marker(location.latlng, {icon: icon}); | ||||
|             newMarker.addTo(map); | ||||
| 
 | ||||
|  | @ -149,7 +169,14 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|             } | ||||
|             self._marker = newMarker; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private init(askPermission: boolean) { | ||||
|         const self = this; | ||||
|         if (self._isActive.data) { | ||||
|             self.MoveToCurrentLoction(16); | ||||
|             return; | ||||
|         } | ||||
|         try { | ||||
|             navigator?.permissions | ||||
|                 ?.query({name: "geolocation"}) | ||||
|  | @ -174,31 +201,6 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private locate() { | ||||
|         const self = this; | ||||
|         const map: any = this._leafletMap.data; | ||||
| 
 | ||||
|         if (navigator.geolocation) { | ||||
|             navigator.geolocation.getCurrentPosition( | ||||
|                 function (position) { | ||||
|                     self._currentGPSLocation.setData({ | ||||
|                         latlng: [position.coords.latitude, position.coords.longitude], | ||||
|                         accuracy: position.coords.accuracy, | ||||
|                     }); | ||||
|                 }, | ||||
|                 function () { | ||||
|                     console.warn("Could not get location with navigator.geolocation"); | ||||
|                 } | ||||
|             ); | ||||
|             return; | ||||
|         } else { | ||||
|             map.findAccuratePosition({ | ||||
|                 maxWait: 10000, // defaults to 10000
 | ||||
|                 desiredAccuracy: 50, // defaults to 20
 | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private MoveToCurrentLoction(targetZoom = 16) { | ||||
|         const location = this._currentGPSLocation.data; | ||||
|         this._lastUserRequest = undefined; | ||||
|  | @ -249,17 +251,21 @@ export default class GeoLocationHandler extends VariableUiElement { | |||
|         } | ||||
| 
 | ||||
|         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"); | ||||
|         if (self._isActive.data) { | ||||
|             return; | ||||
|         } | ||||
|                 this.locate(); | ||||
|         self._isActive.setData(true); | ||||
|         navigator.geolocation.watchPosition( | ||||
|             function (position) { | ||||
|                 self._currentGPSLocation.setData({ | ||||
|                     latlng: [position.coords.latitude, position.coords.longitude], | ||||
|                     accuracy: position.coords.accuracy, | ||||
|                 }); | ||||
|             }, | ||||
|             function () { | ||||
|                 console.warn("Could not get location with navigator.geolocation"); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -47,7 +47,12 @@ export default class StrayClickHandler { | |||
|                     popupAnchor: [0, -45] | ||||
|                 }) | ||||
|             }); | ||||
|             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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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} | ||||
| 
 | ||||
|    | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -276,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) { | ||||
|  | @ -50,7 +57,6 @@ export class Changes implements FeatureSource{ | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|     addTag(elementId: string, tagsFilter: TagsFilter, | ||||
|            tags?: UIEventSource<any>) { | ||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||
|  | @ -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); | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|  | @ -172,7 +175,11 @@ export class ChangesetHandler { | |||
|                     // FAILED
 | ||||
|                     self.CloseChangeset(csId, continuation) | ||||
|                 }) | ||||
|         }, true, reason) | ||||
|             }, { | ||||
|                 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); | ||||
|             } | ||||
|         ); | ||||
|  |  | |||
|  | @ -84,6 +84,7 @@ export default class SimpleMetaTagger { | |||
|         }, | ||||
|         (feature => { | ||||
|             const units = State.state?.layoutToUse?.data?.units ?? []; | ||||
|             let rewritten = false; | ||||
|             for (const key in feature.properties) { | ||||
|                 if (!feature.properties.hasOwnProperty(key)) { | ||||
|                     continue; | ||||
|  | @ -95,16 +96,23 @@ export default class SimpleMetaTagger { | |||
|                     const value = feature.properties[key] | ||||
|                     const [, denomination] = unit.findDenomination(value) | ||||
|                     let canonical = denomination?.canonicalValue(value) ?? undefined; | ||||
|                     console.log("Rewritten ", key, " from", value, "into", canonical) | ||||
|                     if(canonical === value){ | ||||
|                         break; | ||||
|                     } | ||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) | ||||
|                     if (canonical === undefined && !unit.eraseInvalid) { | ||||
|                         break; | ||||
|                     } | ||||
| 
 | ||||
|                     feature.properties[key] = canonical; | ||||
|                     rewritten = true; | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if(rewritten){ | ||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); | ||||
|             } | ||||
|         }) | ||||
|     ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,22 @@ import {UIEventSource} from "../UIEventSource"; | |||
|  */ | ||||
| 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 { | ||||
|             const saved = localStorage.getItem(key); | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
							
								
								
									
										21
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -19,6 +19,7 @@ import TitleHandler from "./Logic/Actors/TitleHandler"; | |||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||
| import {Relation} from "./Logic/Osm/ExtractRelations"; | ||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  | @ -95,6 +96,12 @@ export default class State { | |||
|     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>; | ||||
| 
 | ||||
| 
 | ||||
|     public featurePipeline: FeaturePipeline; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * The map location: currently centered lat, lon and zoom | ||||
|  | @ -311,11 +318,24 @@ export default class State { | |||
|                 (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
 | ||||
|  | @ -341,6 +361,7 @@ export default class State { | |||
| 
 | ||||
|         this.osmConnection = new OsmConnection( | ||||
|             this.featureSwitchIsTesting.data, | ||||
|             this.featureSwitchFakeUser.data, | ||||
|             QueryParameters.GetQueryParameter( | ||||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|  |  | |||
							
								
								
									
										22
									
								
								Svg.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								Svg.ts
									
										
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -5,6 +5,7 @@ import Loc from "../../Models/Loc"; | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import 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,6 +31,7 @@ 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++ | ||||
| 
 | ||||
|     } | ||||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -50,13 +58,11 @@ export default class SimpleAddUI extends Toggle { | |||
|         ]); | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|         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); | ||||
|         } | ||||
| 
 | ||||
|  | @ -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,20 +109,46 @@ 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 | ||||
|             }); | ||||
|              | ||||
|         const confirmButton = new SubtleButton(preset.icon, | ||||
|             let backgroundLayer = undefined; | ||||
|             if(preset.preciseInput.preferredBackground){ | ||||
|                backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) | ||||
|             } | ||||
|              | ||||
|             preciseInput = new LocationInput({ | ||||
|                 mapBackground: backgroundLayer, | ||||
|                 centerLocation:locationSrc | ||||
|                      | ||||
|             }) | ||||
|             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         let confirmButton: BaseUIElement = new SubtleButton(preset.icon(), | ||||
|             new Combine([ | ||||
|                 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 = | ||||
|             new SubtleButton( | ||||
|  | @ -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           ,  | ||||
|                 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 | ||||
|  | @ -195,13 +227,13 @@ export default class SimpleAddUI extends Toggle { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
| /* | ||||
| * 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; | ||||
|             } | ||||
| 
 | ||||
|  | @ -209,14 +241,15 @@ export default class SimpleAddUI extends Toggle { | |||
|             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,7 +350,7 @@ export default class ValidatedTextField { | |||
|                 }) | ||||
|             ) | ||||
|             unitDropDown.GetValue().setData(unit.defaultDenom) | ||||
|             unitDropDown.SetStyle("width: min-content") | ||||
|             unitDropDown.SetClass("w-min") | ||||
| 
 | ||||
|             input = new CombinedInputElement( | ||||
|                 input, | ||||
|  | @ -292,13 +360,12 @@ export default class ValidatedTextField { | |||
|                 (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] | ||||
|  | @ -308,8 +375,9 @@ export default class ValidatedTextField { | |||
|         if (tp.inputHelper) { | ||||
|             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
 | ||||
|  | @ -318,6 +386,7 @@ export default class ValidatedTextField { | |||
|         } | ||||
|         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){ | ||||
|             if (trs.length === 0) { | ||||
|                 return undefined; | ||||
|             } | ||||
| 
 | ||||
|             const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) | ||||
|            if(valuesToRender.length === 1){ | ||||
|             if (valuesToRender.length === 1) { | ||||
|                 return valuesToRender[0]; | ||||
|            }else if(valuesToRender.length > 1){ | ||||
|             } else if (valuesToRender.length > 1) { | ||||
|                 return new List(valuesToRender) | ||||
|             } | ||||
|             return undefined; | ||||
|                 }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) | ||||
|         }).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) | ||||
| 
 | ||||
|         this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") | ||||
|         this.SetClass("flex items-center flex-row text-lg link-underline") | ||||
|         this.SetStyle("word-wrap: anywhere;"); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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,21 +329,35 @@ 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:{ | ||||
|                 element = { | ||||
|                     special: { | ||||
|                         args: args, | ||||
|                         style: style, | ||||
|                         func: knownSpecial | ||||
|                 }} | ||||
|                     } | ||||
|                 } | ||||
|                 return [...partBefore, element, ...partAfter] | ||||
|             } | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										17
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -1,4 +1,5 @@ | |||
| import * as colors from "./assets/colors.json" | ||||
| import {TileRange} from "./Models/TileRange"; | ||||
| 
 | ||||
| export class Utils { | ||||
| 
 | ||||
|  | @ -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 | 
|  | @ -646,5 +646,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~*", | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": { | ||||
|           "and": [ | ||||
|             "wikipedia=", | ||||
|     "condition": { | ||||
|       "or": [ | ||||
|         "wikipedia~*", | ||||
|         "wikidata~*" | ||||
|       ] | ||||
|     }, | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "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" | ||||
|  |  | |||
|  | @ -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" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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"; | ||||
|  | @ -18,6 +18,7 @@ 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) { | ||||
|  |  | |||
|  | @ -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