diff --git a/.forgejo/workflows/on_release.yml b/.forgejo/workflows/on_release.yml index 3845b26cf..e87e9455e 100644 --- a/.forgejo/workflows/on_release.yml +++ b/.forgejo/workflows/on_release.yml @@ -53,9 +53,11 @@ jobs: cd android/app # We assign the version code simply based on the date new_version_code=$(( ( $(date +%s) - $(date -d "2025-07-01" +%s) ) / 86400 )) + new_version_code=$(( 1949 + $new_version_code )) # see https://source.mapcomplete.org/MapComplete/MapComplete/issues/2520 versionname="${{ github.ref_name }}" versionname="${versionname:1}" - sed -i "s/versionCode $version_code/versionCode $new_version_code/" "build.gradle" + echo "Versioncode will be $new_version_code ; versionname is $versionname" + sed -i "s/versionCode .*$/versionCode $new_version_code/" "build.gradle" sed -i "s/versionName \".*\"/versionName \"$versionname\"/" "build.gradle" cat build.gradle | grep "versionName" cat build.gradle | grep "versionCode" diff --git a/.vscode/settings.json b/.vscode/settings.json index b607ae0fc..a0e2c3e54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,5 +60,10 @@ "type": "Gitea", "name": "MapComplete Forgejo" } - ] + ], + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*": "${capture}.license" + }, + "explorer.fileNesting.expand": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index b375f5ed5..c04831189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,63 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.55.7](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.55.6...v0.55.7) (2025-09-02) + +### [0.55.6](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.55.5...v0.55.6) (2025-09-02) + + +### Features + +* add 'download app' item into the menu drawer (if not on Android) ([0a0d3dc](https://source.mapcomplete.org/MapComplete/MapComplete/commits/0a0d3dc8dab91862b93974639a68e1b89ad5789c)) +* add avatar with offline support ([f671cd3](https://source.mapcomplete.org/MapComplete/MapComplete/commits/f671cd342fd84a669104b2e0f452b450b0329ea4)) +* add multiTitle, better error handling, improve help text of script ([0d33e18](https://source.mapcomplete.org/MapComplete/MapComplete/commits/0d33e18a593dd6527465cd5e86b2506fceb4d1ed)) +* include (all) sign languages in the 'LanguageElement' special rendering ([fe31af4](https://source.mapcomplete.org/MapComplete/MapComplete/commits/fe31af4b15446fc7787ba3e2ecc677c46d6c8de2)) +* offline: more features to be able to work fully offline ([06aa8a3](https://source.mapcomplete.org/MapComplete/MapComplete/commits/06aa8a34061ec1e607b8f0de4ab8cf12213b8dae)) +* **offline:** attempt to upload pictures when connection is restored ([dd0ed24](https://source.mapcomplete.org/MapComplete/MapComplete/commits/dd0ed24f3b4425676d6f85a4467bb7c1662459fd)) +* **offline:** better support for making changes while offline ([7155cd7](https://source.mapcomplete.org/MapComplete/MapComplete/commits/7155cd7f6184073bf641f1524ae438a803710b94)) +* **offline:** don't attempt to upload images if offline ([f1da972](https://source.mapcomplete.org/MapComplete/MapComplete/commits/f1da97285fe68c72b13299ebc97c28428fc95230)) +* **offline:** more offline hardening ([561e4cb](https://source.mapcomplete.org/MapComplete/MapComplete/commits/561e4cb00990cb1e1e38f9459ff858256b9acfaf)) +* **offline:** reload data of current view when internet is restored, see [#2111](https://source.mapcomplete.org/MapComplete/MapComplete/issues/2111) ([e44cf02](https://source.mapcomplete.org/MapComplete/MapComplete/commits/e44cf029bdbf50810579b1ae8d7aedfdf63d3df4)) +* **offline:** UX: add icon ([a5bab8d](https://source.mapcomplete.org/MapComplete/MapComplete/commits/a5bab8d819082dee354eed1fc7bcac998c4f49ab)) + + +### Bug Fixes + +* check for type in TagLink ([bd3c266](https://source.mapcomplete.org/MapComplete/MapComplete/commits/bd3c266a4d1ce344b613431e5704a91207347b8c)) +* fix 'non-loading' due to incorrect caching in service worker ([f6d6ec9](https://source.mapcomplete.org/MapComplete/MapComplete/commits/f6d6ec98859de5665fa0e2a048ebfe92e4f924c0)) +* fix [#2509](https://source.mapcomplete.org/MapComplete/MapComplete/issues/2509) ([aed6def](https://source.mapcomplete.org/MapComplete/MapComplete/commits/aed6defa168030fb7947271df068fe5feb0b0ba2)) +* fix crash ([a570e29](https://source.mapcomplete.org/MapComplete/MapComplete/commits/a570e292423785dd7532feb28facc46e4ade5e6d)) +* fix crash in GRB theme when replacing geometry ([3c5a528](https://source.mapcomplete.org/MapComplete/MapComplete/commits/3c5a528307db102ff24d9d3b82b884bca58ad4c3)) +* fix crash in GRB theme when replacing geometry ([6e1aaf6](https://source.mapcomplete.org/MapComplete/MapComplete/commits/6e1aaf6be1950a50eee8c01ade244821bb221563)) +* fix showing splitpoint icons ([9da10a9](https://source.mapcomplete.org/MapComplete/MapComplete/commits/9da10a9b32f91b11fbeafac2d63b7b5d66a1e737)) +* fix small typing issues ([fd5e390](https://source.mapcomplete.org/MapComplete/MapComplete/commits/fd5e39065d4d6381838b718d34f75d02372f66a1)) +* fix tests ([42f07bc](https://source.mapcomplete.org/MapComplete/MapComplete/commits/42f07bc1f3f64a63ce7dec0bf22a4a1e5bdbd42f)) +* fix tests ([8f776bf](https://source.mapcomplete.org/MapComplete/MapComplete/commits/8f776bf0306338f0bac36db20006f7c26ac49f92)) +* fix tests ([b81f597](https://source.mapcomplete.org/MapComplete/MapComplete/commits/b81f59779dea74ca9c01f8d7df30844d29ec01ae)) +* panoramax attribution now filters out empty strings (for some edge cases on non-mapcomplete panoramax servers) ([17f0978](https://source.mapcomplete.org/MapComplete/MapComplete/commits/17f097851aafeb4db9c8cc0590abde9efe8bf599)) +* split values for tag2link ([36b3faf](https://source.mapcomplete.org/MapComplete/MapComplete/commits/36b3faf2d14bf174545d437705a17d3c2917c643)) + + +### Theme improvements + +* **hut:** move 'hut' above shelter, steal shelter type question, also see [#2515](https://source.mapcomplete.org/MapComplete/MapComplete/issues/2515) ([3da62f8](https://source.mapcomplete.org/MapComplete/MapComplete/commits/3da62f8d708c73cd72868db0206ed90cea2b6b98)) +* move shelter filter to filters ([eb317d0](https://source.mapcomplete.org/MapComplete/MapComplete/commits/eb317d0bdabb0e5cf1dc4a871d65757a432c4de3)) +* **nature:** Add picnic sites ([#1849](https://source.mapcomplete.org/MapComplete/MapComplete/issues/1849)) ([4294e49](https://source.mapcomplete.org/MapComplete/MapComplete/commits/4294e4930509bb8157597d3d6242046a5fa14e8d)) +* New arcade theme ([2c65747](https://source.mapcomplete.org/MapComplete/MapComplete/commits/2c6574762c30d60440806ace3f9e3320f771c9a4)) +* **parking:** Add support for access tags ([#1797](https://source.mapcomplete.org/MapComplete/MapComplete/issues/1797)) ([03d07b6](https://source.mapcomplete.org/MapComplete/MapComplete/commits/03d07b670de7fa84c0e58f647073f67fb6d60046)) +* **parkings:** Add charge points ([c0ee578](https://source.mapcomplete.org/MapComplete/MapComplete/commits/c0ee578df1b0db3c9670b6c8f722787e6a9b72f3)) +* **preset_type_select:** fix 'auto-icon' display ([81f98e6](https://source.mapcomplete.org/MapComplete/MapComplete/commits/81f98e62ceec6313256a7864822225efb0b772d1)) +* **preset_type_select:** preset type select now removes keys set by other presets ([152d93b](https://source.mapcomplete.org/MapComplete/MapComplete/commits/152d93bf4bdc73e4d331e2718181599ccf9a18b1)) +* **range:** change rendering ([e9f7192](https://source.mapcomplete.org/MapComplete/MapComplete/commits/e9f71924c14bc9d3b9940060775d8edc8311225c)) +* **shops:** Add self_checkout question ([2cf0bc1](https://source.mapcomplete.org/MapComplete/MapComplete/commits/2cf0bc1866cd8df4bc7006b29a5f49210761ccb3)) +* **shops:** change conditions for self_checkout question ([aecc36d](https://source.mapcomplete.org/MapComplete/MapComplete/commits/aecc36dfbe0972eec5f76d11a5b526a1992ecb2c)) +* **shops:** Exlcude shop=no ([06ac28d](https://source.mapcomplete.org/MapComplete/MapComplete/commits/06ac28dab98777ba91c06ec46800f08c52d14bdc)) +* **shops:** Remove brand tags if marked as without brand ([877dd26](https://source.mapcomplete.org/MapComplete/MapComplete/commits/877dd260aed3a21ffbb1f638b07e35277df835b6)) +* **surveillance:** add some tweaks to the 'panorama'-camera ([db62329](https://source.mapcomplete.org/MapComplete/MapComplete/commits/db62329d396e067dcb2155a7deed2e466065e383)) +* **width:** add cyclestreet indication, add icons ([67c5322](https://source.mapcomplete.org/MapComplete/MapComplete/commits/67c5322a80067acb911eb99f6ec876763039f61f)) +* **width:** add note if street are low on cars, hide 'unknown'-layer by default, add 'separate' as recognized parking type ([9a2b5d0](https://source.mapcomplete.org/MapComplete/MapComplete/commits/9a2b5d0cf7021d369f85b51779cc73ac8c6159fa)) +* **zhv:** fix broken import ([40bd564](https://source.mapcomplete.org/MapComplete/MapComplete/commits/40bd564f864a73e4b9f476e3347c22134cf3f79b)) + ### [0.55.5](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.55.4...v0.55.5) (2025-08-19) diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index cb00c41ad..c4882712f 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -14,8 +14,6 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam - [UI](#ui) + [braced](#braced) + [create_copy](#create_copy) - + [preset_description](#preset_description) - + [show_icons](#show_icons) + [title](#title) + [translated](#translated) - [data](#data) @@ -81,15 +79,13 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam - [tagrendering_manipulation](#tagrendering_manipulation) + [group](#group) + [multi](#multi) + + [open_in_iD](#open_in_id) + + [open_in_josm](#open_in_josm) + [steal](#steal) - - [ui](#ui) - + [preset_type_select](#preset_type_select) - [web_and_communication](#web_and_communication) + [fediverse_link](#fediverse_link) + [link](#link) + [mapillary_link](#mapillary_link) - + [open_in_iD](#open_in_id) - + [open_in_josm](#open_in_josm) + [send_email](#send_email) + [wikidata_label](#wikidata_label) + [wikipedia](#wikipedia) @@ -99,6 +95,8 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam + [histogram](#histogram) + [language_chooser](#language_chooser) + [multi_apply](#multi_apply) + + [preset_description](#preset_description) + + [preset_type_select](#preset_type_select) + [upload_to_osm](#upload_to_osm) # Using expanded syntax @@ -142,7 +140,8 @@ Show a literal text within braces -----|-----|----- | | text | _undefined_ | The value to show | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L296](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L296) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L295](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L295) #### Example usage of braced @@ -152,42 +151,19 @@ Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L296](/src/ Allow to create a copy of the current element -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L315](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L315) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L314](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L314) #### Example usage of create_copy `{create_copy()}` -### preset_description - -Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty - -Defined in [/src/UI/Popup/DataVisualisations.ts#L215](/src/UI/Popup/DataVisualisations.ts#L215) - -#### Example usage of preset_description - -`{preset_description()}` - -### show_icons - -Displays all icons from the specified tagRenderings (if they are known and have an icon) together, e.g. to give a summary of the dietary options - -| name | default | description | ------|-----|----- | -| labels | _undefined_ | A ';'-separated list of labels and/or ids of tagRenderings | -| class | inline-flex mx-4 | CSS-classes of the container, space-separated | - -Defined in [/src/UI/Popup/DataVisualisations.ts#L307](/src/UI/Popup/DataVisualisations.ts#L307) - -#### Example usage of show_icons - -`{show_icons(,inline-flex mx-4)}` - ### title Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?' -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L281](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L281) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L280](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L280) #### Example usage of title @@ -201,7 +177,8 @@ If the given key can be interpreted as a JSON, only show the key containing the -----|-----|----- | | key | value | The attribute to interpret as json | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L251](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L251) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L250](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L250) #### Example usage of translated @@ -215,7 +192,7 @@ Visualises data of a POI, sometimes with data updating capabilities Prints all key-value pairs of the object - used for debugging -Defined in [/src/UI/Popup/DataVisualisations.ts#L270](/src/UI/Popup/DataVisualisations.ts#L270) +Defined in [/src/UI/Popup/DataVisualisations.ts#L263](/src/UI/Popup/DataVisualisations.ts#L263) #### Example usage of all_tags @@ -229,7 +206,7 @@ Converts a short, canonical value into the long, translated text including the u -----|-----|----- | | key | _undefined_ | The key of the tag to give the canonical text for | -Defined in [/src/UI/Popup/DataVisualisations.ts#L163](/src/UI/Popup/DataVisualisations.ts#L163) +Defined in [/src/UI/Popup/DataVisualisations.ts#L155](/src/UI/Popup/DataVisualisations.ts#L155) #### Example usage of canonical @@ -244,7 +221,7 @@ Converts compass degrees (with 0° being north, 90° being east, ...) into a hum | key | _direction:centerpoint | The attribute containing the degrees | | offset | 0 | Offset value that is added to the actual value, e.g. `180` to indicate the opposite (backward) direction | -Defined in [/src/UI/Popup/DataVisualisations.ts#L47](/src/UI/Popup/DataVisualisations.ts#L47) +Defined in [/src/UI/Popup/DataVisualisations.ts#L39](/src/UI/Popup/DataVisualisations.ts#L39) #### Example usage of direction_absolute @@ -254,7 +231,7 @@ Defined in [/src/UI/Popup/DataVisualisations.ts#L47](/src/UI/Popup/DataVisualisa Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object -Defined in [/src/UI/Popup/DataVisualisations.ts#L34](/src/UI/Popup/DataVisualisations.ts#L34) +Defined in [/src/UI/Popup/DataVisualisations.ts#L26](/src/UI/Popup/DataVisualisations.ts#L26) #### Example usage of direction_indicator @@ -270,7 +247,7 @@ A small element, showing if the POI is currently open and when the next change i | prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__ | | postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__ | -Defined in [/src/UI/Popup/DataVisualisations.ts#L126](/src/UI/Popup/DataVisualisations.ts#L126) +Defined in [/src/UI/Popup/DataVisualisations.ts#L118](/src/UI/Popup/DataVisualisations.ts#L118) #### Example usage of opening_hours_state @@ -286,7 +263,7 @@ Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to c | prefix | _empty string_ | Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__ | | postfix | _empty string_ | Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__ | -Defined in [/src/UI/Popup/DataVisualisations.ts#L87](/src/UI/Popup/DataVisualisations.ts#L87) +Defined in [/src/UI/Popup/DataVisualisations.ts#L81](/src/UI/Popup/DataVisualisations.ts#L81) #### Example usage of opening_hours_table @@ -300,7 +277,7 @@ Creates a visualisation for 'points in time', e.g. collection times of a postbox -----|-----|----- | | key | _undefined_ | The key out of which the points_in_time will be parsed | -Defined in [/src/UI/Popup/DataVisualisations.ts#L281](/src/UI/Popup/DataVisualisations.ts#L281) +Defined in [/src/UI/Popup/DataVisualisations.ts#L274](/src/UI/Popup/DataVisualisations.ts#L274) #### Example usage of points_in_time @@ -310,7 +287,7 @@ Defined in [/src/UI/Popup/DataVisualisations.ts#L281](/src/UI/Popup/DataVisualis Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer -Defined in [/src/UI/Popup/DataVisualisations.ts#L203](/src/UI/Popup/DataVisualisations.ts#L203) +Defined in [/src/UI/Popup/DataVisualisations.ts#L195](/src/UI/Popup/DataVisualisations.ts#L195) #### Example usage of statistics @@ -354,7 +331,8 @@ Gives an interactive element which shows a tag comparison between the OSM-object | host | _undefined_ | The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. | | readonly | _undefined_ | If 'yes', will not show 'apply'-buttons | -Defined in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L243](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L243) +Defined +in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L243](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L243) #### Example usage of compare_data @@ -414,7 +392,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | icon | ./assets/svg/addSmall.svg | A nice icon to show in the button | | way_to_conflate | _undefined_ | The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag | -Defined in [/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts#L30](/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts#L30) +Defined +in [/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts#L30](/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts#L30) #### Example usage of conflate_button @@ -543,7 +522,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | snap_onto_layers | _undefined_ | If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead | | snap_to_layer_max_distance | 0.1 | Distance to distort the geometry to snap to this layer | -Defined in [/src/UI/Popup/ImportButtons/WayImportButtonViz.ts#L22](/src/UI/Popup/ImportButtons/WayImportButtonViz.ts#L22) +Defined +in [/src/UI/Popup/ImportButtons/WayImportButtonViz.ts#L22](/src/UI/Popup/ImportButtons/WayImportButtonViz.ts#L22) #### Example usage of import_way_button @@ -561,7 +541,8 @@ Attempts to load (via a proxy) the specified website and parsed ld+json from the | mode | _undefined_ | If `display`, only show the data in tabular and readonly form, ignoring already existing tags. This is used to explicitly show all the tags. If unset or anything else, allow to apply/import on OSM | | collapsed | yes | If the containing accordion should be closed | -Defined in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L105](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L105) +Defined +in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L105](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L105) #### Example usage of linked_data_from_website @@ -580,7 +561,8 @@ Change the status of the given MapRoulette task | maproulette_id | mr_taskId | The property name containing the maproulette id | | ask_feedback | _empty string_ | If not an empty string, this will be used as question to ask some additional feedback. A text field will be added | -Defined in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L25](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L25) +Defined +in [/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L25](/src/UI/SpecialVisualisations/DataImportSpecialVisualisations.ts#L25) #### Example usage of maproulette_set_status @@ -641,7 +623,7 @@ Note that these values can be prepare with javascript in the theme by using a [c | id_of_object_to_apply_this_one | _undefined_ | If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element | | maproulette_id | _undefined_ | If specified, this maproulette-challenge will be closed when the tags are applied. This should be the `id` of the individual task, _not_ the task_id (which corresponds with the challenge). | -Defined in [/src/UI/SpecialVisualisations/TagApplyViz.ts#L17](/src/UI/SpecialVisualisations/TagApplyViz.ts#L17) +Defined in [/src/UI/SpecialVisualisations/TagApplyViz.ts#L13](/src/UI/SpecialVisualisations/TagApplyViz.ts#L13) #### Example usage of tag_apply @@ -655,7 +637,8 @@ These special visualisations are (mostly) interactive components that most eleme An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click` -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L235](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L235) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L234](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L234) #### Example usage of add_new_point @@ -665,7 +648,8 @@ Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L235](/src/ Adds a button which allows to delete the object at this location. The config will be read from the layer config -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L157](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L157) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L157](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L157) #### Example usage of delete_button @@ -680,7 +664,8 @@ Shows a 'nothing is currently known-message if there is at least one unanswered | text | _undefined_ | Text to show | | cssClasses | _undefined_ | Classes to apply onto the text | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L207](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L207) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L206](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L206) #### Example usage of if_nothing_known @@ -696,7 +681,8 @@ A small map showing the selected feature. | idKey | id | The key of one or more properties of the feature, semi-colon separated. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap. | | class | h-40 rounded | CSS-classes (space-separated) that should be applied onto the container | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L81](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L81) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L81](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L81) #### Example usage of minimap @@ -706,7 +692,8 @@ Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L81](/src/U Adds a button which allows to move the object to another location. The config will be read from the layer config -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L137](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L137) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L137](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L137) #### Example usage of move_button @@ -721,7 +708,8 @@ Generates a QR-code to share the selected object | text | _undefined_ | Extra text on the side of the QR-code | | textClass | _undefined_ | CSS class of the the side text | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L178](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L178) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L178](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L178) #### Example usage of qr_code @@ -737,7 +725,8 @@ The special element which shows the questions which are unknown. Added by defaul | blacklisted-labels | _undefined_ | One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'. If both a whitelist and a blacklist are given, will show questions having at least one label from the whitelist but none of the blacklist. | | show_all | _undefined_ | Either `no`, `yes` or `user-preference`. Indicates if all questions should be shown at once | -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L31](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L31) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L31](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L31) #### Example usage of questions @@ -762,7 +751,8 @@ Defined in [/src/UI/Popup/ShareLinkViz.ts#L6](/src/UI/Popup/ShareLinkViz.ts#L6) Adds a button which allows to split a way -Defined in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L123](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L123) +Defined +in [/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L123](/src/UI/SpecialVisualisations/UISpecialVisualisations.ts#L123) #### Example usage of split_button @@ -776,7 +766,8 @@ Elements relating to marking an object as favourite (giving it a heart). Default A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon -Defined in [/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L19](/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L19) +Defined +in [/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L19](/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L19) #### Example usage of favourite_icon @@ -786,7 +777,8 @@ Defined in [/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L19](/src/U A button that allows a (logged in) contributor to mark a location as a favourite location -Defined in [/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L6](/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L6) +Defined +in [/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L6](/src/UI/SpecialVisualisations/FavouriteVisualisations.ts#L6) #### Example usage of favourite_status @@ -804,7 +796,8 @@ Creates an image carousel for the given sources. An attempt will be made to gues -----|-----|----- | | image_key | image;mapillary;image;wikidata;wikimedia_commons;image;panoramax;image;image | The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Multiple values are allowed if ';'-separated | -Defined in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L48](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L48) +Defined +in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L48](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L48) #### Example usage of image_carousel @@ -820,7 +813,8 @@ Creates a button where a user can upload an image to panoramax | label | _undefined_ | The text to show on the button | | disable_blur | _undefined_ | If set to 'true' or 'yes', then face blurring will be disabled. To be used sparingly | -Defined in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L82](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L82) +Defined +in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L82](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L82) #### Example usage of image_upload @@ -835,7 +829,8 @@ A component showing nearby images loaded from various online services such as Ma | mode | closed | Either `open` or `closed`. If `open`, then the image carousel will always be shown | | readonly | _undefined_ | If 'readonly' or 'yes', will not show the 'link'-button | -Defined in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L12](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L12) +Defined +in [/src/UI/SpecialVisualisations/ImageVisualisations.ts#L12](/src/UI/SpecialVisualisations/ImageVisualisations.ts#L12) #### Example usage of nearby_images @@ -853,7 +848,8 @@ Adds an image to a node -----|-----|----- | | Id-key | id | The property name where the ID of the note to close can be found | -Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L115](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L115) +Defined +in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L111](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L111) #### Example usage of add_image_to_note @@ -867,7 +863,8 @@ A textfield to add a comment to a node (with the option to close the note). -----|-----|----- | | Id-key | id | The property name where the ID of the note to close can be found | -Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L79](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L79) +Defined +in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L75](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L75) #### Example usage of add_note_comment @@ -886,7 +883,8 @@ Button to close a note. A predefined text can be defined to close the note with. | minZoom | _undefined_ | If set, only show the closenote button if zoomed in enough | | zoomButton | _undefined_ | Text to show if not zoomed in enough | -Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L22](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L22) +Defined +in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L18](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L18) #### Example usage of close_note @@ -896,7 +894,8 @@ Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L22](/src/UI/Spe Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled -Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L98](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L98) +Defined +in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L94](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L94) #### Example usage of open_note @@ -911,7 +910,8 @@ Visualises the comments for notes | commentsKey | comments | The property name of the comments, which should be stringified json | | start | 0 | Drop the first 'start' comments | -Defined in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L136](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L136) +Defined +in [/src/UI/SpecialVisualisations/NoteVisualisations.ts#L132](/src/UI/SpecialVisualisations/NoteVisualisations.ts#L132) #### Example usage of visualize_note_comments @@ -931,7 +931,8 @@ Invites the contributor to leave a review. Somewhat small UI-element until inter | fallback | _undefined_ | The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value | | question | _undefined_ | The question to ask during the review | -Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L22](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L22) +Defined +in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L22](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L22) #### Example usage of create_review @@ -946,7 +947,8 @@ Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs | subjectKey | name | The key to use to determine the subject. If specified, the subject will be tags[subjectKey] | | fallback | _undefined_ | The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value | -Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L88](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L88) +Defined +in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L88](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L88) #### Example usage of list_reviews @@ -961,7 +963,8 @@ Shows stars which represent the average rating on mangrove. | subjectKey | name | The key to use to determine the subject. If the value is specified, the subject will be tags[subjectKey] and will use this to filter the reviews. | | fallback | _undefined_ | The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value | -Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L125](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L125) +Defined +in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L125](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L125) #### Example usage of rating @@ -977,7 +980,8 @@ A pragmatic combination of `create_review` and `list_reviews` | fallback | _undefined_ | The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value | | question | _undefined_ | The question to ask in the review form. Optional | -Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L182](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L182) +Defined +in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L182](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L182) #### Example usage of reviews @@ -995,7 +999,8 @@ A button which clears the locally downloaded data and the service worker. Login -----|-----|----- | | text | _undefined_ | The text to show on the button | -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L110](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L110) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L110](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L110) #### Example usage of clear_caches @@ -1005,7 +1010,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L110](/src/U A button to remove the travelled track information from the device -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L215](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L215) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L215](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L215) #### Example usage of clear_location_history @@ -1015,7 +1021,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L215](/src/U Shows which questions are disabled for every layer. Used in 'settings' -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L46](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L46) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L46](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L46) #### Example usage of disabled_questions @@ -1025,7 +1032,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L46](/src/UI Shows the current tags of the GPS-representing object, used for debugging -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L69](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L69) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L69](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L69) #### Example usage of gps_all_tags @@ -1035,7 +1043,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L69](/src/UI Shows the current tags of the GPS-representing object, used for debugging -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L58](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L58) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L58](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L58) #### Example usage of gyroscope_all_tags @@ -1049,7 +1058,8 @@ Only makes sense in the usersettings. Allows to import a mangrove public key and -----|-----|----- | | text | _undefined_ | The text that is shown on the button | -Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L162](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L162) +Defined +in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L162](/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L162) #### Example usage of import_mangrove_key @@ -1059,7 +1069,8 @@ Defined in [/src/UI/SpecialVisualisations/ReviewSpecialVisualisations.ts#L162](/ A component to set the language of the user interface -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L26](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L26) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L26](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L26) #### Example usage of language_picker @@ -1074,7 +1085,8 @@ Show a login button | force | _undefined_ | Always show this button, even if logged in | | message | _undefined_ | Message to display on the button | -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L131](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L131) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L131](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L131) #### Example usage of login_button @@ -1084,7 +1096,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L131](/src/U Shows a button where the user can log out -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L192](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L192) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L192](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L192) #### Example usage of logout @@ -1094,7 +1107,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L192](/src/U A module showing the pending changes, with the option to clear the pending changes -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L204](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L204) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L204](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L204) #### Example usage of pending_changes @@ -1109,7 +1123,8 @@ A QR-code which shares the current URL and adds the login token. Anyone with thi | text | _undefined_ | Extra text on the side of the QR-code | | textClass | _undefined_ | CSS class of the the side text | -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L161](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L161) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L161](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L161) #### Example usage of qr_login @@ -1119,7 +1134,8 @@ Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L161](/src/U Shows the current state of storage -Defined in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L86](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L86) +Defined +in [/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L86](/src/UI/SpecialVisualisations/SettingsVisualisations.ts#L86) #### Example usage of storage_all_tags @@ -1139,7 +1155,8 @@ A collapsable group (accordion) | labels | _undefined_ | A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion | | blacklist | _undefined_ | A `;`-separated list of either identifiers or label names. Matching tagrenderings will _not_ be included, even if they are in `labels` | -Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L176](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L176) +Defined +in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L176](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L176) #### Example usage of group @@ -1155,7 +1172,8 @@ Given an embedded tagRendering (read only) and a key, will read the keyname as a | tagrendering | _undefined_ | An entire tagRenderingConfig | | classes | _undefined_ | CSS-classes to apply on every individual item. Seperated by `space` | -Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L96](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L96) +Defined +in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L96](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L96) #### Example usage of multi @@ -1173,6 +1191,28 @@ Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisuali } ``` +### open_in_iD + +Opens the current view in the iD-editor + +Defined +in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L212](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L212) + +#### Example usage of open_in_iD + +`{open_in_iD()}` + +### open_in_josm + +Opens the current view in the JOSM-editor + +Defined +in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L226](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L226) + +#### Example usage of open_in_josm + +`{open_in_josm()}` + ### steal Shows a tagRendering from a different object as if this was the object itself @@ -1182,26 +1222,13 @@ Shows a tagRendering from a different object as if this was the object itself | featureId | _undefined_ | The key of the attribute which contains the id of the feature from which to use the tags | | tagRenderingId | _undefined_ | The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection | -Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L23](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L23) +Defined +in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L23](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L23) #### Example usage of steal `{steal(,)}` -## ui - -Elements to support the user interface, e.g. 'title', 'translated' - -### preset_type_select - -An editable tag rendering which allows to change the type - -Defined in [/src/UI/Popup/DataVisualisations.ts#L231](/src/UI/Popup/DataVisualisations.ts#L231) - -#### Example usage of preset_type_select - -`{preset_type_select()}` - ## web_and_communication Tools to show data from external websites, which link to external websites or which link to external profiles @@ -1214,7 +1241,8 @@ Converts a fediverse username or link into a clickable link -----|-----|----- | | key | _undefined_ | The attribute-name containing the link | -Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L20](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L20) +Defined +in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L16](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L16) #### Example usage of fediverse_link @@ -1224,16 +1252,17 @@ Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisatio Construct a link. By using the 'special' visualisation notation, translations should be easier -| name | default | description | ------|-----|----- | -| text | _undefined_ | Text to be shown | -| href | _undefined_ | The URL to link to. Note that this will be URI-encoded before and (as everything) supports substitutions of attributes | -| class | _undefined_ | CSS-classes to add to the element | -| download | _undefined_ | Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button. | -| arialabel | _undefined_ | If set, this text will be used as aria-label | -| icon | _undefined_ | If set, show this icon next to the link. You might want to combine this with `class: button` | +| name | default | description | +-----------|-------------|---------------------------------------------------------------------------------------------------------------------------------------| +| text | _undefined_ | Text to be shown | +| href | _undefined_ | The URL to link to. Note that this will be URI-encoded before and (as everything) supports substitutions of attributes | +| class | _undefined_ | CSS-classes to add to the element | +| download | _undefined_ | Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button. | +| arialabel | _undefined_ | If set, this text will be used as aria-label | +| icon | _undefined_ | If set, show this icon next to the link. You might want to combine this with `class: button` | -Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L147](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L147) +Defined +in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L143](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L143) #### Example usage of link @@ -1253,26 +1282,6 @@ Defined in [/src/UI/Popup/MapillaryLinkVis.ts#L7](/src/UI/Popup/MapillaryLinkVis `{mapillary_link(18)}` -### open_in_iD - -Opens the current view in the iD-editor - -Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L212](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L212) - -#### Example usage of open_in_iD - -`{open_in_iD()}` - -### open_in_josm - -Opens the current view in the JOSM-editor - -Defined in [/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L226](/src/UI/SpecialVisualisations/TagrenderingManipulationSpecialVisualisations.ts#L226) - -#### Example usage of open_in_josm - -`{open_in_josm()}` - ### send_email Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email @@ -1284,7 +1293,7 @@ Creates a `mailto`-link where some fields are already set and correctly escaped. | body | _undefined_ | The text in the email | | button_text | _undefined_ | The text shown on the button in the UI | -Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L109](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L109) +Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L105](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L105) #### Example usage of send_email @@ -1298,7 +1307,8 @@ Shows the label of the corresponding wikidata-item -----|-----|----- | | keyToShowWikidataFor | wikidata | Use the wikidata entry from this key to show the label | -Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L68](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L68) +Defined +in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L64](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L64) #### Example usage of wikidata_label @@ -1312,7 +1322,8 @@ A box showing the corresponding wikipedia article(s) - based on the **wikidata** -----|-----|----- | | keyToShowWikipediaFor | wikidata;wikipedia | Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used | -Defined in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L39](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L39) +Defined +in [/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L35](/src/UI/SpecialVisualisations/WebAndCommunicationSpecialVisualisations.ts#L35) #### Example usage of wikipedia @@ -1326,7 +1337,7 @@ Various elements Exports the selected feature as GeoJson-file -Defined in [/src/UI/Popup/DataExportVisualisations.ts#L38](/src/UI/Popup/DataExportVisualisations.ts#L38) +Defined in [/src/UI/Popup/DataExportVisualisations.ts#L34](/src/UI/Popup/DataExportVisualisations.ts#L34) #### Example usage of export_as_geojson @@ -1336,7 +1347,7 @@ Defined in [/src/UI/Popup/DataExportVisualisations.ts#L38](/src/UI/Popup/DataExp Exports the selected feature as GPX-file -Defined in [/src/UI/Popup/DataExportVisualisations.ts#L12](/src/UI/Popup/DataExportVisualisations.ts#L12) +Defined in [/src/UI/Popup/DataExportVisualisations.ts#L8](/src/UI/Popup/DataExportVisualisations.ts#L8) #### Example usage of export_as_gpx @@ -1350,7 +1361,7 @@ Create a histogram for a list of given values, read from the properties. -----|-----|----- | | key | _undefined_ | The key to be read and to generate a histogram from | -Defined in [/src/UI/Popup/HistogramViz.ts#L11](/src/UI/Popup/HistogramViz.ts#L11) +Defined in [/src/UI/Popup/HistogramViz.ts#L7](/src/UI/Popup/HistogramViz.ts#L7) #### Example usage of histogram @@ -1360,14 +1371,14 @@ Defined in [/src/UI/Popup/HistogramViz.ts#L11](/src/UI/Popup/HistogramViz.ts#L11 The language element allows to show and pick all known (modern) languages. The key can be set -| name | default | description | ------|-----|----- | -| key | _undefined_ | What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `:nl=yes` if _nl_ is picked | -| question | _undefined_ | What to ask if no questions are known | -| render_list_item | {language()} | How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain). | -| render_single_language | _undefined_ | What will be shown if the feature only supports a single language | -| render_all | {list()} | The full rendering. U0se `{list}` to show where the list of languages must come. Optional if mode=single | -| no_known_languages | _undefined_ | The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead | +| name | default | description | +------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| key | _undefined_ | What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `:nl=yes` if _nl_ is picked | +| question | _undefined_ | What to ask if no questions are known | +| render_list_item | {language()} | How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain). | +| render_single_language | _undefined_ | What will be shown if the feature only supports a single language | +| render_all | {list()} | The full rendering. U0se `{list}` to show where the list of languages must come. Optional if mode=single | +| no_known_languages | _undefined_ | The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead | Defined in [/src/UI/Popup/LanguageElement/LanguageElement.ts#L5](/src/UI/Popup/LanguageElement/LanguageElement.ts#L5) @@ -1403,6 +1414,31 @@ Defined in [/src/UI/Popup/MultiApplyViz.ts#L7](/src/UI/Popup/MultiApplyViz.ts#L7 {multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)} +### preset_description + +Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty + +Defined in [/src/UI/Popup/DataVisualisations.ts#L207](/src/UI/Popup/DataVisualisations.ts#L207) + +#### Example usage of preset_description + +`{preset_description()}` + +### preset_type_select + +An editable tag rendering which allows to change the type. The options are the presets of the layer, effectively +allowing to change act as if the object was made with a different preset. For example + +How this element looks like (in question mode) for [ +`tourism_accomodation`](./Layers/tourism_accomodation.md): ![](./img/Special_preset_type_select_preview.png)The +presets ![](./img/Special_preset_type_select_matching_presets.png) + +Defined in [/src/UI/Popup/DataVisualisations.ts#L222](/src/UI/Popup/DataVisualisations.ts#L222) + +#### Example usage of preset_type_select + +`{preset_type_select()}` + ### upload_to_osm Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored. diff --git a/Docs/img/Special_preset_type_select_matching_presets.png b/Docs/img/Special_preset_type_select_matching_presets.png new file mode 100644 index 000000000..731e4bfbf Binary files /dev/null and b/Docs/img/Special_preset_type_select_matching_presets.png differ diff --git a/Docs/img/Special_preset_type_select_preview.png b/Docs/img/Special_preset_type_select_preview.png new file mode 100644 index 000000000..f752f46d4 Binary files /dev/null and b/Docs/img/Special_preset_type_select_preview.png differ diff --git a/android b/android index 817e8198b..dc3f3f5ac 160000 --- a/android +++ b/android @@ -1 +1 @@ -Subproject commit 817e8198b5e4c30572d7d3f082d60fc10a7be21e +Subproject commit dc3f3f5ac3d4d42bed7aeb58ff5386a063dbac06 diff --git a/assets/layers/arcade/arcade.json b/assets/layers/arcade/arcade.json new file mode 100644 index 000000000..2ab70ba65 --- /dev/null +++ b/assets/layers/arcade/arcade.json @@ -0,0 +1,115 @@ +{ + "id": "arcade", + "name": { + "en": "Arcades" + }, + "description": { + "en": "Layer showing arcades" + }, + "source": { + "osmTags": "leisure=amusement_arcade" + }, + "minzoom": 10, + "title": { + "render": { + "en": "Arcade" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "*": "{name}" + } + } + ] + }, + "pointRendering": [ + { + "location": [ + "point", + "centroid" + ], + "marker": [ + { + "icon": "square", + "color": "white" + }, + { + "icon": "./assets/layers/arcade/arcade.svg" + } + ] + } + ], + "lineRendering": [ + { + "width": 3, + "color": "#0e8517" + } + ], + "presets": [ + { + "title": { + "en": "an arcade" + }, + "tags": [ + "leisure=amusement_arcade" + ] + } + ], + "tagRenderings": [ + "images", + "reviews", + { + "builtin": "name", + "override": { + "question": { + "en": "What is the name of this arcade?" + }, + "render": { + "en": "This arcade is called {name}" + } + } + }, + { + "id": "virtual_reality", + "question": { + "en": "Does this arcade offer virtual-reality gaming?" + }, + "mappings": [ + { + "if": "virtual_reality=yes", + "then": { + "en": "This arcade offers virtual-reality gaming." + } + }, + { + "if": "virtual_reality=only", + "then": { + "en": "This arcade only offers virtual-reality gaming." + } + }, + { + "if": "virtual_reality=", + "then": { + "en": "This arcade doesn't offer virtual-reality gaming" + } + } + ] + }, + "brand", + "opening_hours", + "website", + "email", + "phone", + "payment-options", + "level", + "description", + "toilet_at_amenity_lib.all" + ], + "allowMove": { + "enableImproveAccuracy": true, + "enableRelocation": true + }, + "credits": "Robin van der Linde", + "credits:uid": 5093765 +} \ No newline at end of file diff --git a/assets/layers/arcade/arcade.svg b/assets/layers/arcade/arcade.svg new file mode 100644 index 000000000..5f4031683 --- /dev/null +++ b/assets/layers/arcade/arcade.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/layers/arcade/arcade.svg.license b/assets/layers/arcade/arcade.svg.license new file mode 100644 index 000000000..ff6ee9492 --- /dev/null +++ b/assets/layers/arcade/arcade.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: meased +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/arcade/license_info.json b/assets/layers/arcade/license_info.json new file mode 100644 index 000000000..f82b95ae4 --- /dev/null +++ b/assets/layers/arcade/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "arcade.svg", + "license": "CC0-1.0", + "authors": [ + "meased" + ], + "sources": [ + "https://github.com/gravitystorm/openstreetmap-carto/blob/master/symbols/leisure/amusement_arcade.svg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/campsite/campsite.json b/assets/layers/campsite/campsite.json index bced66605..d29e848c4 100644 --- a/assets/layers/campsite/campsite.json +++ b/assets/layers/campsite/campsite.json @@ -280,41 +280,12 @@ ] } }, - "caravansites.caravansites-toilets", "toilet_at_amenity_lib.all", "questions", "mastodon" ], "filter": [ - { - "id": "fee_filter", - "options": [ - { - "question": { - "en": "Fee", - "ca": "Taxa", - "cs": "Poplatek", - "cy": "Ffi", - "de": "Gebühr", - "it": "A pagamento" - } - }, - { - "question": { - "en": "free of charge", - "ca": "Gratuït", - "cs": "zdarma", - "de": "kostenlos", - "it": "gratuito" - }, - "osmTags": { - "and": [ - "fee=no" - ] - } - } - ] - }, + "free", { "id": "capacity_persons_filter", "options": [ diff --git a/assets/layers/caravansites/caravansites.json b/assets/layers/caravansites/caravansites.json index 1be94c4e2..4240ceb99 100644 --- a/assets/layers/caravansites/caravansites.json +++ b/assets/layers/caravansites/caravansites.json @@ -666,85 +666,7 @@ ] } }, - { - "id": "caravansites-toilets", - "question": { - "en": "Does this place have toilets?", - "ca": "Aquest lloc té lavabos?", - "cs": "Má toto místo toalety?", - "da": "Har dette sted toiletter?", - "de": "Verfügt dieser Ort über Toiletten?", - "es": "¿Este lugar tiene baños?", - "fr": "Y-a-t’il des toilettes sur le site ?", - "hu": "Van-e itt WC?", - "it": "Questo posto ha servizi igienici?", - "ja": "ここにトイレはありますか?", - "nb_NO": "Har dette stedet toaletter?", - "nl": "Heeft deze plaats toiletten?", - "pl": "Czy to miejsce ma toalety?", - "pt": "Este lugar tem casas de banho?", - "pt_BR": "Este lugar tem banheiros?", - "ru": "Здесь есть туалеты?", - "zh_Hant": "這個地方有廁所嗎?" - }, - "mappings": [ - { - "if": { - "and": [ - "toilets=yes" - ] - }, - "then": { - "en": "This place has toilets", - "ca": "Aquest lloc té lavabos", - "cs": "Toto místo má toalety", - "da": "Dette sted har toiletter", - "de": "Dieser Ort verfügt über Toiletten", - "es": "Este lugar tiene baños", - "fr": "Ce site a des toilettes", - "hu": "Itt van WC", - "id": "Tempat sini ada tandas", - "it": "Questo posto ha servizi igienici", - "ja": "ここにはトイレがある", - "nb_NO": "Dette stedet har toalettfasiliteter", - "nl": "Deze plaats heeft toiletten", - "pl": "To miejsce ma toalety", - "pt": "Este lugar tem casa de banho", - "pt_BR": "Este lugar tem banheiros", - "ru": "В этом месте есть туалеты", - "zh_Hant": "這個地方有廁所" - } - }, - { - "if": { - "and": [ - "toilets=no" - ] - }, - "then": { - "en": "This place does not have toilets", - "ca": "Aquest lloc no té lavabos", - "cs": "Toto místo nemá toalety", - "da": "Dette sted har ikke toiletter", - "de": "Dieser Ort verfügt nicht über Toiletten", - "es": "Este lugar no tiene baños", - "eu": "Toki honek ez dauka komunik", - "fr": "Ce site n’a pas de toilettes", - "hu": "Itt nincs WC", - "id": "Tempat sini tiada tandas", - "it": "Questo posto non ha servizi igienici", - "ja": "ここにはトイレがない", - "nb_NO": "Dette stedet har ikke toalettfasiliteter", - "nl": "Deze plaats heeft geen toiletten", - "pl": "To miejsce nie ma toalet", - "pt": "Este lugar não tem casas de banho", - "pt_BR": "Este lugar não tem banheiros", - "ru": "В этом месте нет туалетов", - "zh_Hant": "這個地方並沒有廁所" - } - } - ] - }, + "has_toilets", { "id": "caravansites-website", "question": { @@ -935,5 +857,8 @@ "questions", "reviews" ], + "filter": [ + "free" + ], "allowMove": true } diff --git a/assets/layers/current_view/current_view.json b/assets/layers/current_view/current_view.json index 0d57a35f1..877d0daad 100644 --- a/assets/layers/current_view/current_view.json +++ b/assets/layers/current_view/current_view.json @@ -5,6 +5,7 @@ "shownByDefault": false, "title": "Current View", "popupInFloatover": true, + "titleIcons": [], "pointRendering": [], "lineRendering": [ { diff --git a/assets/layers/diets/diets.json b/assets/layers/diets/diets.json index 7b82778bf..e5acaef47 100644 --- a/assets/layers/diets/diets.json +++ b/assets/layers/diets/diets.json @@ -692,4 +692,4 @@ } } ] -} +} \ No newline at end of file diff --git a/assets/layers/filters/filters.json b/assets/layers/filters/filters.json index 591e40305..f58d3333b 100644 --- a/assets/layers/filters/filters.json +++ b/assets/layers/filters/filters.json @@ -554,7 +554,46 @@ "osmTags": "kids_area!=no" } ] + }, + { + "id": "self_checkout", + "options": [ + { + "question": { + "en": "Has self-checkout", + "nl": "Heeft zelfscan" + }, + "osmTags": { + "or": [ + "self_checkout=yes", + "self_checkout=only" + ] + } + } + ] + }, + { + "id": "shelter", + "options": [ + { + "osmTags": { + "or": [ + "shelter=yes", + "shelter=separate" + ] + }, + "question": { + "en": "With a shelter", + "ca": "Amb refugi", + "cs": "S přístřeškem", + "de": "Mit Unterstand", + "es": "Con refugio", + "fr": "Avec un abri", + "it": "Con una pensilina" + } + } + ] } ], "allowMove": false -} +} \ No newline at end of file diff --git a/assets/layers/grab_rail/grab_rail.json b/assets/layers/grab_rail/grab_rail.json index 5c7d212ed..da6181c8f 100644 --- a/assets/layers/grab_rail/grab_rail.json +++ b/assets/layers/grab_rail/grab_rail.json @@ -19,6 +19,9 @@ "it": "C'è un maniglione?", "nl": "Is er een handgreep of grijpbeugel?" }, + "questionHint": { + "en": "Left and right are interpreted as when sitting on the toilet" + }, "mappings": [ { "if": { @@ -164,6 +167,10 @@ "it": "Il maniglione {{TRANSL}} è pieghevole?", "nl": "Is de grijpbeugel aan de {{TRANSL}}kant opklapbaar?" }, + "questionHint": { + "en": "Left and right are as seen when sitting on the toilet", + "nl": "Links en rechts is wanneer men op de toilet zit" + }, "mappings": [ { "if": "grab_rail:foldable:LOCATION=yes", diff --git a/assets/layers/hut/alpine_hut.svg b/assets/layers/hut/alpine_hut.svg new file mode 100644 index 000000000..e790edd62 --- /dev/null +++ b/assets/layers/hut/alpine_hut.svg @@ -0,0 +1,15 @@ + + + + + + image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/assets/layers/hut/alpine_hut.svg.license b/assets/layers/hut/alpine_hut.svg.license new file mode 100644 index 000000000..5e3616ed7 --- /dev/null +++ b/assets/layers/hut/alpine_hut.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Geozeisig +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/hut/hut.json b/assets/layers/hut/hut.json new file mode 100644 index 000000000..3d126ea6f --- /dev/null +++ b/assets/layers/hut/hut.json @@ -0,0 +1,179 @@ +{ + "id": "hut", + "name": { + "en": "Huts" + }, + "description": { + "en": "Layer showing basic huts, wilderness huts and alpine huts" + }, + "source": { + "osmTags": { + "or": [ + "tourism=wilderness_hut", + "tourism=alpine_hut", + { + "and": [ + "amenity=shelter", + "shelter_type=basic_hut" + ] + } + ] + } + }, + "minzoom": 10, + "title": { + "render": "Hut", + "mappings": [ + { + "if": "name~*", + "then": "{name}" + }, + { + "if": "tourism=wilderness_hut", + "then": "wilderness hut" + }, + { + "if": "tourism=alpine_hut", + "then": "alpine hut" + }, + { + "if": { + "and": [ + "amenity=shelter", + "shelter_type=basic_hut" + ] + }, + "then": "basic hut" + } + ] + }, + "pointRendering": [ + { + "location": [ + "point", + "centroid" + ], + "marker": [ + { + "icon": { + "render": "./assets/layers/shelter/shelter.svg", + "mappings": [ + { + "if": "tourism=wilderness_hut", + "then": "./assets/layers/hut/wilderness_hut.svg" + }, + { + "if": "tourism=alpine_hut", + "then": "./assets/layers/hut/alpine_hut.svg" + }, + { + "if": { + "and": [ + "amenity=shelter", + "shelter_type=basic_hut" + ] + }, + "then": "./assets/layers/shelter/shelter.svg" + } + ] + } + } + ] + } + ], + "lineRendering": [], + "presets": [ + { + "tags": [ + "tourism=wilderness_hut" + ], + "title": { + "en": "wilderness hut" + }, + "description": { + "en": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas and a fireplace or stove for heating and cooking." + } + }, + { + "tags": [ + "tourism=alpine_hut" + ], + "title": { + "en": "alpine hut" + }, + "description": { + "en": "A serviced remote building located in the mountains intended to provide board and lodging." + } + }, + { + "tags": [ + "amenity=shelter", + "shelter_type=basic_hut" + ], + "title": { + "en": "basic hut" + }, + "description": { + "en": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas without a fireplace or stove." + } + } + ], + "tagRenderings": [ + "images", + "name", + { + "builtin": "website", + "override": { + "condition": "tourism=wilderness_hut", + "id": "website-single" + } + }, + { + "builtin": "contact", + "override": { + "condition": "tourism=alpine_hut" + } + }, + "reservation", + "caravansites.caravansites-fee", + { + "id": "drinking_water", + "question": { + "en": "Is drinking water available here?" + }, + "mappings": [ + { + "if": "drinking_water=yes", + "then": { + "en": "Here is drinking water available." + } + }, + { + "if": "drinking_water=no", + "then": { + "en": "Here is no drinking water available." + } + } + ] + }, + "has_toilets", + "description", + { + "id": "preset_type", + "render": "{preset_type_select()}" + }, + { + "builtin": "shelter.shelter-type", + "override": { + "condition": "amenity=shelter" + } + } + ], + "filter":[ + "free" + ], + "allowMove": { + "enableRelocation": false, + "enableImproveAccuracy": true + } +} diff --git a/assets/layers/hut/license_info.json b/assets/layers/hut/license_info.json new file mode 100644 index 000000000..f8341f4f8 --- /dev/null +++ b/assets/layers/hut/license_info.json @@ -0,0 +1,22 @@ +[ + { + "path": "alpine_hut.svg", + "license": "CC0-1.0", + "authors": [ + "Geozeisig" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Alpinehut.svg" + ] + }, + { + "path": "wilderness_hut.svg", + "license": "CC0-1.0", + "authors": [ + "Geozeisig" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Wilderness_hut.svg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/hut/wilderness_hut.svg b/assets/layers/hut/wilderness_hut.svg new file mode 100644 index 000000000..cbf146077 --- /dev/null +++ b/assets/layers/hut/wilderness_hut.svg @@ -0,0 +1,15 @@ + + + + + + image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/assets/layers/hut/wilderness_hut.svg.license b/assets/layers/hut/wilderness_hut.svg.license new file mode 100644 index 000000000..5e3616ed7 --- /dev/null +++ b/assets/layers/hut/wilderness_hut.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Geozeisig +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/parking_spaces/parking_spaces.json b/assets/layers/parking_spaces/parking_spaces.json index 64e5f907e..0c36e859b 100644 --- a/assets/layers/parking_spaces/parking_spaces.json +++ b/assets/layers/parking_spaces/parking_spaces.json @@ -69,6 +69,15 @@ "icon": { "render": "./assets/layers/parking_spaces/parking_space.svg", "mappings": [ + { + "if": { + "or": [ + "access=private", + "access=no" + ] + }, + "then": "./assets/layers/parking_spaces/parking_space_private.svg" + }, { "if": "parking_space=disabled", "then": "./assets/layers/toilet/wheelchair.svg" @@ -99,7 +108,7 @@ ], "lineRendering": [ { - "color": "#696969", + "color": "dimgray", "width": "1" } ], @@ -295,6 +304,69 @@ "it": "Questo è un posto auto riservato al car sharing.", "nl": "Deze parkeerplek is gereserveerd voor autodelen." } + }, + { + "if": "parking_space=women", + "then": { + "en": "This is a parking space reserved for women.", + "nl": "Deze parkeerplek is gereserveerd voor vrouwen." + } + } + ] + }, + { + "id": "access", + "question": { + "en": "Who can use this parking space?", + "nl": "Wie mag deze parkeerplek gebruiken?" + }, + "render": { + "en": "Access of parking space: {access}", + "nl": "Toegang tot parkeerplek: {access}" + }, + "freeform": { + "key": "access", + "type": "string", + "addExtraTags": [ + "fixme=Freeform used on 'access'-tag: possibly a wrong value" + ] + }, + "mappings": [ + { + "if": "access=", + "then": { + "en": "Anyone can use this parking space.", + "nl": "Iedereen kan deze parkeerplek gebruiken." + }, + "hideInAnswer": true + }, + { + "if": "access=yes", + "then": { + "en": "Anyone can use this parking space.", + "nl": "Iedereen kan deze parkeerplek gebruiken." + } + }, + { + "if": "access=customers", + "then": { + "en": "This parking space is reserved for customers.", + "nl": "Deze parkeerplek is gereserveerd voor klanten." + } + }, + { + "if": "access=private", + "then": { + "en": "This parking space is private and cannot be used by the general public.", + "nl": "Deze parkeerplek is privé en mag niet door het grote publiek worden gebruikt." + } + }, + { + "if": "access=permit", + "then": { + "en": "This parking space is reserved for permit holders.", + "nl": "Deze parkeerplek is gereserveerd voor vergunninghouders." + } } ] }, @@ -310,6 +382,19 @@ "nl": "Deze parkeerplek heeft {capacity} plaatsen." }, "mappings": [ + { + "if": "capacity=", + "then": { + "en": "This parking space has 1 space.", + "ca": "Aquest espai d'aparcament té 1 plaça.", + "cs": "Toto parkoviště má 1 místo.", + "de": "Dieser Parkplatz hat 1 Stellplatz.", + "es": "Esta plaza de aparcamiento tiene 1 plaza.", + "it": "Questo posto auto ha 1 spazio.", + "nl": "Deze parkeerplek heeft 1 plaats." + }, + "hideInAnswer": true + }, { "if": "capacity=1", "then": { @@ -329,4 +414,4 @@ "enableImproveAccuracy": true, "enableRelocation": false } -} +} \ No newline at end of file diff --git a/assets/layers/picnic_site/picnic_site.json b/assets/layers/picnic_site/picnic_site.json new file mode 100644 index 000000000..ef02be70b --- /dev/null +++ b/assets/layers/picnic_site/picnic_site.json @@ -0,0 +1,304 @@ +{ + "id": "picnic_site", + "name": { + "en": "Picnic sites", + "nl": "Picknickplaatsen" + }, + "description": { + "en": "Picnic sites for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters", + "nl": "Picknickplaatsen voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen" + }, + "source": { + "osmTags": "tourism=picnic_site" + }, + "minzoom": 10, + "title": { + "render": { + "en": "Picnic site", + "nl": "Picknickplaats" + } + }, + "pointRendering": [ + { + "iconSize": "35,35", + "location": [ + "point", + "centroid" + ], + "anchor": "center", + "marker": [ + { + "color": "#3984e6", + "icon": "circle" + }, + { + "icon": "./assets/layers/picnic_table/picnic_table.svg" + } + ] + } + ], + "lineRendering": [ + { + "color": "#3984e6", + "fillColor": "#3984e6bd", + "width": 5 + } + ], + "presets": [ + { + "tags": [ + "tourism=picnic_site" + ], + "title": { + "en": "a picnic site", + "nl": "een picknickplaats" + }, + "description": { + "en": "A picnic site for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters", + "nl": "Een picknickplaats voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen" + } + } + ], + "tagRenderings": [ + "images", + { + "builtin": "name", + "override": { + "render": { + "en": "This picnic site is called {name}", + "nl": "Deze picknickplaats heet {name}" + } + } + }, + { + "id": "shelter", + "question": { + "en": "Does this picnic site have a shelter?", + "nl": "Heeft deze picknickplaats een schuilplaats?" + }, + "mappings": [ + { + "if": "shelter=yes", + "then": { + "en": "This picnic site has a shelter.", + "nl": "Deze picknickplaats heeft een schuilplaats." + } + }, + { + "if": "shelter=no", + "then": { + "en": "This picnic site does not have a shelter.", + "nl": "Deze picknickplaats heeft geen schuilplaats." + } + }, + { + "if": "shelter=separate", + "then": { + "en": "This picnic site has a shelter, but is is mapped as a different icon.", + "nl": "Deze picknickplaats heeft een schuilplaats, maar deze staat los op de kaart." + } + } + ] + }, + { + "id": "fireplace", + "question": { + "en": "Does this picnic site have a firepit?", + "nl": "Heeft deze picknickplaats een vuurplaats?" + }, + "mappings": [ + { + "if": "fireplace=yes", + "then": { + "en": "This picnic site has a firepit.", + "nl": "Deze picknickplaats heeft een vuurplaats." + } + }, + { + "if": "fireplace=no", + "then": { + "en": "This picnic site does not have a firepit.", + "nl": "Deze picknickplaats heeft geen vuurplaats." + } + }, + { + "if": "fireplace=separate", + "then": { + "en": "This picnic site has a firepit, but it is mapped as a different icon.", + "nl": "Deze picknickplaats heeft een vuurplaats, maar deze staat los op de kaart." + } + } + ] + }, + { + "id": "bbq", + "question": { + "en": "Does this picnic site have a BBQ?", + "nl": "Heeft deze picknickplaats een BBQ?" + }, + "mappings": [ + { + "if": "bbq=yes", + "then": { + "en": "This picnic site has a BBQ.", + "nl": "Deze picknickplaats heeft een BBQ." + } + }, + { + "if": "bbq=no", + "then": { + "en": "This picnic site does not have a BBQ.", + "nl": "Deze picknickplaats heeft geen BBQ." + } + }, + { + "if": "bbq=separate", + "then": { + "en": "This picnic site has a BBQ, but it is mapped as a different icon.", + "nl": "Deze picknickplaats heeft een BBQ, maar deze staat los op de kaart." + } + } + ] + }, + { + "id": "covered", + "question": { + "en": "Is this picnic site covered?", + "nl": "Is deze picknickplaats overdekt?" + }, + "mappings": [ + { + "if": "covered=yes", + "then": { + "en": "This picnic site is covered.", + "nl": "Deze picknickplaats is overdekt." + } + }, + { + "if": "covered=no", + "then": { + "en": "This picnic site is not covered.", + "nl": "Deze picknickplaats is niet overdekt." + } + } + ] + }, + { + "id": "drinking_water", + "question": { + "en": "Does this picnic site have drinking water?", + "nl": "Heeft deze picknickplaats drinkwater?" + }, + "mappings": [ + { + "if": "drinking_water=yes", + "then": { + "en": "This picnic site has drinking water.", + "nl": "Deze picknickplaats heeft drinkwater." + } + }, + { + "if": "drinking_water=no", + "then": { + "en": "This picnic site does not have drinking water.", + "nl": "Deze picknickplaats heeft geen drinkwater." + } + }, + { + "if": "drinking_water=separate", + "then": { + "en": "This picnic site has drinking water, but it is mapped as a different icon.", + "nl": "Deze picknickplaats heeft drinkwater, maar deze staat los op de kaart." + } + } + ] + }, + { + "id": "openfire", + "question": { + "en": "Is open fire allowed at this picnic site?", + "nl": "Is open vuur toegestaan op deze picknickplaats?" + }, + "mappings": [ + { + "if": "openfire=yes", + "then": { + "en": "Open fire is allowed at this picnic site.", + "nl": "Open vuur is toegestaan op deze picknickplaats." + } + }, + { + "if": "openfire=no", + "then": { + "en": "Open fire is not allowed at this picnic site.", + "nl": "Open vuur is niet toegestaan op deze picknickplaats." + } + }, + { + "if": "openfire=permit", + "then": { + "en": "Open fire is allowed at this picnic site with a permit.", + "nl": "Open vuur is toegestaan op deze picknickplaats met een vergunning." + } + } + ] + } + ], + "filter": [ + "shelter", + { + "id": "fireplace", + "options": [ + { + "question": { + "en": "With a firepit", + "nl": "Met een vuurplaats" + }, + "osmTags": { + "or": [ + "fireplace=yes", + "fireplace=separate" + ] + } + } + ] + }, + { + "id": "bbq", + "options": [ + { + "question": { + "en": "With a BBQ", + "nl": "Met een BBQ" + }, + "osmTags": { + "or": [ + "bbq=yes", + "bbq=separate" + ] + } + } + ] + }, + { + "id": "drinking_water", + "options": [ + { + "question": { + "en": "With drinking water", + "nl": "Met drinkwater" + }, + "osmTags": { + "or": [ + "drinking_water=yes", + "drinking_water=separate" + ] + } + } + ] + } + ], + "allowMove": { + "enableImproveAccuracy": true + } +} \ No newline at end of file diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index 69bd3b831..d33a86485 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -3118,7 +3118,8 @@ "if": "nobrand=yes", "addExtraTags": [ "brand=", - "brand:wikidata=" + "brand:wikidata=", + "brand:wikipedia=" ], "then": { "en": "Not part of a bigger brand", @@ -3567,6 +3568,81 @@ "filter": [ "filters.kids_area" ] + }, + { + "id": "self_checkout", + "labels": [ + "self_checkout_questions" + ], + "question": { + "en": "Does this place offer self-checkout?", + "nl": "Biedt deze plaats zelfscannen aan?" + }, + "questionHint": { + "en": "e.g. handheld scanners or a self-checkout kiosk", + "nl": "bijv. handscanners of een zelfscankassa" + }, + "mappings": [ + { + "if": "self_checkout=yes", + "then": { + "en": "This place offers self-checkout", + "nl": "Deze plaats biedt zelfscannen aan" + } + }, + { + "if": "self_checkout=no", + "then": { + "en": "This place does not offer self-checkout", + "nl": "Deze plaats biedt geen zelfscannen aan" + } + }, + { + "if": "self_checkout=only", + "then": { + "en": "This place only offers self-checkout", + "nl": "Deze plaats biedt enkel zelfscannen aan" + } + } + ], + "filter": [ + "filters.self_checkout" + ] + }, + { + "id": "self_checkout_type", + "labels": [ + "self_checkout_questions" + ], + "question": { + "en": "What kind of self-checkout does this place offer?", + "nl": "Wat voor soort zelfscannen biedt deze plaats aan?" + }, + "mappings": [ + { + "if": "self_checkout:handheld=yes", + "ifnot": "self_checkout:handheld=no", + "then": { + "en": "This place offers self-checkout using a handheld scanner", + "nl": "Deze plaats biedt zelfscannen met een handscanner aan" + } + }, + { + "if": "self_checkout:self_scan=yes", + "ifnot": "self_checkout:self_scan=no", + "then": { + "en": "This place offers self-checkout using a self-checkout kiosk", + "nl": "Deze plaats biedt zelfscannen met een zelfscankassa aan" + } + } + ], + "condition": { + "or": [ + "self_checkout=yes", + "self_checkout=only" + ] + }, + "multiAnswer": true } ], "allowMove": false, @@ -3582,4 +3658,4 @@ } } ] -} +} \ No newline at end of file diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index 332a9532a..a8c63e6ef 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -43,7 +43,8 @@ "craft=key_cutter" ] }, - "shop!=mall" + "shop!=mall", + "shop!=no" ] } }, @@ -486,7 +487,12 @@ "es": "Esta tienda no tiene una marca específica, no forma parte de una cadena más grande", "it": "Questo negozio non ha un marchio specifico, non fa parte di una catena più grande", "uk": "Цей магазин не має певного бренду, він не є частиною великої мережі" - } + }, + "addExtraTags": [ + "brand=", + "brand:wikidata=", + "brand:wikipedia=" + ] } ] }, @@ -1666,6 +1672,28 @@ } } }, + { + "builtin": "self_checkout", + "override": { + "+mappings": [ + { + "if": { + "and": [ + "self_checkout=", + "shop!=supermarket", + "shop!=convenience", + "shop!=chemist" + ] + }, + "then": { + "en": "This shop (probably) does not offer self-checkout" + }, + "hideInAnswer": true + } + ] + } + }, + "self_checkout_type", "description", "toilet_at_amenity_lib.all" ], @@ -1739,4 +1767,4 @@ ] }, "allowMove": true -} +} \ No newline at end of file diff --git a/assets/layers/summary/summary.json b/assets/layers/summary/summary.json index def0bd887..60dcf945c 100644 --- a/assets/layers/summary/summary.json +++ b/assets/layers/summary/summary.json @@ -29,8 +29,8 @@ } ] }, - "labelCss": "background: #ffffffbb", - "labelCssClasses": "w-12 text-lg rounded-xl p-1 px-2" + "labelCss": "background: #ffffffcc; min-width: 2rem", + "labelCssClasses": "text-lg rounded p-1 px-2 border-2 border-gray font-bold" } ], "tagRenderings": [ diff --git a/assets/layers/surveillance_camera/surveillance_camera.json b/assets/layers/surveillance_camera/surveillance_camera.json index 2ecaf1df5..e19ab6d52 100644 --- a/assets/layers/surveillance_camera/surveillance_camera.json +++ b/assets/layers/surveillance_camera/surveillance_camera.json @@ -87,6 +87,10 @@ "if": "camera:type=doorbell", "then": "./assets/layers/surveillance_camera/doorbell.svg" }, + { + "if": "camera:type=panorama", + "then": "./assets/themes/surveillance/panorama.svg" + }, { "if": "_direction:leftright=right", "then": "./assets/themes/surveillance/cam_right.svg" @@ -110,6 +114,10 @@ ] }, "then": "50,35,center" + }, + { + "if": "camera:type=panorama", + "then": "55,55,center" } ], "render": "35,35,center" @@ -407,6 +415,16 @@ "ru": "Панорамная камера" } }, + { + "if": "camera:type=panorama", + "icon": "./assets/themes/surveillance/panorama.svg", + "then": { + "en": "A 360° camera", + "de": "Eine 360°-Kamera", + "es": "Una cámara de 360°", + "fr": "Une caméra 360°" + } + }, { "if": "camera:type=doorbell", "icon": { @@ -530,7 +548,7 @@ "da": "Hvilken form for overvågning er dette kamera?", "de": "Was überwacht diese Kamera?", "es": "¿Qué tipo de vigilancia es esta cámara?", - "fr": "De quel genre de surveillance cette caméra est-elle ?", + "fr": "De quel genre de surveillance cette caméra est-elle ?", "it": "Che tipo di sorveglianza è questa telecamera?", "nl": "Wat soort bewaking wordt hier uitgevoerd?", "sl": "Kaj nadzoruje ta kamera?" diff --git a/assets/layers/transit_stops/transit_stops.json b/assets/layers/transit_stops/transit_stops.json index d9d045554..9874e52a7 100644 --- a/assets/layers/transit_stops/transit_stops.json +++ b/assets/layers/transit_stops/transit_stops.json @@ -548,28 +548,7 @@ } ], "filter": [ - { - "id": "shelter", - "options": [ - { - "osmTags": { - "or": [ - "shelter=yes", - "shelter=separate" - ] - }, - "question": { - "en": "With a shelter", - "ca": "Amb refugi", - "cs": "S přístřeškem", - "de": "Mit Unterstand", - "es": "Con refugio", - "fr": "Avec un abri", - "it": "Con una pensilina" - } - } - ] - }, + "shelter", { "id": "bench", "options": [ @@ -617,4 +596,4 @@ "tactile_paving" ], "allowMove": false -} +} \ No newline at end of file diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index a02fcba97..2a812bb1a 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -1906,6 +1906,46 @@ "render": { "*": "{storage_all_tags()}" } + }, + { + "id": "debug_serviceworker_accordeon", + "render": { + "special": { + "header": "debug_serviceworker_accordeon_title", + "labels": "debug_serviceworker", + "type": "group" + } + }, + "condition": "mapcomplete-show_debug=yes" + }, + { + "id": "debug_serviceworker_accordeon_title", + "labels": [ + "hidden" + ], + "render": { + "en": "Debug information about the service worker" + } + }, + { + "id": "expl", + "labels": [ + "debug_serviceworker", + "hidden" + ], + "render": { + "en": "To clear the service worker data, use the 'clear caches' button" + } + }, + { + "id": "service_worker_tags", + "labels": [ + "debug_serviceworker", + "hidden" + ], + "render": { + "*": "{serviceworker_all_tags()}" + } } ], "allowMove": false diff --git a/assets/themes/arcade/arcade.json b/assets/themes/arcade/arcade.json new file mode 100644 index 000000000..137cdf86a --- /dev/null +++ b/assets/themes/arcade/arcade.json @@ -0,0 +1,13 @@ +{ + "id": "arcade", + "title": { + "en": "Arcades" + }, + "description": { + "en": "A map of arcades" + }, + "icon": "./assets/layers/arcade/arcade.svg", + "layers": [ + "arcade" + ] +} \ No newline at end of file diff --git a/assets/themes/grb/grb.json b/assets/themes/grb/grb.json index 78405b1b6..e4f2698c3 100644 --- a/assets/themes/grb/grb.json +++ b/assets/themes/grb/grb.json @@ -701,6 +701,7 @@ { "builtin": "current_view", "override": { + "popupInFloatover": "title", "calculatedTags+": [ "_overlapping=Number(feat.properties.zoom) >= 14 ? overlapWith(feat)('grb').map(ff => ff.feat.properties) : undefined", "_applicable_conflate=get(feat)('_overlapping')?.filter(p => p._imported !== 'yes' && (!p['_imported_osm_still_fresh'] || !p['_imported_osm_object_found']) && p['_overlap_absolute'] > 10 && p['_overlap_percentage'] > 80 && p['_reverse_overlap_percentage'] > 80)?.map(p => p.id)", diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 5b19fa6de..80382fd51 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -38,6 +38,9 @@ "zh_Hant": "顯示由MapComplete進行的變動" }, "icon": "./assets/svg/logo.svg", + "startZoom": 1, + "startLat": 0, + "startLon": 0, "hideFromOverview": true, "layers": [ { diff --git a/assets/themes/nature/nature.json b/assets/themes/nature/nature.json index 7158a1d5e..a91c126a1 100644 --- a/assets/themes/nature/nature.json +++ b/assets/themes/nature/nature.json @@ -60,12 +60,14 @@ "nature_reserve", { "builtin": [ + "hut", "shelter" ], "override": { "minzoom": 11 } }, + "picnic_site", { "builtin": [ "map", diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json index 3c2318ab1..3801f0549 100644 --- a/assets/themes/onwheels/onwheels.json +++ b/assets/themes/onwheels/onwheels.json @@ -598,6 +598,7 @@ { "builtin": "current_view", "override": { + "popupInFloatover": "title", "+pointRendering": [ { "location": [ diff --git a/assets/themes/parkings/parkings.json b/assets/themes/parkings/parkings.json index 0e3480033..bb1ec8692 100644 --- a/assets/themes/parkings/parkings.json +++ b/assets/themes/parkings/parkings.json @@ -65,7 +65,10 @@ "parking_spaces", "parking_ticket_machine", { - "builtin": "charging_station", + "builtin": [ + "charging_station", + "charge_point" + ], "override": { "minzoom": 18 } diff --git a/assets/themes/surveillance/license_info.json b/assets/themes/surveillance/license_info.json index 53a9f3c25..4b40d545c 100644 --- a/assets/themes/surveillance/license_info.json +++ b/assets/themes/surveillance/license_info.json @@ -30,5 +30,13 @@ "Pieter Vander Vennet" ], "sources": [] + }, + { + "path": "panorama.svg", + "license": "CC0-1.0", + "authors": [ + "Martin Bodin" + ], + "sources": [] } ] \ No newline at end of file diff --git a/assets/themes/surveillance/panorama.svg b/assets/themes/surveillance/panorama.svg new file mode 100644 index 000000000..6e135df24 --- /dev/null +++ b/assets/themes/surveillance/panorama.svg @@ -0,0 +1,109 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/surveillance/panorama.svg.license b/assets/themes/surveillance/panorama.svg.license new file mode 100644 index 000000000..27a495574 --- /dev/null +++ b/assets/themes/surveillance/panorama.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Martin Bodin +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/themes/zhv/zhv.json b/assets/themes/zhv/zhv.json index 2138c01f7..f5d67e07c 100644 --- a/assets/themes/zhv/zhv.json +++ b/assets/themes/zhv/zhv.json @@ -56,7 +56,7 @@ "render": { "special": { "type": "import_button", - "targetLayer": "transit_stops", + "targetLayer": "all_transit_stops", "tags": "_tags", "text": { "en": "Add this stop", diff --git a/index.html b/index.html index 04cf21dcf..796ba73ec 100644 --- a/index.html +++ b/index.html @@ -43,8 +43,6 @@ - - diff --git a/langs/en.json b/langs/en.json index 19daa1127..196b2de20 100644 --- a/langs/en.json +++ b/langs/en.json @@ -19,6 +19,7 @@ "allFilteredAway": "No feature in view meets all filters", "loadingData": "Loading data…", "noData": "There are no relevant features in the current view", + "noDataOffline": "No data is loaded and you are offline", "ready": "Done!", "retrying": "Loading data failed. Trying again in {count} seconds…", "zoomIn": "Zoom in to view or edit the data" @@ -319,6 +320,7 @@ "menu": { "aboutCurrentThemeTitle": "About this map", "aboutMapComplete": "About MapComplete", + "downloadApp": "Download the app for Android", "filter": "Filter data", "legal": "Legal notices", "moreUtilsTitle": "Discover more", @@ -339,6 +341,7 @@ "next": "Next", "noTagsSelected": "No tags selected", "number": "number", + "offline": "Your device is offline", "openTheMap": "Open the map", "openTheMapReason": "to view, edit and add information", "opening_hours": { @@ -642,6 +645,8 @@ "uploading": "{count} images are being uploaded…" }, "noBlur": "Images will not be blurred. Do not photograph people", + "offline": "You are currently offline. Uploading images be attempted when your internet is back", + "offlinePending": "{count} images are currently in the queue", "one": { "done": "Your image was successfully uploaded. Thank you!", "failed": "Sorry, we could not upload your image", @@ -656,7 +661,7 @@ "confirmDeleteTitle": "Delete this image?", "delete": "Delete this image", "intro": "The following images are queued for upload", - "menu": "Image upload queue ({count})", + "menu": "Pending changes and image uploads ({count})", "noFailedImages": "There are currently no images in the upload queue", "retryAll": "Retry uploading all images" }, @@ -980,4 +985,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} \ No newline at end of file +} diff --git a/langs/layers/ca.json b/langs/layers/ca.json index e4ac8aee9..a68397a07 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -2278,16 +2278,6 @@ "campsite": { "description": "Càmpings", "filter": { - "0": { - "options": { - "0": { - "question": "Taxa" - }, - "1": { - "question": "Gratuït" - } - } - }, "1": { "options": { "0": { @@ -2478,17 +2468,6 @@ }, "question": "Aquest lloc té una estació d'abocament sanitari?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Aquest lloc té lavabos" - }, - "1": { - "then": "Aquest lloc no té lavabos" - } - }, - "question": "Aquest lloc té lavabos?" - }, "caravansites-website": { "question": "Aquest lloc té un lloc web?", "render": "Lloc web oficial: {website}" @@ -5520,6 +5499,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "Amb refugi" + } + } + }, "3": { "options": { "0": { @@ -8006,6 +7992,9 @@ "mappings": { "0": { "then": "Aquest espai d'aparcament té 1 plaça." + }, + "1": { + "then": "Aquest espai d'aparcament té 1 plaça." } }, "render": "Aquests espais d'aparcament tenen {capacity} places." @@ -11566,7 +11555,7 @@ "2": { "then": "Una càmera panoràmica" }, - "3": { + "4": { "then": "Un timbre que es pot activar remotament en qualsevol moment o mitjançant la detecció de moviment. Aquests són típicament Smart, banderes connectades a Internet. Les marques típiques són Ring, Google Nest, Eufy, ..." } }, @@ -12449,13 +12438,6 @@ "transit_stops": { "description": "Capa que mostra diferents tipus de parades de transport públic.", "filter": { - "0": { - "options": { - "0": { - "question": "Amb refugi" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/cs.json b/langs/layers/cs.json index e22d6d14a..348bf81cb 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -2454,16 +2454,6 @@ "campsite": { "description": "Kempy", "filter": { - "0": { - "options": { - "0": { - "question": "Poplatek" - }, - "1": { - "question": "zdarma" - } - } - }, "1": { "options": { "0": { @@ -2673,17 +2663,6 @@ }, "question": "Má toto místo sanitární skládku?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Toto místo má toalety" - }, - "1": { - "then": "Toto místo nemá toalety" - } - }, - "question": "Má toto místo toalety?" - }, "caravansites-website": { "question": "Má toto místo webové stránky?", "render": "Oficiální webové stránky: {website}" @@ -5841,6 +5820,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "S přístřeškem" + } + } + }, "3": { "options": { "0": { @@ -8633,6 +8619,9 @@ "mappings": { "0": { "then": "Toto parkoviště má 1 místo." + }, + "1": { + "then": "Toto parkoviště má 1 místo." } }, "render": "Toto parkoviště má {capacity} míst." @@ -12510,7 +12499,7 @@ "2": { "then": "Otáčecí kamera" }, - "3": { + "4": { "then": "Domovní zvonek, který lze spouštět kdykoli vzdáleně nebo detekcí pohybu. Jsou to typicky chytré zvonky připojené k Internetu. Typické značky jsou Ring, Google Nest, Eufy…" } }, @@ -13556,13 +13545,6 @@ "transit_stops": { "description": "Vrstva zobrazující různé typy zastávek veřejné dopravy.", "filter": { - "0": { - "options": { - "0": { - "question": "S přístřeškem" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/cy.json b/langs/layers/cy.json index 2f2908031..69d8a735f 100644 --- a/langs/layers/cy.json +++ b/langs/layers/cy.json @@ -225,13 +225,6 @@ }, "campsite": { "filter": { - "0": { - "options": { - "0": { - "question": "Ffi" - } - } - }, "1": { "options": { "7": { diff --git a/langs/layers/da.json b/langs/layers/da.json index d1a4d9c39..19ad241fb 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -1561,17 +1561,6 @@ }, "question": "Har dette sted en sanitær tømningsstation?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Dette sted har toiletter" - }, - "1": { - "then": "Dette sted har ikke toiletter" - } - }, - "question": "Har dette sted toiletter?" - }, "caravansites-website": { "question": "Har dette sted et websted?", "render": "Officiel hjemmeside: {website}" @@ -3152,7 +3141,7 @@ "2": { "then": "Et kamera, der panorerer" }, - "3": { + "4": { "then": "En dørklokke, som kan tændes på afstand når som helst eller ved bevægelsesregistrering. Disse er typisk intelligente internetforbundne dørklokker. Typiske mærker er Ring, Google Nest, Eufy, …" } } diff --git a/langs/layers/de.json b/langs/layers/de.json index a6c6281f5..6ecd78b43 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -2241,16 +2241,6 @@ "campsite": { "description": "Zeltplätze", "filter": { - "0": { - "options": { - "0": { - "question": "Gebühr" - }, - "1": { - "question": "kostenlos" - } - } - }, "1": { "options": { "0": { @@ -2460,17 +2450,6 @@ }, "question": "Hat dieser Ort eine sanitäre Entsorgungsstation?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Dieser Ort verfügt über Toiletten" - }, - "1": { - "then": "Dieser Ort verfügt nicht über Toiletten" - } - }, - "question": "Verfügt dieser Ort über Toiletten?" - }, "caravansites-website": { "question": "Hat dieser Ort eine Webseite?", "render": "Offizielle Webseite: {website}" @@ -5509,6 +5488,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "Mit Unterstand" + } + } + }, "3": { "options": { "0": { @@ -7981,6 +7967,9 @@ "mappings": { "0": { "then": "Dieser Parkplatz hat 1 Stellplatz." + }, + "1": { + "then": "Dieser Parkplatz hat 1 Stellplatz." } }, "render": "Dieser Parkplatz hat {capacity} Stellplätze." @@ -11581,6 +11570,9 @@ "then": "Eine bewegliche Kamera" }, "3": { + "then": "Eine 360°-Kamera" + }, + "4": { "then": "Eine Türklingel, die jederzeit oder per Bewegungserkennung ferngeschaltet werden kann. Dies sind typischerweise Smart, internetgebundene Türklingeln. Typische Marken sind Ring, Google Nest, Eufy, ..." } }, @@ -12463,13 +12455,6 @@ "transit_stops": { "description": "Ebene mit verschiedenen Arten von Haltestellen.", "filter": { - "0": { - "options": { - "0": { - "question": "Mit Unterstand" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 6365bf5d6..3c1bb83b9 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -531,6 +531,40 @@ "render": "Animal shelter" } }, + "arcade": { + "description": "Layer showing arcades", + "name": "Arcades", + "presets": { + "0": { + "title": "an arcade" + } + }, + "tagRenderings": { + "name": { + "override": { + "question": "What is the name of this arcade?", + "render": "This arcade is called {name}" + } + }, + "virtual_reality": { + "mappings": { + "0": { + "then": "This arcade offers virtual-reality gaming." + }, + "1": { + "then": "This arcade only offers virtual-reality gaming." + }, + "2": { + "then": "This arcade doesn't offer virtual-reality gaming" + } + }, + "question": "Does this arcade offer virtual-reality gaming?" + } + }, + "title": { + "render": "Arcade" + } + }, "artwork": { "description": "An open map of statues, busts, graffitis and other artwork all over the world", "name": "Artworks", @@ -2454,16 +2488,6 @@ "campsite": { "description": "Campsites", "filter": { - "0": { - "options": { - "0": { - "question": "Fee" - }, - "1": { - "question": "free of charge" - } - } - }, "1": { "options": { "0": { @@ -2673,17 +2697,6 @@ }, "question": "Does this place have a sanitary dump station?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "This place has toilets" - }, - "1": { - "then": "This place does not have toilets" - } - }, - "question": "Does this place have toilets?" - }, "caravansites-website": { "question": "Does this place have a website?", "render": "Official website: {website}" @@ -5841,6 +5854,20 @@ } } }, + "22": { + "options": { + "0": { + "question": "Has self-checkout" + } + } + }, + "23": { + "options": { + "0": { + "question": "With a shelter" + } + } + }, "3": { "options": { "0": { @@ -7019,6 +7046,37 @@ "render": "Hospital" } }, + "hut": { + "description": "Layer showing basic huts, wilderness huts and alpine huts", + "name": "Huts", + "presets": { + "0": { + "description": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas and a fireplace or stove for heating and cooking.", + "title": "wilderness hut" + }, + "1": { + "description": "A serviced remote building located in the mountains intended to provide board and lodging.", + "title": "alpine hut" + }, + "2": { + "description": "An unserviced fully enclosed hut (with roof and walls) with beds or suitable sleeping areas without a fireplace or stove.", + "title": "basic hut" + } + }, + "tagRenderings": { + "drinking_water": { + "mappings": { + "0": { + "then": "Here is drinking water available." + }, + "1": { + "then": "Here is no drinking water available." + } + }, + "question": "Is drinking water available here?" + } + } + }, "hydrant": { "description": "Map layer to show fire hydrants.", "name": "Hydrants", @@ -8629,10 +8687,34 @@ "description": "Layer showing individual parking spaces.", "name": "Parking Spaces", "tagRenderings": { + "access": { + "mappings": { + "0": { + "then": "Anyone can use this parking space." + }, + "1": { + "then": "Anyone can use this parking space." + }, + "2": { + "then": "This parking space is reserved for customers." + }, + "3": { + "then": "This parking space is private and cannot be used by the general public." + }, + "4": { + "then": "This parking space is reserved for permit holders." + } + }, + "question": "Who can use this parking space?", + "render": "Access of parking space: {access}" + }, "capacity": { "mappings": { "0": { "then": "This parking space has 1 space." + }, + "1": { + "then": "This parking space has 1 space." } }, "render": "This parking spaces has {capacity} spaces." @@ -8657,6 +8739,9 @@ "13": { "then": "This is a parking space reserved for car sharing." }, + "14": { + "then": "This is a parking space reserved for women." + }, "2": { "then": "This is a disabled parking space." }, @@ -8788,6 +8873,130 @@ "render": "Physiotherapist {name}" } }, + "picnic_site": { + "description": "Picnic sites for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters", + "filter": { + "1": { + "options": { + "0": { + "question": "With a firepit" + } + } + }, + "2": { + "options": { + "0": { + "question": "With a BBQ" + } + } + }, + "3": { + "options": { + "0": { + "question": "With drinking water" + } + } + } + }, + "name": "Picnic sites", + "presets": { + "0": { + "description": "A picnic site for eating outdoors, featuring amenities like toilets, water taps, BBQ, benches and shelters", + "title": "a picnic site" + } + }, + "tagRenderings": { + "bbq": { + "mappings": { + "0": { + "then": "This picnic site has a BBQ." + }, + "1": { + "then": "This picnic site does not have a BBQ." + }, + "2": { + "then": "This picnic site has a BBQ, but it is mapped as a different icon." + } + }, + "question": "Does this picnic site have a BBQ?" + }, + "covered": { + "mappings": { + "0": { + "then": "This picnic site is covered." + }, + "1": { + "then": "This picnic site is not covered." + } + }, + "question": "Is this picnic site covered?" + }, + "drinking_water": { + "mappings": { + "0": { + "then": "This picnic site has drinking water." + }, + "1": { + "then": "This picnic site does not have drinking water." + }, + "2": { + "then": "This picnic site has drinking water, but it is mapped as a different icon." + } + }, + "question": "Does this picnic site have drinking water?" + }, + "fireplace": { + "mappings": { + "0": { + "then": "This picnic site has a firepit." + }, + "1": { + "then": "This picnic site does not have a firepit." + }, + "2": { + "then": "This picnic site has a firepit, but it is mapped as a different icon." + } + }, + "question": "Does this picnic site have a firepit?" + }, + "name": { + "override": { + "render": "This picnic site is called {name}" + } + }, + "openfire": { + "mappings": { + "0": { + "then": "Open fire is allowed at this picnic site." + }, + "1": { + "then": "Open fire is not allowed at this picnic site." + }, + "2": { + "then": "Open fire is allowed at this picnic site with a permit." + } + }, + "question": "Is open fire allowed at this picnic site?" + }, + "shelter": { + "mappings": { + "0": { + "then": "This picnic site has a shelter." + }, + "1": { + "then": "This picnic site does not have a shelter." + }, + "2": { + "then": "This picnic site has a shelter, but is is mapped as a different icon." + } + }, + "question": "Does this picnic site have a shelter?" + } + }, + "title": { + "render": "Picnic site" + } + }, "picnic_table": { "description": "The layer showing picnic tables", "name": "Picnic tables", @@ -9978,6 +10187,32 @@ }, "question": "What kind of seating does {title()} have?" }, + "self_checkout": { + "mappings": { + "0": { + "then": "This place offers self-checkout" + }, + "1": { + "then": "This place does not offer self-checkout" + }, + "2": { + "then": "This place only offers self-checkout" + } + }, + "question": "Does this place offer self-checkout?", + "questionHint": "e.g. handheld scanners or a self-checkout kiosk" + }, + "self_checkout_type": { + "mappings": { + "0": { + "then": "This place offers self-checkout using a handheld scanner" + }, + "1": { + "then": "This place offers self-checkout using a self-checkout kiosk" + } + }, + "question": "What kind of self-checkout does this place offer?" + }, "service:electricity": { "mappings": { "0": { @@ -11230,6 +11465,15 @@ "second_hand": { "question": "Does this shop sell second-hand items?" }, + "self_checkout": { + "override": { + "+mappings": { + "0": { + "then": "This shop (probably) does not offer self-checkout" + } + } + } + }, "sells_new_bikes": { "mappings": { "0": { @@ -12511,6 +12755,9 @@ "then": "A panning camera" }, "3": { + "then": "A 360° camera" + }, + "4": { "then": "A doorbell which might be turned on remotely at any time or by motion detection. These are typically Smart, internet-connected doorbells. Typical brands are Ring, Google Nest, Eufy, …" } }, @@ -13556,13 +13803,6 @@ "transit_stops": { "description": "Layer showing different types of transit stops.", "filter": { - "0": { - "options": { - "0": { - "question": "With a shelter" - } - } - }, "1": { "options": { "0": { @@ -14108,6 +14348,9 @@ "debug_accordeon_title": { "render": "Debug information" }, + "debug_serviceworker_accordeon_title": { + "render": "Debug information about the service worker" + }, "debug_storage_accordeon_title": { "render": "Debug information about local storage" }, @@ -14118,6 +14361,9 @@ } } }, + "expl": { + "render": "To clear the service worker data, use the 'clear caches' button" + }, "fixate-north": { "mappings": { "0": { diff --git a/langs/layers/es.json b/langs/layers/es.json index 0baced4d3..076643f52 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -2270,17 +2270,6 @@ }, "question": "¿Este lugar tiene un punto de vaciado de aguas grises?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Este lugar tiene baños" - }, - "1": { - "then": "Este lugar no tiene baños" - } - }, - "question": "¿Este lugar tiene baños?" - }, "caravansites-website": { "question": "¿Este lugar tiene una página web?", "render": "Página web oficial: {website}" @@ -5162,6 +5151,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "Con refugio" + } + } + }, "3": { "options": { "0": { @@ -7594,6 +7590,9 @@ "mappings": { "0": { "then": "Esta plaza de aparcamiento tiene 1 plaza." + }, + "1": { + "then": "Esta plaza de aparcamiento tiene 1 plaza." } }, "render": "Esta plaza de aparcamiento tiene {capacity} plazas." @@ -10582,6 +10581,9 @@ }, "2": { "then": "Una cámara panorámica" + }, + "3": { + "then": "Una cámara de 360°" } }, "question": "¿Qué tipo de cámara es esta?" @@ -11317,13 +11319,6 @@ "transit_stops": { "description": "Capa que muestra diferentes tipos de paradas de transporte.", "filter": { - "0": { - "options": { - "0": { - "question": "Con refugio" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/eu.json b/langs/layers/eu.json index 30ad60b23..8473c20b9 100644 --- a/langs/layers/eu.json +++ b/langs/layers/eu.json @@ -289,13 +289,6 @@ } } }, - "caravansites-toilets": { - "mappings": { - "1": { - "then": "Toki honek ez dauka komunik" - } - } - }, "caravansites-website": { "question": "Toki honek webgunerik ba al du?" } diff --git a/langs/layers/fr.json b/langs/layers/fr.json index dad2406a9..cceb1374f 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -1873,17 +1873,6 @@ }, "question": "Ce site possède-t’il un lieu de vidange ?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Ce site a des toilettes" - }, - "1": { - "then": "Ce site n’a pas de toilettes" - } - }, - "question": "Y-a-t’il des toilettes sur le site ?" - }, "caravansites-website": { "question": "Ce lieu a-t’il un site internet ?", "render": "Site officiel : {website}" @@ -3609,6 +3598,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "Avec un abri" + } + } + }, "6": { "options": { "0": { @@ -6519,6 +6515,9 @@ }, "2": { "then": "Une caméra panoramique" + }, + "3": { + "then": "Une caméra 360°" } }, "question": "Quel genre de caméra est-ce ?" @@ -6543,7 +6542,7 @@ "then": "Une zone intérieure privée est surveillée, par exemple un magasin, un parking souterrain privé…" } }, - "question": "De quel genre de surveillance cette caméra est-elle ?" + "question": "De quel genre de surveillance cette caméra est-elle ?" }, "Surveillance:zone": { "mappings": { @@ -6916,13 +6915,6 @@ }, "transit_stops": { "filter": { - "0": { - "options": { - "0": { - "question": "Avec un abri" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/hu.json b/langs/layers/hu.json index d1e1287f3..c95ddb68a 100644 --- a/langs/layers/hu.json +++ b/langs/layers/hu.json @@ -466,19 +466,6 @@ "description": "Új hivatalos lakóautóhely hozzáadása. Ez arra vannak kijelölve, hogy lakóautóval ott éjszakázzunk. Lehet, hogy úgy néz ki, mint egy igazi kemping, de az is lehet, hogy csak olyan, mint egy parkoló. Előfordulhat, hogy egyáltalán nem jelzik őket, hanem csak egy önkormányzati határozatban vannak kijelölve. A lakóautósoknak szánt olyan hagyományos parkolók, ahol nem várhatóan nem fognak éjszakázni, -nem minősül- lakóautóhelynek. ", "title": "lakóautós megállóhely" } - }, - "tagRenderings": { - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Itt van WC" - }, - "1": { - "then": "Itt nincs WC" - } - }, - "question": "Van-e itt WC?" - } } }, "charging_station": { diff --git a/langs/layers/id.json b/langs/layers/id.json index cc5cd707c..beb701796 100644 --- a/langs/layers/id.json +++ b/langs/layers/id.json @@ -196,16 +196,6 @@ }, "question": "Apakah tempat ini memiliki tempat pembuangan sanitasi?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Tempat sini ada tandas" - }, - "1": { - "then": "Tempat sini tiada tandas" - } - } - }, "caravansites-website": { "question": "Tempat sini terada situs web?", "render": "Situs resmi: {website}" diff --git a/langs/layers/it.json b/langs/layers/it.json index 5a210ede3..990c15157 100644 --- a/langs/layers/it.json +++ b/langs/layers/it.json @@ -2423,16 +2423,6 @@ "campsite": { "description": "Campeggi", "filter": { - "0": { - "options": { - "0": { - "question": "A pagamento" - }, - "1": { - "question": "gratuito" - } - } - }, "1": { "options": { "0": { @@ -2642,17 +2632,6 @@ }, "question": "Questo posto ha una stazione di scarico sanitario?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Questo posto ha servizi igienici" - }, - "1": { - "then": "Questo posto non ha servizi igienici" - } - }, - "question": "Questo posto ha servizi igienici?" - }, "caravansites-website": { "question": "Questo posto ha un sito web?", "render": "Sito web ufficiale: {website}" @@ -5768,6 +5747,13 @@ } } }, + "23": { + "options": { + "0": { + "question": "Con una pensilina" + } + } + }, "3": { "options": { "0": { @@ -8469,6 +8455,9 @@ "mappings": { "0": { "then": "Questo posto auto ha 1 spazio." + }, + "1": { + "then": "Questo posto auto ha 1 spazio." } }, "render": "Questo posto auto ha {capacity} spazi." @@ -12119,7 +12108,7 @@ "2": { "then": "Una telecamera panoramica" }, - "3": { + "4": { "then": "Un campanello che potrebbe essere acceso da remoto in qualsiasi momento o tramite rilevamento del movimento. Questi sono tipicamente campanelli Smart, connessi a Internet. Marchi tipici sono Ring, Google Nest, Eufy, ..." } }, @@ -13134,13 +13123,6 @@ "transit_stops": { "description": "Livello che mostra diversi tipi di fermate dei mezzi pubblici.", "filter": { - "0": { - "options": { - "0": { - "question": "Con una pensilina" - } - } - }, "1": { "options": { "0": { diff --git a/langs/layers/ja.json b/langs/layers/ja.json index b9808a0b2..7ee7a672c 100644 --- a/langs/layers/ja.json +++ b/langs/layers/ja.json @@ -217,17 +217,6 @@ }, "question": "この場所に衛生的なゴミ捨て場はありますか?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "ここにはトイレがある" - }, - "1": { - "then": "ここにはトイレがない" - } - }, - "question": "ここにトイレはありますか?" - }, "caravansites-website": { "question": "ここにはウェブサイトがありますか?", "render": "公式Webサイト: {website}" diff --git a/langs/layers/nb_NO.json b/langs/layers/nb_NO.json index 2879f7599..e85806b21 100644 --- a/langs/layers/nb_NO.json +++ b/langs/layers/nb_NO.json @@ -443,17 +443,6 @@ "question": "Hva heter dette stedet?", "render": "Dette stedet heter {name}" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Dette stedet har toalettfasiliteter" - }, - "1": { - "then": "Dette stedet har ikke toalettfasiliteter" - } - }, - "question": "Har dette stedet toaletter?" - }, "caravansites-website": { "question": "Har dette stedet en nettside?", "render": "Offisiell nettside: {website}" diff --git a/langs/layers/nl.json b/langs/layers/nl.json index b0a54c59f..f7f4a808a 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -2543,17 +2543,6 @@ }, "question": "Heeft deze plaats een loosplaats?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Deze plaats heeft toiletten" - }, - "1": { - "then": "Deze plaats heeft geen toiletten" - } - }, - "question": "Heeft deze plaats toiletten?" - }, "caravansites-website": { "question": "Heeft deze plaats een website?", "render": "Officiële website: : {website}" @@ -5516,6 +5505,13 @@ } } }, + "22": { + "options": { + "0": { + "question": "Heeft zelfscan" + } + } + }, "5": { "options": { "0": { @@ -7607,10 +7603,34 @@ "description": "Laag met individuele parkeerplekken.", "name": "Parkeerplekken", "tagRenderings": { + "access": { + "mappings": { + "0": { + "then": "Iedereen kan deze parkeerplek gebruiken." + }, + "1": { + "then": "Iedereen kan deze parkeerplek gebruiken." + }, + "2": { + "then": "Deze parkeerplek is gereserveerd voor klanten." + }, + "3": { + "then": "Deze parkeerplek is privé en mag niet door het grote publiek worden gebruikt." + }, + "4": { + "then": "Deze parkeerplek is gereserveerd voor vergunninghouders." + } + }, + "question": "Wie mag deze parkeerplek gebruiken?", + "render": "Toegang tot parkeerplek: {access}" + }, "capacity": { "mappings": { "0": { "then": "Deze parkeerplek heeft 1 plaats." + }, + "1": { + "then": "Deze parkeerplek heeft 1 plaats." } }, "render": "Deze parkeerplek heeft {capacity} plaatsen." @@ -7635,6 +7655,9 @@ "13": { "then": "Deze parkeerplek is gereserveerd voor autodelen." }, + "14": { + "then": "Deze parkeerplek is gereserveerd voor vrouwen." + }, "2": { "then": "Dit is een gehandicaptenparkeerplaats." }, @@ -7765,6 +7788,130 @@ "render": "Kinesist {name}" } }, + "picnic_site": { + "description": "Picknickplaatsen voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen", + "filter": { + "1": { + "options": { + "0": { + "question": "Met een vuurplaats" + } + } + }, + "2": { + "options": { + "0": { + "question": "Met een BBQ" + } + } + }, + "3": { + "options": { + "0": { + "question": "Met drinkwater" + } + } + } + }, + "name": "Picknickplaatsen", + "presets": { + "0": { + "description": "Een picknickplaats voor het eten in de buitenlucht, met voorzieningen zoals toiletten, waterkranen, BBQ, banken en schuilplaatsen", + "title": "een picknickplaats" + } + }, + "tagRenderings": { + "bbq": { + "mappings": { + "0": { + "then": "Deze picknickplaats heeft een BBQ." + }, + "1": { + "then": "Deze picknickplaats heeft geen BBQ." + }, + "2": { + "then": "Deze picknickplaats heeft een BBQ, maar deze staat los op de kaart." + } + }, + "question": "Heeft deze picknickplaats een BBQ?" + }, + "covered": { + "mappings": { + "0": { + "then": "Deze picknickplaats is overdekt." + }, + "1": { + "then": "Deze picknickplaats is niet overdekt." + } + }, + "question": "Is deze picknickplaats overdekt?" + }, + "drinking_water": { + "mappings": { + "0": { + "then": "Deze picknickplaats heeft drinkwater." + }, + "1": { + "then": "Deze picknickplaats heeft geen drinkwater." + }, + "2": { + "then": "Deze picknickplaats heeft drinkwater, maar deze staat los op de kaart." + } + }, + "question": "Heeft deze picknickplaats drinkwater?" + }, + "fireplace": { + "mappings": { + "0": { + "then": "Deze picknickplaats heeft een vuurplaats." + }, + "1": { + "then": "Deze picknickplaats heeft geen vuurplaats." + }, + "2": { + "then": "Deze picknickplaats heeft een vuurplaats, maar deze staat los op de kaart." + } + }, + "question": "Heeft deze picknickplaats een vuurplaats?" + }, + "name": { + "override": { + "render": "Deze picknickplaats heet {name}" + } + }, + "openfire": { + "mappings": { + "0": { + "then": "Open vuur is toegestaan op deze picknickplaats." + }, + "1": { + "then": "Open vuur is niet toegestaan op deze picknickplaats." + }, + "2": { + "then": "Open vuur is toegestaan op deze picknickplaats met een vergunning." + } + }, + "question": "Is open vuur toegestaan op deze picknickplaats?" + }, + "shelter": { + "mappings": { + "0": { + "then": "Deze picknickplaats heeft een schuilplaats." + }, + "1": { + "then": "Deze picknickplaats heeft geen schuilplaats." + }, + "2": { + "then": "Deze picknickplaats heeft een schuilplaats, maar deze staat los op de kaart." + } + }, + "question": "Heeft deze picknickplaats een schuilplaats?" + } + }, + "title": { + "render": "Picknickplaats" + } + }, "picnic_table": { "description": "Deze laag toont picknicktafels", "name": "Picknicktafels", @@ -8632,6 +8779,32 @@ }, "question": "Wat voor zitplaatsen heeft {title()}?" }, + "self_checkout": { + "mappings": { + "0": { + "then": "Deze plaats biedt zelfscannen aan" + }, + "1": { + "then": "Deze plaats biedt geen zelfscannen aan" + }, + "2": { + "then": "Deze plaats biedt enkel zelfscannen aan" + } + }, + "question": "Biedt deze plaats zelfscannen aan?", + "questionHint": "bijv. handscanners of een zelfscankassa" + }, + "self_checkout_type": { + "mappings": { + "0": { + "then": "Deze plaats biedt zelfscannen met een handscanner aan" + }, + "1": { + "then": "Deze plaats biedt zelfscannen met een zelfscankassa aan" + } + }, + "question": "Wat voor soort zelfscannen biedt deze plaats aan?" + }, "service:electricity": { "mappings": { "0": { diff --git a/langs/layers/pl.json b/langs/layers/pl.json index decd96ce4..187e36d08 100644 --- a/langs/layers/pl.json +++ b/langs/layers/pl.json @@ -1143,17 +1143,6 @@ }, "question": "Czy w tym miejscu znajduje się stacja zrzutu ścieków sanitarnych?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "To miejsce ma toalety" - }, - "1": { - "then": "To miejsce nie ma toalet" - } - }, - "question": "Czy to miejsce ma toalety?" - }, "caravansites-website": { "question": "Czy to miejsce ma stronę internetową?", "render": "Official website: {website}" diff --git a/langs/layers/pt.json b/langs/layers/pt.json index 6e2b577c2..b1599bf54 100644 --- a/langs/layers/pt.json +++ b/langs/layers/pt.json @@ -1418,17 +1418,6 @@ }, "question": "Este local tem uma estação de aterro sanitário?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Este lugar tem casa de banho" - }, - "1": { - "then": "Este lugar não tem casas de banho" - } - }, - "question": "Este lugar tem casas de banho?" - }, "caravansites-website": { "question": "Este lugar tem um website?", "render": "Site oficial: {website}" diff --git a/langs/layers/pt_BR.json b/langs/layers/pt_BR.json index 2834d9a0c..6da173f4d 100644 --- a/langs/layers/pt_BR.json +++ b/langs/layers/pt_BR.json @@ -1428,17 +1428,6 @@ }, "question": "Este local tem uma estação de aterro sanitário?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "Este lugar tem banheiros" - }, - "1": { - "then": "Este lugar não tem banheiros" - } - }, - "question": "Este lugar tem banheiros?" - }, "caravansites-website": { "question": "Este lugar tem um website?", "render": "Site oficial: {website}" diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 4cda3d5e1..915e17453 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -710,17 +710,6 @@ }, "question": "В этом кемпинге есть место для слива отходов из туалетных резервуаров?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "В этом месте есть туалеты" - }, - "1": { - "then": "В этом месте нет туалетов" - } - }, - "question": "Здесь есть туалеты?" - }, "caravansites-website": { "question": "Есть ли у этого места веб-сайт?", "render": "Официальный сайт: {website}" diff --git a/langs/layers/zh_Hant.json b/langs/layers/zh_Hant.json index 44fd18acb..adf784206 100644 --- a/langs/layers/zh_Hant.json +++ b/langs/layers/zh_Hant.json @@ -594,17 +594,6 @@ }, "question": "這個地方有衛生設施嗎?" }, - "caravansites-toilets": { - "mappings": { - "0": { - "then": "這個地方有廁所" - }, - "1": { - "then": "這個地方並沒有廁所" - } - }, - "question": "這個地方有廁所嗎?" - }, "caravansites-website": { "question": "這個地方有網站嗎?", "render": "官方網站:{website}" diff --git a/langs/themes/en.json b/langs/themes/en.json index de5ff9837..0c7b8469d 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -8,6 +8,10 @@ "description": "On this map, one can find and mark nearby defibrillators", "title": "Defibrillators" }, + "arcade": { + "description": "A map of arcades", + "title": "Arcades" + }, "architecture": { "description": "A map showing the architectural style of buildings", "title": "Buildings with an architectural style" diff --git a/package-lock.json b/package-lock.json index 33eba2697..c2bec2fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.55.5", + "version": "0.55.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.55.5", + "version": "0.55.7", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 150b17ef9..03d99f05e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.55.5", + "version": "0.55.7", "repository": "https://source.mapcomplete.org/MapComplete/MapComplete", "description": "A small website to edit OSM easily", "bugs": "hhttps://source.mapcomplete.org/MapComplete/MapComplete/issues", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index d0eba851f..0e54a4fa9 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -5198,23 +5198,22 @@ input[type="range"].range-lg::-moz-range-thumb { --low-interaction-background: #eeeeee; --low-interaction-background-50: #eeeeee90; --low-interaction-foreground: black; - --low-interaction-contrast: #ff00ff; --low-interaction-border: #dcdcdc; --interactive-background: #dddddd; --interactive-foreground: black; - --interactive-contrast: #ff00ff; + --interactive-contrast: #C107C5; --interaction-border: #bfbfbf; - --button-background: #282828; + --button-background-primary: #191919; --button-background-hover: #484848; - --button-primary-background-hover: #353535; + --button-primary-background-hover: rgba(48, 47, 47, 0.94); --button-foreground: white; - --button-border-color: #F7F7F7; + --button-background: #fafafa; + --button-border: #B8B8B8; --disabled: #B8B8B8; --disabled-font: #B8B8B8; - --catch-detail-color: black; - /*#3a3aeb;*/ - --catch-detail-foregroundcolor: white; - --catch-detail-color-contrast: #fb3afb; + --catch-detail-color: var(--background-color); + --catch-detail-foregroundcolor: var(--foreground-color); + --catch-detail-color-contrast: var(--interactive-contrast); --image-carousel-height: 350px; /** Technical value, used by icon.svelte */ @@ -5323,7 +5322,7 @@ input[type="text"] { } .border-interactive { - border: 2px dashed var(--catch-detail-color-contrast); + border: 2px dashed var(--interactive-contrast); border-radius: 0.5rem; } @@ -5358,11 +5357,14 @@ button, .button { align-items: center; padding: 0.25rem 1rem; margin: 0.25rem; - border: 1px solid var(--button-background-hover); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - border-radius: 15px; + background: var(--button-background); + border: 2px solid var(--button-border); + border-radius: 7px; + transition: background-color 200ms; +} + +.low-interaction button{ background: var(--background-color); - transition: all 200ms; } .group > button { @@ -5374,12 +5376,19 @@ button.w-full { margin-left: 0; } -button:hover:not(.disabled):not(.as-link), .button:hover:not(.disabled):not(.as-link) { +button.primary:hover:not(.disabled), .button.primary:hover:not(.disabled) { + background-color: var(--button-primary-background-hover); + border: 2px solid var(--interactive-contrast) +} + +button:hover:not(.disabled):not(.as-link):not(.primary), .button:hover:not(.disabled):not(.as-link):not(.primary) { background-color: var(--low-interaction-background); } -button:focus, .button:focus { - border-color: var(--interactive-contrast); +:focus-visible { + outline: auto; + outline-color: var(--interactive-contrast); + outline-style: solid; } .focus { @@ -5388,12 +5397,8 @@ button:focus, .button:focus { button.primary, .button.primary { color: var(--button-foreground); - background-color: var(--button-background); - border-color: var(--button-border-color); -} - -button.primary:hover:not(.disabled), .button.primary:hover:not(.disabled) { - background-color: var(--button-primary-background-hover); + background-color: var(--button-background-primary); + border: 2px solid var(--button-background-primary) } button.disabled { @@ -5441,10 +5446,51 @@ button.unstyled, .button-unstyled button { padding: 0; } +/****** Tablist elements *****/ + +.tablist { + margin: 0.25rem; + padding: 0.5rem; + border: 2px dashed var(--button-background-hover); + border-radius: 0.5rem; + display: flex; + justify-content: center; + flex-wrap: wrap; +} + +.tab { + border: unset; + border-radius: 0; + transition: all; + color: var(--foreground-color); + border-bottom: 2px solid var(--foreground-color); + font-weight: bold; + margin: 0.25rem; + padding: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.tab-selected { + opacity: 100%; + background: var(--interactive-background); +} + +/* Actually used, don't remove*/ + +.tab-unselected { + background: #00000000 !important; + opacity: 60%; +} + +.tab-unselected:hover { + background: var(--interactive-background); +} + /******* Other input elements ******/ .hover-alert:hover { - color: var(--catch-detail-color-contrast) + color: var(--interactive-contrast) } .links-w-full a:not(.weblate-link), .links-w-full button.as-link { @@ -5468,7 +5514,7 @@ select { } select:hover { - border-color: var(--catch-detail-color-contrast); + border-color: var(--interactive-contrast); } .neutral-label { @@ -5570,17 +5616,6 @@ h2.group { background-color: var(--interactive-background); } -.information { - /* The class to convey important information which does _not_ denote an error... */ - background-color: var(--low-interaction-background); - color: var(--alert-foreground-color); - border-radius: 1em; - margin: 0.25em; - text-align: center; - padding: 0.15em 0.3em; - border: 3px dotted var(--catch-detail-color-contrast); -} - .low-interaction .interactive { background-color: var(--interactive-background); } diff --git a/scripts/fetchLanguages.ts b/scripts/fetchLanguages.ts index 8f58b3805..2b001df70 100644 --- a/scripts/fetchLanguages.ts +++ b/scripts/fetchLanguages.ts @@ -18,7 +18,7 @@ interface value { } interface LanguageSpecResult { - directionalityLabel: value + directionalityLabel?: value lang: value code: value label: value @@ -77,6 +77,29 @@ async function fetchRegularLanguages() { return result.results.bindings } +async function fetchSignLanguages() { + const query = ` + + SELECT ?lang ?label ?code +WHERE +{ + ?lang wdt:P31 wd:Q34228. + OPTIONAL { + ?lang wdt:P1813 ?code. + } + ?lang rdfs:label ?label. + SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } +}` + const url = Wikidata.wds.sparqlQuery(query) + + // request the generated URL with your favorite HTTP request library + const result = await Utils.downloadJson<{ results: { bindings: any[] } }>(url, { + "User-Agent": "MapComplete script", + }) + return result.results.bindings + +} + /** * Fetches the object as is. Sets a 'code' binding as predifined value * @param id @@ -169,7 +192,20 @@ async function getOfficialLanguagesPerCountryCached( return officialLanguages } +async function generateSignLanguageOverview(){ + const signLanguages = await fetchSignLanguages() + const signPerId = WikidataUtils.extractLanguageData(signLanguages, WikidataUtils.languageRemapping) + const asRecord : Record> = {} + for (const lng of signPerId.keys()) { + asRecord[lng.toLowerCase()] = Utils.MapToObj(signPerId.get(lng).translations) + } + return asRecord + +} + async function main(wipeCache = false) { + const signLanguages = await generateSignLanguageOverview() + const cacheFile = "./src/assets/generated/languages-wd.json" if (wipeCache || !existsSync(cacheFile)) { console.log("Refreshing cache") @@ -181,7 +217,8 @@ async function main(wipeCache = false) { const data = JSON.parse(readFileSync(cacheFile, { encoding: "utf8" })) const perId = WikidataUtils.extractLanguageData(data, WikidataUtils.languageRemapping) const nativeList = getNativeList(perId) - writeFileSync("./src/assets/language_native.json", JSON.stringify(nativeList, null, " ")) + writeFileSync("./src/assets/language_native.json", JSON.stringify({ ...nativeList, ...signLanguages }, null, " ")) + const languagesPerCountry = Utils.TransposeMap( await getOfficialLanguagesPerCountryCached(wipeCache) ) diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index 694c4feec..d9a2ff247 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -22,7 +22,6 @@ import ThemeViewState from "../src/Models/ThemeViewState" import Validators from "../src/UI/InputElement/Validators" import questions from "../public/assets/generated/layers/questions.json" import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" -import { Utils } from "../src/Utils" import { TagUtils } from "../src/Logic/Tags/TagUtils" import Script from "./Script" import { Changes } from "../src/Logic/Osm/Changes" @@ -562,7 +561,7 @@ export class GenerateDocs extends Script { item.url.startsWith("pmtiles://") ) ) - const serverInfos = Utils.DedupOnId(serverInfosDupl, (item) => item.url) + const serverInfos = Lists.dedupOnId(serverInfosDupl, (item) => item.url) const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category))) titles.sort() diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index 69d15f0b9..6b4972b1f 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -1,7 +1,7 @@ import * as fs from "fs" import Script from "./Script" -function genImages(dryrun = false) { +function genImages() { console.log("Generating images") const dir = fs.readdirSync("./assets/svg") for (const path of dir) { @@ -64,7 +64,7 @@ class GenerateIncludedImages extends Script { super("Converts all images from assets/svg into svelte-classes.") } - async main(args: string[]): Promise { + async main(): Promise { genImages() } } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 964963620..0e48135d5 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -8,7 +8,7 @@ import { DoesImageExist, PrevalidateTheme, ValidateLayer, - ValidateThemeEnsemble, + ValidateThemeEnsemble } from "../src/Models/ThemeConfig/Conversion/Validation" import { Translation } from "../src/UI/i18n/Translation" import { OrderLayer, PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" @@ -19,7 +19,7 @@ import { DesugaringStep, Each, Fuse, - On, + On } from "../src/Models/ThemeConfig/Conversion/Conversion" import { Utils } from "../src/Utils" import Script from "./Script" @@ -191,7 +191,7 @@ class LayerBuilder extends Conversion> { return `./assets/layers/${id}/${id}.json` } - writeLayer(layer: LayerConfigJson) { + public writeLayer(layer: LayerConfigJson) { if (layer.labels?.some((l) => this._labelBlacklist.has(l))) { console.log("Not writing layer " + layer.id + ", censored") return @@ -200,6 +200,15 @@ class LayerBuilder extends Conversion> { if (!existsSync(LayerOverviewUtils.layerPath)) { mkdirSync(LayerOverviewUtils.layerPath) } + + const usedImages = Lists.dedup(new ExtractImages(true, new Set(this._desugaringState.tagRenderings.keys())) + .convertStrict({ layers: [layer], id: "dummy", icon: undefined, title: undefined }) + .map((x) => x.path)) + usedImages.sort() + + layer["_usedImages"] = usedImages + + writeFileSync(LayerBuilder.targetPath(layer.id), JSON.stringify(layer, null, " "), { encoding: "utf8", }) diff --git a/scripts/generateLicenseInfo.ts b/scripts/generateLicenseInfo.ts index 2cb5ee215..a1352a612 100644 --- a/scripts/generateLicenseInfo.ts +++ b/scripts/generateLicenseInfo.ts @@ -327,7 +327,7 @@ export class GenerateLicenseInfo extends Script { } licenses.sort((a, b) => (a.path < b.path ? -1 : 1)) - licenses = Utils.DedupOnId(licenses, (l) => l.path) + licenses = Lists.dedupOnId(licenses, (l) => l.path) const path = dir + "/license_info.json" if (licenses.length === 0) { console.log("Removing", path, "as it is empty") diff --git a/scripts/importCustomTheme.ts b/scripts/importCustomTheme.ts index 70de201c0..2ee03fd66 100644 --- a/scripts/importCustomTheme.ts +++ b/scripts/importCustomTheme.ts @@ -14,13 +14,20 @@ import { GenerateLicenseInfo } from "./generateLicenseInfo" class ImportCustomTheme extends Script { constructor() { - super("Given the path of a custom layer, will load the layer into mapcomplete as official") + super(["Given the path of a custom layer, will load the layer into mapcomplete as official","", + "Usage:", + "vite-node scripts/importCustomTheme.ts "].join("\n")) } async main(args: string[]) { + if(args.length === 0){ + this.printHelp() + return + } const path = args[0] - const layerconfig = JSON.parse(readFileSync(path, "utf-8")) + const layerconfig = JSON.parse( + readFileSync(path, "utf-8")) const id = layerconfig.id const dirPath = "./assets/layers/" + id if (!existsSync(dirPath)) { diff --git a/scripts/osm_cleanup/FixWikimediaInImageTag.ts b/scripts/osm_cleanup/FixWikimediaInImageTag.ts new file mode 100644 index 000000000..1b93fc6c7 --- /dev/null +++ b/scripts/osm_cleanup/FixWikimediaInImageTag.ts @@ -0,0 +1,95 @@ +import Script from "../Script" +import { Overpass } from "../../src/Logic/Osm/Overpass" +import Constants from "../../src/Models/Constants" +import { BBox } from "../../src/Logic/BBox" +import { RegexTag } from "../../src/Logic/Tags/RegexTag" +import { Tag } from "../../src/Logic/Tags/Tag" + +import { Feature, FeatureCollection } from "geojson" +import { existsSync, readFileSync, writeFileSync } from "fs" +import { Changes } from "../../src/Logic/Osm/Changes" +import ChangeTagAction from "../../src/Logic/Osm/Actions/ChangeTagAction" +import { And } from "../../src/Logic/Tags/And" +import { Lists } from "../../src/Utils/Lists" + +export class FixWikimediaInImageTag extends Script { + + constructor() { + super("For the given bbox, queries all `image=http(s)://commons.wikimedia.org` tags and replaces it with `commons`-tagging") + } + + + private handleFeature(f: Feature): ChangeTagAction { + const p = f.properties + const existingCommons = p["wikimedia_commons"] + const img = p["image"] + const id = p.id + if (!img) { + return undefined + } + + const extractedCommons: string = img.match(/^https?:\/\/commons.wikimedia.org\/wiki\/(.*)$/)[1] + console.log("Feature " + p.id + ": " + img + ", extr " + extractedCommons + ", old: " + existingCommons) + + + if (existingCommons === extractedCommons) { + return new ChangeTagAction(id, + new Tag("image", ""), + p, + { + changeType: "cleanup", + theme: "/" + } + ) + } + if (existingCommons) { + return undefined + } + if (!extractedCommons.startsWith("File:")) { + return undefined + } + return new ChangeTagAction(id, + new And([new Tag("image", ""), new Tag("wikimedia_commons", decodeURIComponent(extractedCommons))]), + p, + { + changeType: "cleanup", + theme: "/" + } + ) + } + + private async fetchData(bbox: BBox): Promise { + const pth = "fix_wikimedia_" + bbox.toLngLatFlat().join("_") + ".geojson" + if (existsSync(pth)) { + return JSON.parse(readFileSync(pth, "utf-8")) + } + const overpass = new Overpass(Constants.defaultOverpassUrls[0], + new RegexTag("image", /https?:\/\/commons.wikimedia.org/) + ) + const [feats] = await overpass.queryGeoJson(bbox) + writeFileSync(pth, JSON.stringify(feats), "utf8") + return feats + } + + async main(args: string[]): Promise { + if (args.length < 1) { + this.printHelp() + //return + } + + const bbox = new BBox([3.632100582083325, + 51.11343904784337, 3.8584183481742116, + 50.99383861993195]) + const feats = await this.fetchData(bbox) + + const actions = Lists.noNull(feats.features.map(f => this.handleFeature(f))) + + const xml = await Changes.createChangesetXMLForJosm(actions) + const pth = "move_image_to_wikimedia_commons_" + bbox.toLngLatFlat().join("_") + ".osc" + writeFileSync(pth, xml, "utf-8") + console.log("Written xml to file://" + pth) + + } +} + +new FixWikimediaInImageTag().run() diff --git a/src/InstallServiceWorker.ts b/src/InstallServiceWorker.ts index 85a0d76a7..c7fdbfbd2 100644 --- a/src/InstallServiceWorker.ts +++ b/src/InstallServiceWorker.ts @@ -1,13 +1,17 @@ -export {} -window.addEventListener("load", async () => { - if (!("serviceWorker" in navigator)) { - console.log("Service workers are not supported") - return - } - try { +export class InstallServiceWorker { + + static async installServiceWorker() { + if (!("serviceWorker" in navigator)) { + throw ("Service workers are not supported") + } await navigator.serviceWorker.register("/service-worker.js", { type: "module" }) console.log("Service worker registration successful") - } catch (err) { - console.error("Service worker registration failed", err) + } -}) + + static async precache(assets: string[]) { + if (assets?.length > 0) { + await fetch("./service-worker/precache?assets=" + assets.join(";")) + } + } +} diff --git a/src/Logic/Actors/SelectedElementTagsUpdater.ts b/src/Logic/Actors/SelectedElementTagsUpdater.ts index 67a7c2b44..f34ec863e 100644 --- a/src/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/src/Logic/Actors/SelectedElementTagsUpdater.ts @@ -10,6 +10,7 @@ import { Changes } from "../Osm/Changes" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState" +import Objects from "../../Utils/Objects" export default class SelectedElementTagsUpdater { private static readonly metatags = new Set([ @@ -160,7 +161,7 @@ export default class SelectedElementTagsUpdater { const newGeometry = osmObject.asGeoJson()?.geometry const oldFeature = state.indexedFeatures.featuresById.data.get(id) const oldGeometry = oldFeature?.geometry - if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) { + if (oldGeometry !== undefined && !Objects.sameObject(newGeometry, oldGeometry)) { console.log("Detected a difference in geometry for ", id) this.invalidateCache(s) oldFeature.geometry = newGeometry diff --git a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts index c9cb4b04c..8f3d009f2 100644 --- a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts +++ b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts @@ -4,9 +4,9 @@ import TileLocalStorage from "./TileLocalStorage" import { GeoOperations } from "../../GeoOperations" import FeaturePropertiesStore from "./FeaturePropertiesStore" import { UIEventSource } from "../../UIEventSource" -import { Utils } from "../../../Utils" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" +import { Lists } from "../../../Utils/Lists" class SingleTileSaver { private readonly _storage: UIEventSource @@ -31,7 +31,7 @@ class SingleTileSaver { } public saveFeatures(features: Feature[]) { - if (Utils.sameList(features, this._storage.data)) { + if (Lists.sameList(features, this._storage.data)) { return } for (const feature of features) { diff --git a/src/Logic/FeatureSource/FeatureSource.ts b/src/Logic/FeatureSource/FeatureSource.ts index a6278e77d..db1b0be0d 100644 --- a/src/Logic/FeatureSource/FeatureSource.ts +++ b/src/Logic/FeatureSource/FeatureSource.ts @@ -36,6 +36,6 @@ export interface FeatureSourceForTile extends Featu /** * A feature source which is aware of the indexes it contains */ -export interface IndexedFeatureSource extends FeatureSource { +export interface IndexedFeatureSource extends FeatureSource { readonly featuresById: Store> } diff --git a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts index dee5f8994..90cc9ef1b 100644 --- a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts +++ b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts @@ -6,7 +6,7 @@ import { Stores, UIEventSource } from "../../UIEventSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { Feature } from "geojson" -import { Utils } from "../../../Utils" +import Objects from "../../../Utils/Objects" export default class ChangeGeometryApplicator implements FeatureSource { public readonly features: UIEventSource = new UIEventSource([]) @@ -69,7 +69,7 @@ export default class ChangeGeometryApplicator implements FeatureSource { // We only apply the last change as that one'll have the latest geometry const change = changesForFeature[changesForFeature.length - 1] copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) - if (Utils.SameObject(copy.geometry, feature.geometry)) { + if (Objects.sameObject(copy.geometry, feature.geometry)) { // No actual changes: pass along the original newFeatures.push(feature) continue diff --git a/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index b6eaa9b3f..c9b9db19d 100644 --- a/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,17 +1,16 @@ import { Store, UIEventSource } from "../../UIEventSource" import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource" import { Feature } from "geojson" -import { OsmFeature } from "../../../Models/OsmFeature" import { Lists } from "../../../Utils/Lists" /** * The featureSourceMerger receives complete geometries from various sources. * If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained */ -export default class FeatureSourceMerger - implements IndexedFeatureSource +export default class FeatureSourceMerger = FeatureSource> + implements IndexedFeatureSource { - public features: UIEventSource = new UIEventSource([]) + public features: UIEventSource = new UIEventSource([]) public readonly featuresById: Store> protected readonly _featuresById: UIEventSource> protected readonly _sources: Src[] @@ -55,7 +54,7 @@ export default class FeatureSourceMerger = new Map() + const all: Map = new Map() const unseen = new Set() // We seed the dictionary with the previously loaded features const oldValues = this.features.data ?? [] @@ -118,10 +117,11 @@ export default class FeatureSourceMerger = UpdatableFeatureSource > - extends FeatureSourceMerger - implements IndexedFeatureSource, UpdatableFeatureSource + extends FeatureSourceMerger + implements IndexedFeatureSource, UpdatableFeatureSource { constructor(...sources: Src[]) { super(...sources) diff --git a/src/Logic/FeatureSource/Sources/GeoJsonSource.ts b/src/Logic/FeatureSource/Sources/GeoJsonSource.ts index 9b217bcfc..5faf3352c 100644 --- a/src/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/src/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -3,13 +3,15 @@ import { Utils } from "../../../Utils" import { FeatureSource } from "../FeatureSource" import { BBox } from "../../BBox" import { GeoOperations } from "../../GeoOperations" -import { Feature } from "geojson" +import { Feature, Geometry } from "geojson" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { Tiles } from "../../../Models/TileRange" -export default class GeoJsonSource implements FeatureSource { - private readonly _features: UIEventSource = new UIEventSource(undefined) - public readonly features: Store = this._features +export default class GeoJsonSource>> implements FeatureSource { + private readonly _features: UIEventSource = new UIEventSource(undefined) + public readonly features: Store = this._features private readonly seenids: Set private readonly idKey?: string private readonly url: string @@ -96,7 +98,7 @@ export default class GeoJsonSource implements FeatureSource { const url = this.url try { const cacheAge = (options?.maxCacheAgeSec ?? 300) * 1000 - let json = <{ features: Feature[] }>await Utils.downloadJsonCached(url, cacheAge) + let json = <{ features: T[] }>await Utils.downloadJsonCached(url, cacheAge) if (json.features === undefined || json.features === null) { json.features = [] @@ -106,7 +108,7 @@ export default class GeoJsonSource implements FeatureSource { json = GeoOperations.GeoJsonToWGS84(json) } - const newFeatures: Feature[] = [] + const newFeatures: T[] = [] let i = 0 for (const feature of json.features) { if (feature.geometry.type === "Point") { diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts index ccaa6c78a..c487814b4 100644 --- a/src/Logic/FeatureSource/Sources/MvtSource.ts +++ b/src/Logic/FeatureSource/Sources/MvtSource.ts @@ -5,8 +5,8 @@ import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { MvtToGeojson } from "mvt-to-geojson" import { OsmTags } from "../../../Models/OsmFeature" -export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource { - public readonly features: Store[]> +export default class MvtSource> implements FeatureSourceForTile, UpdatableFeatureSource { + public readonly features: Store public readonly x: number public readonly y: number public readonly z: number diff --git a/src/Logic/FeatureSource/Sources/OsmFeatureSource.ts b/src/Logic/FeatureSource/Sources/OsmFeatureSource.ts index ea08cb463..3f84d0104 100644 --- a/src/Logic/FeatureSource/Sources/OsmFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/OsmFeatureSource.ts @@ -4,16 +4,16 @@ import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" import { TagsFilter } from "../../Tags/TagsFilter" -import { Feature } from "geojson" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" import { Lists } from "../../../Utils/Lists" +import { OsmFeature } from "../../../Models/OsmFeature" /** * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' */ -export default class OsmFeatureSource extends FeatureSourceMerger { +export default class OsmFeatureSource extends FeatureSourceMerger { private readonly _bounds: Store private readonly isActive: Store private readonly _backend: string @@ -33,7 +33,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { public readonly isRunning: UIEventSource = new UIEventSource(false) private readonly _downloadedTiles: Set = new Set() - private readonly _downloadedData: Feature[][] = [] + private readonly _downloadedData: T[][] = [] private readonly _patchRelations: boolean /** * Downloads data directly from the OSM-api within the given bounds. @@ -90,7 +90,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { } } - private registerFeatures(features: Feature[]): void { + private registerFeatures(features: T[]): void { this._downloadedData.push(features) super.addData(this._downloadedData) } @@ -160,7 +160,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { const osmJson = await Utils.downloadJsonCached(url, 2000) try { this.options?.fullNodeDatabase?.handleOsmJson(osmJson, z, x, y) - let features = []>OsmToGeoJson(osmJson, { + let features = OsmToGeoJson(osmJson, { flatProperties: true, }).features diff --git a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts index 9d0037246..7cbad0368 100644 --- a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts @@ -1,4 +1,3 @@ -import { Feature, FeatureCollection, Geometry } from "geojson" import { UpdatableFeatureSource } from "../FeatureSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" @@ -7,19 +6,20 @@ import { Overpass } from "../../Osm/Overpass" import { Utils } from "../../../Utils" import { TagsFilter } from "../../Tags/TagsFilter" import { BBox } from "../../BBox" -import { OsmTags } from "../../../Models/OsmFeature" +import { OsmFeature } from "../../../Models/OsmFeature" import { Lists } from "../../../Utils/Lists" -;("use strict") + +("use strict") /** * A wrapper around the 'Overpass'-object. * It has more logic and will automatically fetch the data for the right bbox and the active layers */ -export default class OverpassFeatureSource implements UpdatableFeatureSource { +export default class OverpassFeatureSource implements UpdatableFeatureSource { /** * The last loaded features, as geojson */ - public readonly features: UIEventSource = new UIEventSource(undefined) + public readonly features: UIEventSource = new UIEventSource(undefined) public readonly runningQuery: UIEventSource = new UIEventSource(false) public readonly timeout: UIEventSource = new UIEventSource(0) @@ -110,7 +110,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource { if (!navigator.onLine) { return } - let data: FeatureCollection = undefined + let data: { features: T[] } = undefined let lastUsed = 0 const start = new Date() const layersToDownload = this._layersToDownload.data @@ -142,7 +142,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource { return undefined } this.runningQuery.setData(true) - data = (await overpass.queryGeoJson(bounds))[0] + data = (await overpass.queryGeoJson(bounds))[0] } catch (e) { this.retries.data++ this.retries.ping() @@ -229,7 +229,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource { const requestedBounds = this.state.bounds.data if ( this._lastQueryBBox !== undefined && - Utils.sameList(this._layersToDownload.data, this._lastRequestedLayers) && + Lists.sameList(this._layersToDownload.data, this._lastRequestedLayers) && requestedBounds.isContainedIn(this._lastQueryBBox) ) { return undefined diff --git a/src/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/src/Logic/FeatureSource/Sources/StaticFeatureSource.ts index ccbedda8f..7c060196a 100644 --- a/src/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -26,9 +26,6 @@ export default class StaticFeatureSource implements } } - public static fromGeojson(geojson: T[]): StaticFeatureSource { - return new StaticFeatureSource(geojson) - } } export class WritableStaticFeatureSource diff --git a/src/Logic/FeatureSource/Sources/ThemeSource.ts b/src/Logic/FeatureSource/Sources/ThemeSource.ts index fc79e477a..a9c4a91ed 100644 --- a/src/Logic/FeatureSource/Sources/ThemeSource.ts +++ b/src/Logic/FeatureSource/Sources/ThemeSource.ts @@ -12,15 +12,16 @@ import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeature import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource" import FeatureSourceMerger from "./FeatureSourceMerger" -import { Feature } from "geojson" +import { Feature, Geometry } from "geojson" import { OsmFeature } from "../../../Models/OsmFeature" +import { IsOnline } from "../../Web/IsOnline" /** * This source will fetch the needed data from various sources for the given layout. * * Note that special layers (with `source=null` will be ignored) */ -export default class ThemeSource implements IndexedFeatureSource { +export default class ThemeSource & {id: string}>> implements IndexedFeatureSource { /** * Indicates if a data source is loading something */ @@ -28,12 +29,12 @@ export default class ThemeSource implements IndexedFeatureSource { public static readonly fromCacheZoomLevel = 15 - public features: UIEventSource = new UIEventSource([]) - public readonly featuresById: Store> - private readonly core: Store + public features: UIEventSource = new UIEventSource([]) + public readonly featuresById: Store> + private readonly core: Store> - private readonly addedSources: FeatureSource[] = [] - private readonly addedItems: OsmFeature[] = [] + private readonly addedSources: FeatureSource[] = [] + private readonly addedItems: T[] = [] constructor( layers: LayerConfig[], @@ -47,11 +48,11 @@ export default class ThemeSource implements IndexedFeatureSource { const isLoading = new UIEventSource(true) this.isLoading = isLoading - const features = (this.features = new UIEventSource([])) + const features = (this.features = new UIEventSource([])) const featuresById = (this.featuresById = new UIEventSource(new Map())) this.core = mvtAvailableLayers.mapD((mvtAvailableLayers) => { this.core?.data?.destruct() - const core = new ThemeSourceCore( + const core = new ThemeSourceCore( layers, featureSwitches, mapProperties, @@ -67,18 +68,30 @@ export default class ThemeSource implements IndexedFeatureSource { core.featuresById.addCallbackAndRun((data) => featuresById.set(data)) return core }) + + IsOnline.isOnline.addCallback(async online => { + if (online) { + // Connectivity is restored - let us try to update the data + console.log("Internet got restored - starting to download all data") + isLoading.set(true) + await this.downloadAll() + isLoading.set(false) + } else { + isLoading.set(false) + } + }) } public async downloadAll() { return this.core.data.downloadAll() } - public addSource(source: FeatureSource) { + public addSource(source: FeatureSource) { this.core.data?.addSource(source) this.addedSources.push(source) } - public addItem(obj: OsmFeature) { + public addItem(obj: T) { this.core.data?.addItem(obj) this.addedItems.push(obj) } @@ -89,7 +102,7 @@ export default class ThemeSource implements IndexedFeatureSource { * * Note that special layers (with `source=null` will be ignored) */ -class ThemeSourceCore extends FeatureSourceMerger { +class ThemeSourceCore extends FeatureSourceMerger { /** * This source is _only_ triggered when the data is downloaded for CSV export * @private @@ -113,10 +126,10 @@ class ThemeSourceCore extends FeatureSourceMerger { const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) - const fromCache = new Map() + const fromCache = new Map>() if (featureSwitches.featureSwitchCache.data) { for (const layer of osmLayers) { - const src = new LocalStorageFeatureSource( + const src = new LocalStorageFeatureSource( backend, layer, ThemeSource.fromCacheZoomLevel, @@ -129,13 +142,13 @@ class ThemeSourceCore extends FeatureSourceMerger { fromCache.set(layer.id, src) } } - const mvtSources: UpdatableFeatureSource[] = osmLayers + const mvtSources: UpdatableFeatureSource[] = osmLayers .filter((f) => mvtAvailableLayers.has(f.id)) - .map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id))) - const nonMvtSources: FeatureSource[] = [] + .map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id))) + const nonMvtSources: FeatureSource[] = [] const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id)) - const osmApiSource = ThemeSourceCore.setupOsmApiSource( + const osmApiSource = ThemeSourceCore.setupOsmApiSource( osmLayers, bounds, zoom, @@ -145,14 +158,14 @@ class ThemeSourceCore extends FeatureSourceMerger { ) nonMvtSources.push(osmApiSource) - let overpassSource: OverpassFeatureSource = undefined + let overpassSource: OverpassFeatureSource = undefined if (nonMvtLayers.length > 0) { console.log( "Layers ", nonMvtLayers.map((l) => l.id), " cannot be fetched from the cache server, defaulting to overpass/OSM-api" ) - overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches) + overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches) nonMvtSources.push(overpassSource) } @@ -164,11 +177,11 @@ class ThemeSourceCore extends FeatureSourceMerger { overpassSource?.runningQuery?.addCallbackAndRun(() => setIsLoading()) osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading()) - const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) => + const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) => ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) ) - const downloadAll = new OverpassFeatureSource( + const downloadAll = new OverpassFeatureSource( { layers: layers.filter((l) => l.isNormal()), bounds: mapProperties.bounds, @@ -196,19 +209,19 @@ class ThemeSourceCore extends FeatureSourceMerger { this._mapBounds = mapProperties.bounds } - private static setupMvtSource( + private static setupMvtSource( layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActive?: Store - ): UpdatableFeatureSource { - return new DynamicMvtileSource(layer, mapProperties, { isActive }) + ): UpdatableFeatureSource { + return new DynamicMvtileSource(layer, mapProperties, { isActive }) } - private static setupGeojsonSource( + private static setupGeojsonSource( layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActiveByFilter?: Store - ): UpdatableFeatureSource { + ): UpdatableFeatureSource { const source = layer.source const isActive = mapProperties.zoom.map( (z) => (isActiveByFilter?.data ?? true) && z >= layer.minzoom, @@ -216,20 +229,20 @@ class ThemeSourceCore extends FeatureSourceMerger { ) if (source.geojsonZoomLevel === undefined) { // This is a 'load everything at once' geojson layer - return new GeoJsonSource(layer, { isActive }) + return new GeoJsonSource(layer, { isActive }) } else { - return new DynamicGeoJsonTileSource(layer, mapProperties, { isActive }) + return new DynamicGeoJsonTileSource(layer, mapProperties, { isActive }) } } - private static setupOsmApiSource( + private static setupOsmApiSource( osmLayers: LayerConfig[], bounds: Store, zoom: Store, backend: string, featureSwitches: FeatureSwitchState, fullNodeDatabase: FullNodeDatabaseSource - ): OsmFeatureSource | undefined { + ): OsmFeatureSource | undefined { if (osmLayers.length == 0) { return undefined } @@ -248,7 +261,7 @@ class ThemeSourceCore extends FeatureSourceMerger { if (typeof allowedFeatures === "boolean") { throw "Invalid filter to init OsmFeatureSource: it optimizes away to " + allowedFeatures } - return new OsmFeatureSource({ + return new OsmFeatureSource({ allowedFeatures, bounds, backend, @@ -258,12 +271,12 @@ class ThemeSourceCore extends FeatureSourceMerger { }) } - private static setupOverpass( + private static setupOverpass( osmLayers: LayerConfig[], bounds: Store, zoom: Store, featureSwitches: FeatureSwitchState - ): OverpassFeatureSource | undefined { + ): OverpassFeatureSource | undefined { if (osmLayers.length == 0) { return undefined } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource.ts index baa496393..c836d503a 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource.ts @@ -3,6 +3,7 @@ import { Feature, Point } from "geojson" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import { GeoOperations } from "../../GeoOperations" import { Tiles } from "../../../Models/TileRange" +import { Lists } from "../../../Utils/Lists" export interface ClusteringOptions { /** @@ -86,11 +87,10 @@ export class ClusteringFeatureSource = Feature> features: Feature[], tileId: number ): Feature { - let lon: number - let lat: number - const [z, x, y] = Tiles.tile_from_index(tileId) + let coordinates: [number, number] if (this.showSummaryAt === "tilecenter") { - ;[lon, lat] = Tiles.centerPointOf(z, x, y) + const [z, x, y] = Tiles.tile_from_index(tileId) + coordinates = Tiles.centerPointOf(z, x, y) } else { let lonSum = 0 let latSum = 0 @@ -99,14 +99,15 @@ export class ClusteringFeatureSource = Feature> lonSum += lon latSum += lat } - lon = lonSum / features.length - lat = latSum / features.length + const lon = lonSum / features.length + const lat = latSum / features.length + coordinates = [lon, lat] } return { type: "Feature", geometry: { type: "Point", - coordinates: [lon, lat], + coordinates }, properties: { id: "summary_" + this.id + "_" + tileId, @@ -120,10 +121,10 @@ export class ClusteringFeatureSource = Feature> /** * Groups multiple summaries together */ -export class ClusterGrouping implements FeatureSource> { - private readonly _features: UIEventSource[]> = +export class ClusterGrouping implements FeatureSource> { + private readonly _features: UIEventSource[]> = new UIEventSource([]) - public readonly features: Store[]> = this._features + public readonly features: Store[]> = this._features public static readonly singleton = new ClusterGrouping() @@ -140,27 +141,34 @@ export class ClusterGrouping implements FeatureSource[]>[] = [] private update() { - const countPerTile = new Map() + const countPerTile = new Map() for (const source of this.allSource) { for (const f of source.data) { const id = f.properties.tile_id - const count = f.properties.total + (countPerTile.get(id) ?? 0) - countPerTile.set(id, count) + if (!countPerTile.has(id)) { + countPerTile.set(id, []) + } + const ls = countPerTile.get(id) + ls.push({ lon: f.geometry.coordinates[0], lat: f.geometry.coordinates[1], count: f.properties.total }) } } const features: Feature[] = [] const now = new Date().getTime() + "" for (const tileId of countPerTile.keys()) { - const coordinates = Tiles.centerPointOf(tileId) + const data = countPerTile.get(tileId) + const total = Lists.sum(data.map(d => d.count)) + const lon = Lists.sum(data.map(d => d.lon * d.count)) / total + const lat = Lists.sum(data.map(d => d.lat * d.count)) / total + features.push({ type: "Feature", properties: { - total_metric: "" + countPerTile.get(tileId), + total_metric: "" + total, id: "clustered_all_" + tileId + "_" + now, // We add the date to force a fresh ID every time, this makes sure values are updated }, geometry: { type: "Point", - coordinates, + coordinates: [lon, lat] }, }) } @@ -169,6 +177,10 @@ export class ClusterGrouping implements FeatureSource[]>) { + if (this.allSource.indexOf(features) >= 0) { + console.error("This source has already been registered") + return + } this.allSource.push(features) features.addCallbackAndRun(() => { //this.isDirty.set(true) diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 71a2edbcb..91504a441 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -4,8 +4,9 @@ import { Utils } from "../../../Utils" import GeoJsonSource from "../Sources/GeoJsonSource" import { BBox } from "../../BBox" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { Feature, Geometry } from "geojson" -export default class DynamicGeoJsonTileSource extends UpdatableDynamicTileSource { +export default class DynamicGeoJsonTileSource & {id: string} > > extends UpdatableDynamicTileSource { private static whitelistCache = new Map>>() constructor( diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts index 22b18b258..be79abc30 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts @@ -9,8 +9,10 @@ import Constants from "../../../Models/Constants" import { UpdatableFeatureSourceMerger } from "../Sources/FeatureSourceMerger" import { LineSourceMerger } from "./LineSourceMerger" import { PolygonSourceMerger } from "./PolygonSourceMerger" +import { OsmFeature, OsmTags } from "../../../Models/OsmFeature" +import { Feature, Point } from "geojson" -class PolygonMvtSource extends PolygonSourceMerger { +class PolygonMvtSource

& { id: string }> extends PolygonSourceMerger

{ constructor( layer: LayerConfig, mapProperties: { @@ -44,7 +46,7 @@ class PolygonMvtSource extends PolygonSourceMerger { } } -class LineMvtSource extends LineSourceMerger { +class LineMvtSource extends LineSourceMerger { constructor( layer: LayerConfig, mapProperties: { @@ -78,7 +80,7 @@ class LineMvtSource extends LineSourceMerger { } } -class PointMvtSource extends UpdatableDynamicTileSource { +class PointMvtSource> extends UpdatableDynamicTileSource { constructor( layer: LayerConfig, mapProperties: { @@ -102,7 +104,7 @@ class PointMvtSource extends UpdatableDynamicTileSource { layer: layer.id, type: "pois", }) - return new MvtSource(url, x, y, z) + return new MvtSource(url, x, y, z) }, mapProperties, { @@ -112,7 +114,7 @@ class PointMvtSource extends UpdatableDynamicTileSource { } } -export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger { +export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger { constructor( layer: LayerConfig, mapProperties: { @@ -124,9 +126,9 @@ export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger { } ) { super( - new PointMvtSource(layer, mapProperties, options), - new LineMvtSource(layer, mapProperties, options), - new PolygonMvtSource(layer, mapProperties, options) + new PointMvtSource(layer, mapProperties, options), + new LineMvtSource(layer, mapProperties, options), + new PolygonMvtSource(layer, mapProperties, options), ) } } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 9578db09d..9369a67ca 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -3,14 +3,15 @@ import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" +import { Feature, Geometry } from "geojson" /*** * A tiled source which dynamically loads the required tiles at a fixed zoom level. * A single featureSource will be initialized for every tile in view; which will later be merged into this featureSource */ -export default class DynamicTileSource< - Src extends FeatureSource = FeatureSource -> extends FeatureSourceMerger { +export default class DynamicTileSource = FeatureSource +> extends FeatureSourceMerger { private readonly loadedTiles = new Set() private readonly zDiff: number private readonly zoomlevel: Store @@ -97,9 +98,9 @@ export default class DynamicTileSource< } } -export class UpdatableDynamicTileSource - extends DynamicTileSource - implements UpdatableFeatureSource +export class UpdatableDynamicTileSource & {id: string}>, Src extends UpdatableFeatureSource = UpdatableFeatureSource> + extends DynamicTileSource + implements UpdatableFeatureSource { constructor( zoomlevel: Store, diff --git a/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts index 8290e70a3..f2d6a0aff 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts @@ -1,8 +1,7 @@ import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { Store } from "../../UIEventSource" import { BBox } from "../../BBox" -import { Utils } from "../../../Utils" -import { Feature, MultiLineString, Position } from "geojson" +import { Feature, LineString, MultiLineString, Position } from "geojson" import { GeoOperations } from "../../GeoOperations" import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { Lists } from "../../../Utils/Lists" @@ -11,15 +10,16 @@ import { Lists } from "../../../Utils/Lists" * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. * This is used to reconstruct polygons of vector tiles */ -export class LineSourceMerger extends UpdatableDynamicTileSource< - FeatureSourceForTile & UpdatableFeatureSource +export class LineSourceMerger

& { id: string }> extends UpdatableDynamicTileSource< + Feature, FeatureSourceForTile> & UpdatableFeatureSource> > { private readonly _zoomlevel: Store constructor( zoomlevel: Store, minzoom: number, - constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, + constructSource: (tileIndex: number) => FeatureSourceForTile< + Feature> & UpdatableFeatureSource>, mapProperties: { bounds: Store zoom: Store @@ -32,9 +32,9 @@ export class LineSourceMerger extends UpdatableDynamicTileSource< this._zoomlevel = zoomlevel } - protected addDataFromSources(sources: FeatureSourceForTile[]) { + protected addDataFromSources(sources: FeatureSourceForTile>[]) { sources = Lists.noNull(sources) - const all: Map> = new Map() + const all: Map> = new Map() const currentZoom = this._zoomlevel?.data ?? 0 for (const source of sources) { if (source.z != currentZoom) { @@ -48,10 +48,10 @@ export class LineSourceMerger extends UpdatableDynamicTileSource< } else if (f.geometry.type === "MultiLineString") { coordinates.push(...f.geometry.coordinates) } else { - console.error("Invalid geometry type:", f.geometry.type) + console.error("Invalid geometry type:", f.geometry["type"]) continue } - const oldV = all.get(id) + const oldV: Feature = all.get(id) if (!oldV) { all.set(id, { type: "Feature", @@ -63,7 +63,13 @@ export class LineSourceMerger extends UpdatableDynamicTileSource< }) continue } - oldV.geometry.coordinates.push(...coordinates) + for (const coordinate of coordinates) { + if (oldV.geometry.type === "LineString") { + oldV.geometry.coordinates.push(...coordinate) + } else { + oldV.geometry.coordinates.push(coordinate) + } + } } } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts index 5efb4c552..db0c4e04d 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/LocalStorageFeatureSource.ts @@ -2,11 +2,11 @@ import DynamicTileSource from "./DynamicTileSource" import { ImmutableStore, Store } from "../../UIEventSource" import { BBox } from "../../BBox" import TileLocalStorage from "../Actors/TileLocalStorage" -import { Feature } from "geojson" +import { Feature, Geometry } from "geojson" import StaticFeatureSource from "../Sources/StaticFeatureSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" -export default class LocalStorageFeatureSource extends DynamicTileSource { +export default class LocalStorageFeatureSource> extends DynamicTileSource { constructor( backend: string, layer: LayerConfig, @@ -30,8 +30,8 @@ export default class LocalStorageFeatureSource extends DynamicTileSource { new ImmutableStore(zoomlevel), layer.minzoom, (tileIndex) => - new StaticFeatureSource( - storage.getTileSource(tileIndex).mapD((features) => { + new StaticFeatureSource( + > storage.getTileSource(tileIndex).mapD((features) => { if (features.length === undefined) { console.trace("These are not features:", features) storage.invalidate(tileIndex) diff --git a/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts index 37bda203e..3af4901ba 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts @@ -1,7 +1,7 @@ import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { Store } from "../../UIEventSource" import { BBox } from "../../BBox" -import { Feature } from "geojson" +import { Feature, Polygon } from "geojson" import { GeoOperations } from "../../GeoOperations" import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { Lists } from "../../../Utils/Lists" @@ -10,13 +10,14 @@ import { Lists } from "../../../Utils/Lists" * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. * This is used to reconstruct polygons of vector tiles */ -export class PolygonSourceMerger extends UpdatableDynamicTileSource< - FeatureSourceForTile & UpdatableFeatureSource +export class PolygonSourceMerger

& { id: string }, + F extends Feature = Feature> extends UpdatableDynamicTileSource< + F, FeatureSourceForTile & UpdatableFeatureSource > { constructor( zoomlevel: Store, minzoom: number, - constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, + constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, mapProperties: { bounds: Store zoom: Store @@ -28,9 +29,9 @@ export class PolygonSourceMerger extends UpdatableDynamicTileSource< super(zoomlevel, minzoom, constructSource, mapProperties, options) } - protected addDataFromSources(sources: FeatureSourceForTile[]) { + protected addDataFromSources(sources: FeatureSourceForTile[]) { sources = Lists.noNull(sources) - const all: Map = new Map() + const all: Map = new Map() const zooms: Map = new Map() for (const source of sources) { @@ -60,7 +61,7 @@ export class PolygonSourceMerger extends UpdatableDynamicTileSource< zooms.set(id, z) continue } - const merged = GeoOperations.union(f, oldV) + const merged = GeoOperations.union(f, oldV) merged.properties = oldV.properties all.set(id, merged) zooms.set(id, z) diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 52211abbd..c7fdb66dc 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -53,11 +53,11 @@ export class GeoOperations { /** * Create a union between two features */ - public static union( + public static union

( f0: Feature, f1: Feature - ): Feature | null { - return turf.union(turf.featureCollection([f0, f1])) + ): Feature | null { + return turf.union

(turf.featureCollection([f0, f1])) } public static intersect( diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts index d4caf5b34..bff020e47 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -6,7 +6,6 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource" import ImageProvider, { ProvidedImage } from "./ImageProvider" import { WikidataImageProvider } from "./WikidataImageProvider" import Panoramax from "./Panoramax" -import { Utils } from "../../Utils" import { ServerSourceInfo } from "../../Models/SourceOverview" import { Lists } from "../../Utils/Lists" @@ -151,7 +150,7 @@ export default class AllImageProviders { } const source = Stores.concat(allSources).map((result) => { const all = result.flatMap((x) => x) - return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id]) + return Lists.dedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id]) }) this._cachedImageStores[cachekey] = source return source diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 5cbdd29a8..e9aa4cd69 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -15,6 +15,7 @@ import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement" import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import ExifReader from "exifreader" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../Web/IsOnline" /** * The ImageUploadManager has a @@ -81,10 +82,17 @@ export class ImageUploadManager { this._changes = changes this._gps = gpsLocation this._reportError = reportError - Stores.chronic(5 * 60000).addCallback(() => { + Stores.chronic(5 * 60000).addCallback(async () => { // If images failed to upload: attempt to reupload - this.uploadQueue() + await this.uploadQueue() }) + + IsOnline.isOnline.addCallback(async (isOnline) => { + if (isOnline) { + await this.uploadQueue() + } + }, + ) } public async canBeUploaded(file: File): Promise { @@ -171,6 +179,9 @@ export class ImageUploadManager { if (this.uploadingAll) { return } + if(!IsOnline.isOnline){ + return + } try { let queue: ImageUploadArguments[] const failed: Set = new Set() diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index a1bea5f5a..362a4ab25 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -11,6 +11,9 @@ import { Feature, Point } from "geojson" import { AddImageOptions } from "panoramax-js/dist/Panoramax" import { ServerSourceInfo } from "../../Models/SourceOverview" import { ComponentType } from "svelte/types/runtime/internal/dev" +import { Strings } from "../../Utils/Strings" +import { Utils } from "../../Utils" +import { Lists } from "../../Utils/Lists" export default class PanoramaxImageProvider extends ImageProvider { public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider() @@ -194,9 +197,13 @@ export default class PanoramaxImageProvider extends ImageProvider { public async DownloadAttribution(providedImage: { id: string }): Promise { const meta = await this.getInfoFor(providedImage.id) + const artists = Lists.noEmpty(meta.data.providers.map(p => p.name)) + + // We take the last provider, as that one probably contain the username of the uploader + const artist = artists.at(-1) return { - artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader + artist, date: new Date(meta.data.properties["datetime"]), licenseShortName: meta.data.properties["geovisio:license"], } diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index ea5db41c7..00f5e6167 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -15,7 +15,9 @@ export class WikimediaImageProvider extends ImageProvider { "https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", ] - public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"] + public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, + "http://commons.wikimedia.org/wiki/", + "http://upload.wikimedia.org", "File:"] private readonly commons_key = "wikimedia_commons" public readonly defaultKeyPrefixes = [this.commons_key, "image"] public readonly name = "Wikimedia" diff --git a/src/Logic/OfflineBasemapManager.ts b/src/Logic/OfflineBasemapManager.ts index 84e6a7cf9..d2e59eb82 100644 --- a/src/Logic/OfflineBasemapManager.ts +++ b/src/Logic/OfflineBasemapManager.ts @@ -398,7 +398,6 @@ export class OfflineBasemapManager { console.log("Not found in the archives:", { z, x, y }) return undefined } - console.log("Served tile", { z, x, y }, "from installed archive") return new Response(tileData.data, { headers: { "Content-Type": "application/x.protobuf" }, }) diff --git a/src/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts b/src/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts index eba4275ec..227ecb043 100644 --- a/src/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts +++ b/src/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts @@ -12,6 +12,7 @@ import CreateNewWayAction from "./CreateNewWayAction" import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig" import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import { Position } from "geojson" +import type { OsmFeature } from "../../../Models/OsmFeature" export interface MergePointConfig { withinRangeOfM: number @@ -71,7 +72,7 @@ export default class CreateWayWithPointReuseAction private readonly _state: { theme: ThemeConfig changes: Changes - indexedFeatures: IndexedFeatureSource + indexedFeatures: IndexedFeatureSource fullNodeDatabase?: FullNodeDatabaseSource } private readonly _config: MergePointConfig[] @@ -82,7 +83,7 @@ export default class CreateWayWithPointReuseAction state: { theme: ThemeConfig changes: Changes - indexedFeatures: IndexedFeatureSource + indexedFeatures: IndexedFeatureSource fullNodeDatabase?: FullNodeDatabaseSource }, config: MergePointConfig[] @@ -199,7 +200,7 @@ export default class CreateWayWithPointReuseAction } features.push(newGeometry) } - return StaticFeatureSource.fromGeojson(features) + return new StaticFeatureSource(features) } public async CreateChangeDescriptions(changes: Changes): Promise { diff --git a/src/Logic/Osm/Actions/ReplaceGeometryAction.ts b/src/Logic/Osm/Actions/ReplaceGeometryAction.ts index 4888dc650..304e56190 100644 --- a/src/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/src/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -14,6 +14,7 @@ import { OsmConnection } from "../OsmConnection" import { Feature, Geometry, LineString, Point } from "geojson" import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import { Lists } from "../../../Utils/Lists" +import { OsmFeature } from "../../../Models/OsmFeature" export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction { /** @@ -90,13 +91,13 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr public async getPreview(): Promise { const { closestIds, allNodesById, detachedNodes, reprojectedNodes } = await this.GetClosestIds() - const preview: Feature[] = closestIds.map((newId, i) => { + const preview: Feature[] = closestIds.map((newId, i) => { if (this.identicalTo[i] !== undefined) { return undefined } if (newId === undefined) { - return { + return { type: "Feature", properties: { newpoint: "yes", @@ -127,7 +128,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => { const origNode = allNodesById.get(nodeId) - const feature: Feature = { + const feature: Feature> = { type: "Feature", properties: { move: "yes", @@ -149,7 +150,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr detachedNodes.forEach(({ reason }, id) => { const origNode = allNodesById.get(id) - const feature: Feature = { + const feature: OsmFeature & Feature = { type: "Feature", properties: { detach: "yes", @@ -165,7 +166,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr preview.push(feature) }) - return StaticFeatureSource.fromGeojson(Lists.noNull(preview)) + return new StaticFeatureSource(Lists.noNull(preview)) } /** diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 122701495..4e2dc7933 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -19,6 +19,7 @@ import MarkdownUtils from "../../Utils/MarkdownUtils" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { Feature, Point } from "geojson" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../Web/IsOnline" /** * Handles all changes made to OSM. @@ -287,6 +288,10 @@ export class Changes { if (this.pendingChanges.data.length === 0) { return } + if(!IsOnline.isOnline.data){ + // No use to upload, we aren't connected anyway + return + } if (this.isUploading.data) { console.log("Is already uploading... Abort") return diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 733c4e5ec..0f44110cd 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -8,6 +8,7 @@ import Constants from "../../Models/Constants" import { Feature, Point } from "geojson" import { AndroidPolyfill } from "../Web/AndroidPolyfill" import { QueryParameters } from "../Web/QueryParameters" +import { IsOnline } from "../Web/IsOnline" interface OsmUserInfo { id: number @@ -131,6 +132,7 @@ export class OsmConnection { * Details of the currently logged-in user; undefined if not logged in */ public userDetails: UIEventSource + public isLoggedIn: Store public gpxServiceIsOnline: UIEventSource = new UIEventSource( "unknown" @@ -182,7 +184,7 @@ export class OsmConnection { this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET } - this.userDetails = new UIEventSource(undefined, "userDetails") + this.userDetails = UIEventSource.asObject(LocalStorageSource.get("user_details"), undefined) if (options.fakeUser) { const ud = this.userDetails.data ud.csCount = 5678 @@ -197,13 +199,7 @@ export class OsmConnection { } this.updateCapabilities() - this.isLoggedIn = this.userDetails.map( - (user) => - !!user && - (this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"), - [this.apiIsOnline] - ) - + this.isLoggedIn = this.userDetails.map((user) => !!user) this._dryRun = options.dryRun ?? new UIEventSource(false) if (options?.shared_cookie) { @@ -284,6 +280,9 @@ export class OsmConnection { } public async AttemptLogin() { + if (!IsOnline.isOnline.data) { + return + } this.updateCapabilities() if (this.loadingStatus.data !== "logged-in") { this.loadingStatus.setData("loading") @@ -308,6 +307,9 @@ export class OsmConnection { } private async loadUserInfo() { + if (!IsOnline.isOnline.data) { + return + } try { const result = await this.interact("user/details.json") if (result === null) { diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index db8dcfa27..e1dda7820 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -34,9 +34,8 @@ export class OsmPreferences { this.auth = auth this._fakeUser = fakeUser this.osmConnection = osmConnection - osmConnection.userDetails.addCallbackAndRunD(() => { + osmConnection.userDetails.once(() => { this.loadBulkPreferences() - return true }) } diff --git a/src/Logic/Osm/Overpass.ts b/src/Logic/Osm/Overpass.ts index 7ec59a323..c242367fc 100644 --- a/src/Logic/Osm/Overpass.ts +++ b/src/Logic/Osm/Overpass.ts @@ -3,9 +3,9 @@ import { Utils } from "../../Utils" import { ImmutableStore, Store } from "../UIEventSource" import { BBox } from "../BBox" import osmtogeojson from "osmtogeojson" -import { FeatureCollection, Geometry } from "geojson" -import { OsmTags } from "../../Models/OsmFeature" -;("use strict") +import { Feature, FeatureCollection } from "geojson" + +("use strict") /** * Interfaces overpass to get all the latest data */ @@ -37,7 +37,7 @@ export class Overpass { this._includeMeta = includeMeta } - public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { + public async queryGeoJson(bounds: BBox): Promise<[{ features: T[] } & FeatureCollection, Date]> { const bbox = "[bbox:" + bounds.getSouth() + @@ -49,16 +49,16 @@ export class Overpass { bounds.getEast() + "]" const query = this.buildScript(bbox) - return await this.ExecuteQuery(query) + return await this.executeQuery(query) } public buildUrl(query: string) { return `${this._interpreterUrl}?data=${encodeURIComponent(query)}` } - private async ExecuteQuery( + private async executeQuery( query: string - ): Promise<[FeatureCollection, Date]> { + ): Promise<[{ features: T[] } & FeatureCollection, Date]> { const json = await Utils.downloadJson<{ elements: [] remark @@ -73,7 +73,7 @@ export class Overpass { console.warn("No features for", this.buildUrl(query)) } - const geojson = >osmtogeojson(json) + const geojson = <{ features: T[] } & FeatureCollection>osmtogeojson(json) const osmTime = new Date(json.osm3s.timestamp_osm_base) return [geojson, osmTime] } diff --git a/src/Logic/Search/FilterSearch.ts b/src/Logic/Search/FilterSearch.ts index 6dbf346f2..7cc80b653 100644 --- a/src/Logic/Search/FilterSearch.ts +++ b/src/Logic/Search/FilterSearch.ts @@ -32,7 +32,7 @@ export default class FilterSearch { .split(" ") .map((query) => { if (!Strings.isEmoji(query)) { - return Utils.simplifyStringForSearch(query) + return Strings.simplifyStringForSearch(query) } return query }) @@ -64,7 +64,7 @@ export default class FilterSearch { option.searchTerms?.["en"] ?? []), ].flatMap((term) => [term, ...(term?.split(" ") ?? [])]) - terms = terms.map((t) => Utils.simplifyStringForSearch(t)) + terms = terms.map((t) => Strings.simplifyStringForSearch(t)) terms.push(option.emoji) Lists.noNullInplace(terms) const distances = queries.flatMap((query) => diff --git a/src/Logic/Search/LayerSearch.ts b/src/Logic/Search/LayerSearch.ts index 3d23767c4..ebe644122 100644 --- a/src/Logic/Search/LayerSearch.ts +++ b/src/Logic/Search/LayerSearch.ts @@ -2,7 +2,7 @@ import SearchUtils from "./SearchUtils" import ThemeSearch from "./ThemeSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" -import { Utils } from "../../Utils" +import { Strings } from "../../Utils/Strings" export default class LayerSearch { private readonly _theme: ThemeConfig @@ -24,7 +24,7 @@ export default class LayerSearch { const queryParts = query .trim() .split(" ") - .map((q) => Utils.simplifyStringForSearch(q)) + .map((q) => Strings.simplifyStringForSearch(q)) for (const id in ThemeSearch.officialThemes.layers) { if (options?.whitelist && !options?.whitelist.has(id)) { continue diff --git a/src/Logic/Search/LocalElementSearch.ts b/src/Logic/Search/LocalElementSearch.ts index b34e34bf4..e4d697816 100644 --- a/src/Logic/Search/LocalElementSearch.ts +++ b/src/Logic/Search/LocalElementSearch.ts @@ -6,6 +6,7 @@ import { GeoOperations } from "../GeoOperations" import { ImmutableStore, Store, Stores } from "../UIEventSource" import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch" import { Lists } from "../../Utils/Lists" +import { Strings } from "../../Utils/Strings" type IntermediateResult = { feature: Feature @@ -61,7 +62,7 @@ export default class LocalElementSearch implements GeocodingProvider { ...searchTerms .flatMap((entry) => entry.split(/ /)) .map((entry) => { - let simplified = Utils.simplifyStringForSearch(entry) + let simplified = Strings.simplifyStringForSearch(entry) if (matchStart) { simplified = simplified.slice(0, query.length) } @@ -103,7 +104,7 @@ export default class LocalElementSearch implements GeocodingProvider { const centerPoint: [number, number] = [center.lon, center.lat] const properties = this._state.perLayer const candidateId = OpenStreetMapIdSearch.extractId(query) - query = Utils.simplifyStringForSearch(query) + query = Strings.simplifyStringForSearch(query) const partials: Store[] = [] diff --git a/src/Logic/Search/PhotonSearch.ts b/src/Logic/Search/PhotonSearch.ts index 3f6af41bd..a34ade4b8 100644 --- a/src/Logic/Search/PhotonSearch.ts +++ b/src/Logic/Search/PhotonSearch.ts @@ -125,6 +125,10 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding if (query.length < 3) { return [] } + if(query.indexOf("https://") >=0){ + // Photon gives a '403 forbidden' when this is part of the search string + return [] + } const limit = this.searchLimit let bbox = "" if (options?.bbox && !this.ignoreBounds) { diff --git a/src/Logic/Search/SearchUtils.ts b/src/Logic/Search/SearchUtils.ts index a50ac5bf7..e5f993de2 100644 --- a/src/Logic/Search/SearchUtils.ts +++ b/src/Logic/Search/SearchUtils.ts @@ -2,6 +2,7 @@ import Locale from "../../UI/i18n/Locale" import { Utils } from "../../Utils" import ThemeSearch from "./ThemeSearch" import { Lists } from "../../Utils/Lists" +import { Strings } from "../../Utils/Strings" export default class SearchUtils { /** Applies special search terms, such as 'studio', 'osmcha', ... @@ -60,7 +61,7 @@ export default class SearchUtils { const queryParts = query .trim() .split(" ") - .map((q) => Utils.simplifyStringForSearch(q)) + .map((q) => Strings.simplifyStringForSearch(q)) let terms: string[] if (Array.isArray(keywords)) { terms = keywords @@ -74,7 +75,7 @@ export default class SearchUtils { const q = queryParts[i] let minDistance: number = 99 for (const term of termsAll) { - const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term)) + const d = Utils.levenshteinDistance(q, Strings.simplifyStringForSearch(term)) if (d < minDistance) { minDistance = d } diff --git a/src/Logic/Search/ThemeSearch.ts b/src/Logic/Search/ThemeSearch.ts index 96530d93d..060a41423 100644 --- a/src/Logic/Search/ThemeSearch.ts +++ b/src/Logic/Search/ThemeSearch.ts @@ -13,16 +13,22 @@ import { Lists } from "../../Utils/Lists" export class ThemeSearchIndex { private readonly themeIndex: Fuse private readonly layerIndex: Fuse<{ id: string; description }> + /** + * A 'search' will also search matching layers from the official themes. + * This might cause themes to show up which weren't passed in 'themesToSearch', so we create a whitelist + */ + private readonly themeWhitelist: Set constructor( language: string, - themesToSearch?: MinimalThemeInformation[], + themesToSearch: MinimalThemeInformation[], layersToIgnore: string[] = [] ) { - const themes = Utils.noNull(themesToSearch ?? ThemeSearch.officialThemes?.themes) + const themes = Utils.noNull(themesToSearch) if (!themes) { throw "No themes loaded. Did generate:layeroverview fail?" } + this.themeWhitelist = new Set(themes.map(th => th.id)) const fuseOptions: IFuseOptions = { ignoreLocation: true, threshold: 0.2, @@ -82,6 +88,10 @@ export class ThemeSearchIndex { const matchingThemes = ThemeSearch.layersToThemes.get(layer.item.id) const score = layer.score matchingThemes?.forEach((th) => { + if (!this.themeWhitelist.has(th.id)) { + // This theme was not in the 'themesToSearch' + return + } const previous = result.get(th.id) ?? 10000 result.set(th.id, Math.min(previous, score * 5)) }) diff --git a/src/UI/BigComponents/GeolocationControl.ts b/src/Logic/State/GeolocationControlState.ts similarity index 100% rename from src/UI/BigComponents/GeolocationControl.ts rename to src/Logic/State/GeolocationControlState.ts diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index fe78cab3d..b7d93bc58 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -389,7 +389,7 @@ export default class UserRelatedState { */ public addUnofficialTheme(themeInfo: MinimalThemeInformation) { const pref = this.osmConnection.getPreference("unofficial-theme-" + themeInfo.id) - this.osmConnection.isLoggedIn.when(() => pref.set(JSON.stringify(themeInfo))) + this.osmConnection.isLoggedIn.once(() => pref.set(JSON.stringify(themeInfo))) } public getUnofficialTheme(id: string): MinimalThemeInformation | undefined { diff --git a/src/Logic/Tags/And.ts b/src/Logic/Tags/And.ts index b1b69b8ab..77ec8e5bb 100644 --- a/src/Logic/Tags/And.ts +++ b/src/Logic/Tags/And.ts @@ -314,6 +314,14 @@ export class And extends TagsFilter { for (let j = i + 1; j < optimized.length; j++) { const ti = optimized[i] const tj = optimized[j] + if ( + !ti || + !tj || + typeof ti.shadows !== "function" || + typeof tj.shadows !== "function" + ) { + continue + } if (ti.shadows(tj)) { // if 'ti' is true, this implies 'tj' is always true as well. // if 'ti' is false, then 'tj' might be true or false @@ -322,6 +330,7 @@ export class And extends TagsFilter { // If 'ti' is true, then 'tj' will be true too and 'tj' can be ignored // If 'ti' is false, then the entire expression will be false and it doesn't matter what 'tj' yields optimized.splice(j, 1) + j-- } else if (tj.shadows(ti)) { optimized.splice(i, 1) i-- diff --git a/src/Logic/Tags/SubstitutingTag.ts b/src/Logic/Tags/SubstitutingTag.ts index e44e28394..9930a1de1 100644 --- a/src/Logic/Tags/SubstitutingTag.ts +++ b/src/Logic/Tags/SubstitutingTag.ts @@ -120,7 +120,7 @@ export default class SubstitutingTag extends TagsFilter { } isNegative(): boolean { - return false + return this._value === "" } visit(f: (tagsFilter: TagsFilter) => void) { diff --git a/src/Logic/Tags/TagTypes.ts b/src/Logic/Tags/TagTypes.ts index 9e19bc842..98730bb3b 100644 --- a/src/Logic/Tags/TagTypes.ts +++ b/src/Logic/Tags/TagTypes.ts @@ -13,7 +13,7 @@ type Brand = { [__is_optimized]: B } */ export type OptimizedTag = Brand -export type UploadableTag = Tag | SubstitutingTag | And +export type UploadableTag = Tag | SubstitutingTag | And /** * Not nested */ diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts index c09a77617..55b142506 100644 --- a/src/Logic/Tags/TagUtils.ts +++ b/src/Logic/Tags/TagUtils.ts @@ -328,22 +328,6 @@ export class TagUtils { return keyValues } - /** - * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags - */ - static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record): Tag[] { - const tags: Tag[] = [] - tagFilters.visit((tf: UploadableTag) => { - if (tf instanceof Tag) { - tags.push(tf) - } - if (tf instanceof SubstitutingTag) { - tags.push(tf.asTag(currentProperties)) - } - }) - return tags - } - static optimzeJson(json: TagConfigJson): TagConfigJson | boolean { const optimized = TagUtils.Tag(json).optimize() if (optimized === true || optimized === false) { @@ -994,11 +978,44 @@ export class TagUtils { ].join("\n") } - static fromProperties(tags: Record): TagConfigJson | boolean { + public static fromProperties(tags: Record): TagConfigJson | boolean { const opt = new And(Object.keys(tags).map((k) => new Tag(k, tags[k]))).optimize() if (opt === true || opt === false) { return opt } return opt.asJson() } + + /** + * Returns a similarly structured tag, but all tags with an empty value are removed. + * Those are assumed to be all met (and thus true) + * + * TagUtils.removeEmptyParts(new And([new Tag("a", "b"), new Tag("c", "")])) // => new Tag("a","b") + * TagUtils.removeEmptyParts(new And([new Tag("c", "")])) // => true + */ + public static removeEmptyParts(tag: UploadableTag): UploadableTag | true { + if (tag["and"]) { + const tags = tag["and"] + const cleaned = tags.map(t => TagUtils.removeEmptyParts(t)) + const filtered = cleaned.filter(t => t !== true) + if (filtered.length === 0) { + return true + } + return new And(filtered).optimize() + } + if (tag.isNegative()) { + return true + } + return tag + } + + static flattenAnd(tg: UploadableTag | UploadableTag[]): (SubstitutingTag | Tag)[] { + if (Array.isArray(tg)) { + return tg.flatMap(t => TagUtils.flattenAnd(t)) + } + if (tg["and"] || tg instanceof And) { + return tg["and"].flatMap(tg => TagUtils.flattenAnd(tg)) + } + return [tg] + } } diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index 22e2431cc..8d31037ec 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -1,5 +1,6 @@ import { Utils } from "../Utils" import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store" +import { Lists } from "../Utils/Lists" /** * Various static utils @@ -66,7 +67,7 @@ export class Stores { stable.setData(undefined) return } - if (Utils.sameList(stable.data, list)) { + if (Lists.sameList(stable.data, list)) { return } stable.setData(list) @@ -87,6 +88,8 @@ export class Stores { }) return newStore } + + } export abstract class Store implements Readable { @@ -338,8 +341,13 @@ export abstract class Store implements Readable { public abstract destroy() - when(callback: () => void, condition?: (v: T) => boolean) { - condition ??= (v) => v === true + /** + * Execute `f` once, but only if the value within is true-ish (or the explicit 'condition' is met) + * @param callback + * @param condition + */ + public once(callback: () => void, condition?: (v: T) => boolean) { + condition ??= (v) => !!v this.addCallbackAndRunD((v) => { if (condition(v)) { callback() @@ -347,6 +355,16 @@ export abstract class Store implements Readable { } }) } + + /** + * Create a new UIEVentSource. Whenever 'this.data' changes, the returned UIEventSource will get this value as well. + * However, this value can be overridden without affecting source + */ + public followingClone(): UIEventSource { + const src = new UIEventSource(this.data) + this.addCallback((t) => src.setData(t)) + return src + } } export class ImmutableStore extends Store { @@ -814,17 +832,6 @@ export class UIEventSource extends Store implements Writable { (b) => JSON.stringify(b) ?? "" ) } - - /** - * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. - * However, this value can be overriden without affecting source - */ - static feedFrom(store: Store): UIEventSource { - const src = new UIEventSource(store.data) - store.addCallback((t) => src.setData(t)) - return src - } - /** * Adds a callback * diff --git a/src/Logic/Web/AndroidPolyfill.ts b/src/Logic/Web/AndroidPolyfill.ts index 57436c6a2..b92e07b2b 100644 --- a/src/Logic/Web/AndroidPolyfill.ts +++ b/src/Logic/Web/AndroidPolyfill.ts @@ -120,7 +120,7 @@ export class AndroidPolyfill { if (typeof v === "string") { v = JSON.parse(v) } - console.log("Got inset sizes:", result) + console.log("Got inset sizes:", JSON.stringify(result)) insets.bottom.set(v.bottom / window.devicePixelRatio) insets.top.set(v.top / window.devicePixelRatio) }) @@ -186,4 +186,8 @@ export class AndroidPolyfill { } ) } + + static exit() { + this.databridgePlugin.request({key: "exit"}) + } } diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index b77ca4472..1c4f228ac 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -5,6 +5,7 @@ import { Feature, Position } from "geojson" import { GeoOperations } from "../GeoOperations" import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { WithUserRelatedState } from "../../Models/ThemeViewState/WithUserRelatedState" +import { IsOnline } from "./IsOnline" export interface ReviewCollection { readonly subjectUri?: Store @@ -238,11 +239,14 @@ export default class FeatureReviews implements ReviewCollection { if (!loadingAllowed.data) { return } + if (!IsOnline.isOnline.data) { + return + } const reviews = await MangroveReviews.getReviews({ sub }) console.debug("Got reviews for", feature, reviews, sub) this.addReviews(reviews.reviews, this._name.data) }, - [this._name, loadingAllowed] + [this._name, loadingAllowed, IsOnline.isOnline] ) /* We also construct all subject queries _without_ encoding the name to work around a previous bug * See https://github.com/giggls/opencampsitemap/issues/30 diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts index 576d012fb..75ea0f9f0 100644 --- a/src/Logic/Web/NearbyImagesSearch.ts +++ b/src/Logic/Web/NearbyImagesSearch.ts @@ -10,6 +10,7 @@ import { Point } from "geojson" import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist" import { Mapillary } from "../ImageProviders/Mapillary" import { ServerSourceInfo } from "../../Models/SourceOverview" +import { Lists } from "../../Utils/Lists" interface ImageFetcher { /** @@ -465,6 +466,6 @@ export class CombinedFetcher { this.fetchImage(source, lat, lon, state, sink) } - return { images: sink.mapD((imgs) => Utils.DedupOnId(imgs, (i) => i["id"])), state } + return { images: sink.mapD((imgs) => Lists.dedupOnId(imgs, (i) => i["id"])), state } } } diff --git a/src/Models/Denomination.ts b/src/Models/Denomination.ts index 659cc9a11..b3b8f9dd6 100644 --- a/src/Models/Denomination.ts +++ b/src/Models/Denomination.ts @@ -64,7 +64,7 @@ export class Denomination { throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead` } - const humanTexts = Translations.T(json.human) + const humanTexts: TypedTranslation<{ quantity: string }> = Translations.T(json.human) humanTexts.OnEveryLanguage((text, language) => { if (text.indexOf("{quantity}") < 0) { throw `In denomination: a human text should contain {quantity} (at ${context}.human.${language}). The offending text is: ${text}` @@ -85,21 +85,6 @@ export class Denomination { ) } - public clone() { - return new Denomination( - this.canonical, - this._canonicalSingular, - this.useIfNoUnitGiven, - this.prefix, - this.addSpace, - this.alternativeDenominations, - this.human, - this.humanSingular, - this.validator, - this.factorToCanonical - ) - } - public withBlankCanonical() { return new Denomination( "", @@ -181,18 +166,17 @@ export class Denomination { } value = value.toLowerCase() - const self = this - function startsWith(key) { - if (self.prefix) { + const startsWith = (key) => { + if (this.prefix) { return value.startsWith(key) } else { return value.endsWith(key) } } - function substr(key) { - if (self.prefix) { + const substr = (key) => { + if (this.prefix) { return value.substring(key.length).trim() } let trimmed = value.substring(0, value.length - key.length).trim() diff --git a/src/Models/MapProperties.ts b/src/Models/MapProperties.ts index b5a3f1974..e4e46ba58 100644 --- a/src/Models/MapProperties.ts +++ b/src/Models/MapProperties.ts @@ -18,6 +18,9 @@ export interface MapProperties { readonly maxbounds: UIEventSource readonly allowMoving: UIEventSource readonly allowRotating: UIEventSource + /** + * Current rotation of the map, ccw in degrees + */ readonly rotation: UIEventSource readonly pitch: UIEventSource readonly lastClickLocation: Store<{ diff --git a/src/Models/SourceOverview.ts b/src/Models/SourceOverview.ts index 5d4cf5e66..63d7a4606 100644 --- a/src/Models/SourceOverview.ts +++ b/src/Models/SourceOverview.ts @@ -10,7 +10,9 @@ import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig" import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson" import SpecialVisualizations from "../../src/UI/SpecialVisualizations" import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils" -import { QuestionableTagRenderingConfigJson } from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import { + QuestionableTagRenderingConfigJson +} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson" import { Lists } from "../Utils/Lists" @@ -206,7 +208,7 @@ export class SourceOverview { } urls = urls.filter((item) => !!item?.url) urls.sort((a, b) => (a < b ? -1 : 1)) - urls = Utils.DedupOnId(urls, (item) => item.url) + urls = Lists.dedupOnId(urls, (item) => item.url) this.eliUrlsCached = urls return urls } diff --git a/src/Models/ThemeConfig/Conversion/FixImages.ts b/src/Models/ThemeConfig/Conversion/FixImages.ts index 1eca0e2a9..eb680177e 100644 --- a/src/Models/ThemeConfig/Conversion/FixImages.ts +++ b/src/Models/ThemeConfig/Conversion/FixImages.ts @@ -190,10 +190,7 @@ export class ExtractImages extends Conversion< if (!allRenderedValuesAreImages && isImage) { // Extract images from the translations allFoundImages.push( - ...Translations.T( - img.leaf, - "extract_images from " + img.path.join(".") - ) + ...Translations.T(img.leaf) .ExtractImages(false) .map((path) => ({ path, diff --git a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts index 11b849c3c..6db428e67 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -1,18 +1,6 @@ -import { - Concat, - DesugaringContext, - DesugaringStep, - Each, - FirstOf, - Fuse, - On, - SetDefault, -} from "./Conversion" +import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion" import { LayerConfigJson } from "../Json/LayerConfigJson" -import { - MinimalTagRenderingConfigJson, - TagRenderingConfigJson, -} from "../Json/TagRenderingConfigJson" +import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { Utils } from "../../../Utils" import RewritableConfigJson from "../Json/RewritableConfigJson" import SpecialVisualizations from "../../../UI/SpecialVisualizations" @@ -1163,7 +1151,9 @@ export class PrepareLayer extends Fuse { "Fully prepares and expands a layer for the LayerConfig.", new DeriveSource(), new On("tagRenderings", new Each(new RewriteSpecial())), - new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), + new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(function (list: (T | T[])[]): T[] { + return Lists.flatten(list) + })), new On( "tagRenderings", (layer) => @@ -1180,7 +1170,9 @@ export class PrepareLayer extends Fuse { new On< (LineRenderingConfigJson | RewritableConfigJson)[], LayerConfigJson - >("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)), + >("lineRendering", new Each(new ExpandRewrite()).andThenF(function (list: (T | T[])[]): T[] { + return Lists.flatten(list) + })), new On( "pointRendering", (layer) => diff --git a/src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts b/src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts index 55918223e..1f73474fc 100644 --- a/src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrevalidateLayer.ts @@ -178,9 +178,7 @@ export class PrevalidateLayer extends DesugaringStep { { // Check for multiple, identical builtin questions - usability for studio users - const duplicates = Utils.Duplicates( - json.tagRenderings.filter((tr) => typeof tr === "string") - ) + const duplicates = Lists.duplicates(json.tagRenderings.filter((tr) => typeof tr === "string")) for (let i = 0; i < json.tagRenderings.length; i++) { const tagRendering = json.tagRenderings[i] if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) { @@ -209,7 +207,7 @@ export class PrevalidateLayer extends DesugaringStep { { // duplicate ids in tagrenderings check const duplicates = Lists.noNull( - Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"])) + Lists.duplicates(json.tagRenderings?.map((tr) => tr?.["id"])) ) if (duplicates?.length > 0) { // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list @@ -312,9 +310,7 @@ export class PrevalidateLayer extends DesugaringStep { ) } - const duplicateIds = Utils.Duplicates( - (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions") - ) + const duplicateIds = Lists.duplicates((json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")) if (duplicateIds.length > 0 && !Utils.runningFromConsole) { context .enter("tagRenderings") diff --git a/src/Models/ThemeConfig/Conversion/ValidateTheme.ts b/src/Models/ThemeConfig/Conversion/ValidateTheme.ts index e24ce8c46..25d2fbf22 100644 --- a/src/Models/ThemeConfig/Conversion/ValidateTheme.ts +++ b/src/Models/ThemeConfig/Conversion/ValidateTheme.ts @@ -7,6 +7,7 @@ import ThemeConfig from "../ThemeConfig" import { Utils } from "../../../Utils" import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation" import Constants from "../../Constants" +import { Lists } from "../../../Utils/Lists" export class ValidateTheme extends DesugaringStep { /** @@ -105,7 +106,7 @@ export class ValidateTheme extends DesugaringStep { } this._validateImage.convert(theme.icon, context.enter("icon")) } - const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) + const dups = Lists.duplicates(json.layers.map((layer) => layer["id"])) if (dups.length > 0) { context.err( `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 2b87c97b6..c4383e497 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -24,6 +24,8 @@ import { AvailableRasterLayers } from "../../RasterLayers" import { eliCategory } from "../../RasterLayerProperties" import licenses from "../../../assets/generated/license_info.json" import { Strings } from "../../../Utils/Strings" +import { Lists } from "../../../Utils/Lists" +import Objects from "../../../Utils/Objects" export class ValidateLanguageCompleteness extends DesugaringStep { private readonly _languages: string[] @@ -1064,7 +1066,7 @@ export class DetectDuplicatePresets extends DesugaringStep { const enNames = presets.map((p) => p.title.textFor("en")) if (new Set(enNames).size != enNames.length) { - const dups = Utils.Duplicates(enNames) + const dups = Lists.duplicates(enNames) const layersWithDup = json.layers.filter((l) => l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) ) @@ -1084,11 +1086,8 @@ export class DetectDuplicatePresets extends DesugaringStep { const presetBTags = optimizedTags[j] const presetB = presets[j] if ( - Utils.SameObject(presetATags, presetBTags) && - Utils.sameList( - presetA.preciseInput.snapToLayers, - presetB.preciseInput.snapToLayers - ) + Objects.sameObject(presetATags, presetBTags) && + Lists.sameList(presetA.preciseInput.snapToLayers, presetB.preciseInput.snapToLayers) ) { context.err( `This theme has multiple presets with the same tags: ${presetATags.asHumanString( diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index 34f4831f8..b0aa9cecc 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -216,6 +216,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs */ multiAnswer?: boolean + /** + * question: If one or more answers match (in case of a multiAnswer), what title/intro should be added? + * ifunset: don't show a title + */ + multiTitle?: Translatable + /** * Allow freeform text input from the user */ diff --git a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts index 4644b1a8f..4177ea817 100644 --- a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts +++ b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts @@ -45,6 +45,7 @@ export interface ThemeConfigJson { * question: What is the title of this theme? * * The human-readable title, as shown in the welcome message and the index page + * ifunset: reuse 'name' from the only layer * group: basic */ title: Translatable diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index 3fa07c22f..8ca0de4e2 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -13,7 +13,6 @@ import PointRenderingConfig from "./PointRenderingConfig" import WithContextLoader from "./WithContextLoader" import LineRenderingConfig from "./LineRenderingConfig" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" -import { Utils } from "../../Utils" import { TagsFilter } from "../../Logic/Tags/TagsFilter" import FilterConfigJson from "./Json/FilterConfigJson" import { Overpass } from "../../Logic/Osm/Overpass" @@ -340,7 +339,7 @@ export default class LayerConfig extends WithContextLoader { } { - const duplicateIds = Utils.Duplicates(this.filters.map((f) => f.id)) + const duplicateIds = Lists.duplicates(this.filters.map((f) => f.id)) if (duplicateIds.length > 0) { throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)` } diff --git a/src/Models/ThemeConfig/PointRenderingConfig.ts b/src/Models/ThemeConfig/PointRenderingConfig.ts index 7348231b9..8229fd8e2 100644 --- a/src/Models/ThemeConfig/PointRenderingConfig.ts +++ b/src/Models/ThemeConfig/PointRenderingConfig.ts @@ -129,7 +129,8 @@ export default class PointRenderingConfig extends WithContextLoader { context + ".rotationAlignment" ) } - private static FromHtmlMulti( + + private static fromHtmlMulti( multiSpec: string, tags: Store> ): BaseUIElement { @@ -207,7 +208,7 @@ export default class PointRenderingConfig extends WithContextLoader { : undefined let badges = undefined if (options?.includeBadges ?? true) { - badges = this.GetBadges(tags, options?.metatags) + badges = this.getBadges(tags, options?.metatags) } const iconAndBadges = new Combine([icon, badges]).SetClass("block relative") @@ -235,7 +236,7 @@ export default class PointRenderingConfig extends WithContextLoader { } else if (label === undefined) { htmlEl = new Combine([iconAndBadges]) } else { - htmlEl = new Combine([iconAndBadges, label]).SetStyle("flex flex-col") + htmlEl = new Combine([iconAndBadges, label]) } if (css !== undefined) { @@ -251,7 +252,7 @@ export default class PointRenderingConfig extends WithContextLoader { } } - private GetBadges( + private getBadges( tags: Store>, metaTags?: Store> ): BaseUIElement { @@ -283,15 +284,14 @@ export default class PointRenderingConfig extends WithContextLoader { if (htmlDefs.startsWith("<") && htmlDefs.endsWith(">")) { // This is probably an HTML-element return new FixedUiElement(Utils.SubstituteKeys(htmlDefs, tagsData)) - .SetStyle("width: 1.5rem") - .SetClass("block") + .SetClass("block w-6") } if (!htmlDefs) { return undefined } - const badgeElement = PointRenderingConfig.FromHtmlMulti( + const badgeElement = PointRenderingConfig.fromHtmlMulti( htmlDefs, tags )?.SetClass("block relative") @@ -299,8 +299,7 @@ export default class PointRenderingConfig extends WithContextLoader { return undefined } return new Combine([badgeElement]) - .SetStyle("width: 1.5rem") - .SetClass("block") + .SetClass("block w-6") }) return new Combine(badgeElements).SetClass("inline-flex h-full") @@ -318,9 +317,9 @@ export default class PointRenderingConfig extends WithContextLoader { const cssClassesLabel = this.labelCssClasses?.GetRenderValue(tags.data)?.txt return new VariableUiElement( tags.map((tags) => { - const label = this.label + const label = new VariableUiElement(this.label ?.GetRenderValue(tags) - ?.Subs(tags) + ?.Subs(tags).current) ?.SetClass("flex items-center justify-center absolute marker-label") ?.SetClass(cssClassesLabel) if (cssLabel) { diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index da027a349..29b342ed2 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -5,10 +5,7 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" import { And } from "../../Logic/Tags/And" import { Utils } from "../../Utils" import { Tag } from "../../Logic/Tags/Tag" -import { - MappingConfigJson, - QuestionableTagRenderingConfigJson, -} from "./Json/QuestionableTagRenderingConfigJson" +import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" import Validators, { ValidatorType } from "../../UI/InputElement/Validators" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" import { RegexTag } from "../../Logic/Tags/RegexTag" @@ -22,6 +19,8 @@ import LayerConfig from "./LayerConfig" import ComparingTag from "../../Logic/Tags/ComparingTag" import { Unit } from "../Unit" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../../Logic/Web/IsOnline" +import SubstitutingTag from "../../Logic/Tags/SubstitutingTag" export interface Mapping { readonly if: UploadableTag @@ -91,6 +90,7 @@ export default class TagRenderingConfig { } public readonly multiAnswer: boolean + public readonly multiTitle?: Translation public mappings: Mapping[] public readonly editButtonAriaLabel?: Translation @@ -163,6 +163,7 @@ export default class TagRenderingConfig { this.questionHintIsMd = json["questionHintIsMd"] ?? false this.alwaysForceSaveButton = json["#force-save-button"] === "yes" this.description = Translations.T(json.description, translationKey + ".description") + this.multiTitle = Translations.T(json.multiTitle, translationKey + ".multiTitle") this._definedIn = json._definedIn if (json.onSoftDelete && !Array.isArray(json.onSoftDelete)) { throw context + ".onSoftDelete Not an array: " + typeof json.onSoftDelete @@ -800,7 +801,7 @@ export default class TagRenderingConfig { multiSelectedMapping: boolean[] | undefined, currentProperties: Record, unit?: Unit - ): UploadableTag[] { + ): (Tag | SubstitutingTag)[] { if (typeof freeformValue === "string") { freeformValue = freeformValue?.trim() } @@ -817,7 +818,8 @@ export default class TagRenderingConfig { valueNoUnit, () => currentProperties["_country"] ) - freeformValue = formatted + denom.canonical + // In general, we want a space between the amount and the unit + freeformValue = formatted + " " + denom.canonical } else { freeformValue = validator.reformat( freeformValue, @@ -862,7 +864,7 @@ export default class TagRenderingConfig { const freeformOnly = { [this.freeform.key]: freeformValue } const matchingMapping = this.mappings?.find((m) => m.if.matchesProperties(freeformOnly)) if (matchingMapping) { - return [matchingMapping.if, ...(matchingMapping.addExtraTags ?? [])] + return [...TagUtils.flattenAnd(matchingMapping.if), ...(matchingMapping.addExtraTags ?? [])] } // Either no mappings, or this is a radio-button selected freeform value const tag = [ @@ -874,7 +876,7 @@ export default class TagRenderingConfig { return undefined } - return tag + return TagUtils.flattenAnd(tag) } if (this.multiAnswer) { @@ -904,7 +906,7 @@ export default class TagRenderingConfig { ) { return undefined } - return and + return TagUtils.flattenAnd(and) } // Is at least one mapping shown in the answer? @@ -924,11 +926,11 @@ export default class TagRenderingConfig { if (useFreeform) { return [ new Tag(this.freeform.key, freeformValue), - ...(this.freeform.addExtraTags ?? []), + ...(TagUtils.flattenAnd(this.freeform.addExtraTags) ?? []) ] } else if (singleSelectedMapping !== undefined) { return [ - this.mappings[singleSelectedMapping].if, + ...TagUtils.flattenAnd(this.mappings[singleSelectedMapping].if), ...(this.mappings[singleSelectedMapping].addExtraTags ?? []), ] } else { @@ -1167,10 +1169,10 @@ export class TagRenderingConfigUtils { const extraMappings = tags.bindD((tags) => { const country = tags._country if (country === undefined) { - return undefined + return undefined } const center = GeoOperations.centerpointCoordinates(feature) - return UIEventSource.fromPromise( + return UIEventSource.fromPromiseWithErr( NameSuggestionIndex.generateMappings( config.freeform.key, tags, @@ -1180,7 +1182,20 @@ export class TagRenderingConfigUtils { ) ) }) - return extraMappings.mapD((extraMappings) => { + return extraMappings.map((extraMappingsErr) => { + if(extraMappingsErr?.["error"]){ + console.log("Could not download the NSI: ", extraMappingsErr["error"]) + return config + } + const extraMappings = extraMappingsErr?.["success"] + if(extraMappings === undefined){ + if(!IsOnline.isOnline.data){ + // The 'extraMappings' will still attempt to download the NSI - it might be in the service worker's cache + // As such, if they happen to come through anyway, they'll be shown + return config + } + return undefined + } if (extraMappings.length == 0) { return config } @@ -1200,6 +1215,6 @@ export class TagRenderingConfigUtils { }) ?? [] clone.mappings = [...oldMappingsCloned, ...extraMappings] return clone - }) + }, [IsOnline.isOnline]) } } diff --git a/src/Models/ThemeViewState/UserMapFeatureswitchState.ts b/src/Models/ThemeViewState/UserMapFeatureswitchState.ts index f6cfbc442..98f7f9ad2 100644 --- a/src/Models/ThemeViewState/UserMapFeatureswitchState.ts +++ b/src/Models/ThemeViewState/UserMapFeatureswitchState.ts @@ -18,7 +18,6 @@ import { FeatureSource, WritableFeatureSource } from "../../Logic/FeatureSource/ import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import { WithUserRelatedState } from "./WithUserRelatedState" import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" -import { GeolocationControlState } from "../../UI/BigComponents/GeolocationControl" import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer" import { BBox } from "../../Logic/BBox" import ShowDataLayer from "../../UI/Map/ShowDataLayer" @@ -26,6 +25,7 @@ import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager" import { IsOnline } from "../../Logic/Web/IsOnline" import { Tiles } from "../TileRange" import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" +import { GeolocationControlState } from "../../Logic/State/GeolocationControlState" /** * The first core of the state management; everything related to: @@ -273,7 +273,6 @@ export class UserMapFeatureswitchState extends WithUserRelatedState { * * Note: this method is _incorrectly_ marked as not used */ - // @ts-ignore public showCurrentLocationOn(map: Store) { const id = "gps_location" const layer = this.theme.getLayer(id) diff --git a/src/Models/ThemeViewState/WithSpecialLayers.ts b/src/Models/ThemeViewState/WithSpecialLayers.ts index e1a5e163f..b961cd3e4 100644 --- a/src/Models/ThemeViewState/WithSpecialLayers.ts +++ b/src/Models/ThemeViewState/WithSpecialLayers.ts @@ -18,11 +18,10 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource" import { SummaryTileSource, - SummaryTileSourceRewriter, + SummaryTileSourceRewriter } from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions" import { ClusterGrouping } from "../../Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource" -import { OsmTags } from "../OsmFeature" export class WithSpecialLayers extends WithChangesState { readonly favourites: FavouritesFeatureSource diff --git a/src/Models/Unit.ts b/src/Models/Unit.ts index 9004dfc2f..a7e5428b7 100644 --- a/src/Models/Unit.ts +++ b/src/Models/Unit.ts @@ -1,7 +1,8 @@ -import BaseUIElement from "../UI/BaseUIElement" import { Denomination } from "./Denomination" import { Validator } from "../UI/InputElement/Validator" import FloatValidator from "../UI/InputElement/Validators/FloatValidator" +import { Translation } from "../UI/i18n/Translation" +import Translations from "../UI/i18n/Translations" export class Unit { public readonly appliesToKeys: Set @@ -109,7 +110,7 @@ export class Unit { return [undefined, undefined] } - asHumanLongValue(value: string | number, country: () => string): BaseUIElement | string { + asHumanLongValue(value: string | number, country: () => string): Translation { if (value === undefined) { return undefined } @@ -120,10 +121,10 @@ export class Unit { return human.Subs({ quantity: stripped + "/" }) } if (stripped === "1") { - return denom?.humanSingular ?? stripped + return Translations.T(denom?.humanSingular ?? stripped) } if (human === undefined) { - return stripped ?? value + return Translations.T(stripped ?? value) } return human.Subs({ quantity: stripped }) @@ -173,7 +174,6 @@ export class Unit { /** * Gets the value in the canonical denomination; * e.g. "1cm -> 0.01" as it is 0.01meter - * @param v */ public valueInCanonical(value: string, country: () => string): number { const denom = this.findDenomination(value, country) diff --git a/src/UI/AllThemesGui.svelte b/src/UI/AllThemesGui.svelte index 5845c70fc..61a0cdbdb 100644 --- a/src/UI/AllThemesGui.svelte +++ b/src/UI/AllThemesGui.svelte @@ -61,7 +61,7 @@ let userLanguages = osmConnection.userDetails.map((ud) => ud?.languages ?? []) let search: UIEventSource = new UIEventSource("") - let searchStable = search.stabilized(100) + let searchStable: Store = search.stabilized(100) const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter( (th) => th.hideFromOverview === false @@ -83,13 +83,12 @@ ).mapD((stableIds) => Lists.noNullInplace(stableIds.map((id) => state.getUnofficialTheme(id)))) function filtered(themes: Store): Store { - const searchIndex = Locale.language.map( - (language) => { - return new ThemeSearchIndex(language, themes.data) - }, - [themes] + const searchIndex = themes.mapD( + (themes) => new ThemeSearchIndex(Locale.language.data, themes), + [Locale.language] ) + return searchStable.map( (searchTerm) => { if (!themes.data) { @@ -99,11 +98,9 @@ return themes.data } - const index = searchIndex.data - - return index.search(searchTerm) + return searchIndex.data?.search(searchTerm) }, - [searchIndex] + [searchIndex, themes] ) } @@ -125,18 +122,21 @@ AndroidPolyfill.onBackButton( () => { + console.log("AllThemesGui received a backbutton from Android") + if(guistate.closeAll()){ + return true + } if (searchIsFocussed.data) { searchIsFocussed.set(false) return true } + // We'll probably want to exit the app + AndroidPolyfill.exit() return false }, { returnToIndex: new ImmutableStore(false) } ) - const topSPace = AndroidPolyfill.getInsetSizes().top - const bottom = AndroidPolyfill.getInsetSizes().bottom - /** * Opens the first search candidate */ @@ -207,7 +207,7 @@ - + {#if $recentThemes.length > 2}

diff --git a/src/UI/Base/Avatar.svelte b/src/UI/Base/Avatar.svelte new file mode 100644 index 000000000..47834b9d3 --- /dev/null +++ b/src/UI/Base/Avatar.svelte @@ -0,0 +1,20 @@ + + +{#if !$userdetails.img || !($loaded || $isOnline)} + +{:else} + avatar {loaded.set(true)}} /> +{/if} diff --git a/src/UI/Base/BackButton.svelte b/src/UI/Base/BackButton.svelte index 5f48bf7e6..d58867ea1 100644 --- a/src/UI/Base/BackButton.svelte +++ b/src/UI/Base/BackButton.svelte @@ -15,7 +15,7 @@ dispatch("click")} - options={{ extraClasses: twMerge("flex items-center", clss) }} + options={{ extraClasses: twMerge("flex items-center justify-start", clss) }} > diff --git a/src/UI/Base/LoginToggle.svelte b/src/UI/Base/LoginToggle.svelte index 632c1ac7e..859f8fb0c 100644 --- a/src/UI/Base/LoginToggle.svelte +++ b/src/UI/Base/LoginToggle.svelte @@ -14,10 +14,15 @@ osmConnection: OsmConnection featureSwitches?: { featureSwitchEnableLogin?: UIEventSource } } + /** + * Do show this element when in offline mode + */ + export let offline = false /** * If set, 'loading' will act as if we are already logged in. */ - export let ignoreLoading: boolean = false + export let ignoreLoading: boolean = offline // If it works in offline mode, it'll work while we are logging in too + /** * If set and the OSM-api fails, do _not_ show any error messages nor the successful state, just hide. * Will still show the "not-logged-in"-slot @@ -32,21 +37,26 @@ unknown: t.loginFailedUnreachableMode, readonly: t.loginFailedReadonlyMode, } - const apiState: Store = + const apiState: Store = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online") const online = IsOnline.isOnline + let loggedIn = state?.osmConnection?.isLoggedIn {#if $badge} - {#if !$online} + {#if !$online && !offline} {#if !hiddenFail} -
Your device is offline
+
+ +
{/if} {:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"} - {:else if $loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline"} + {:else if $loggedIn} + + {:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline" || $apiState === "unreachable")} {#if !hiddenFail}
@@ -61,8 +71,7 @@
{/if} - {:else if $loadingStatus === "logged-in"} - + {:else if $loadingStatus === "not-attempted"} {/if} diff --git a/src/UI/Base/MapControlButton.svelte b/src/UI/Base/MapControlButton.svelte index 56f6a51d0..85c9c7dab 100644 --- a/src/UI/Base/MapControlButton.svelte +++ b/src/UI/Base/MapControlButton.svelte @@ -22,7 +22,7 @@ use:ariaLabelStore={arialabelString} disabled={!$enabled} class={twJoin( - "pointer-events-auto relative h-fit w-fit rounded-full", + "pointer-events-auto relative h-fit w-fit rounded-full border-gray-500", cls, $enabled ? "" : "disabled" )} diff --git a/src/UI/Base/Searchbar.svelte b/src/UI/Base/Searchbar.svelte index 738808da2..25879b7dc 100644 --- a/src/UI/Base/Searchbar.svelte +++ b/src/UI/Base/Searchbar.svelte @@ -44,7 +44,7 @@
dispatch("search")}>