forked from MapComplete/MapComplete
		
	Merge pull request #1731 from pietervdvn/feature/favourites
Feature/favourites
This commit is contained in:
		
						commit
						4197ec0055
					
				
					 80 changed files with 2715 additions and 1059 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,7 @@ scratch | ||||||
| assets/editor-layer-index.json | assets/editor-layer-index.json | ||||||
| assets/generated/* | assets/generated/* | ||||||
| src/assets/generated/ | src/assets/generated/ | ||||||
|  | assets/layers/favourite/favourite.json | ||||||
| public/*.webmanifest | public/*.webmanifest | ||||||
| /*.html | /*.html | ||||||
| !/index.html | !/index.html | ||||||
|  |  | ||||||
							
								
								
									
										29
									
								
								Docs/UserTests/2023-12-4 User Test Favourites.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Docs/UserTests/2023-12-4 User Test Favourites.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | 
 | ||||||
|  | ## Task | ||||||
|  | 
 | ||||||
|  | Add a (specified) feature as favourite | ||||||
|  | Find and use the list of favourites | ||||||
|  | Determine information from this list | ||||||
|  | Open the popup from this list | ||||||
|  | 
 | ||||||
|  | ## Background info | ||||||
|  | 
 | ||||||
|  | User has used mapcomplete before | ||||||
|  | 
 | ||||||
|  | ## Results | ||||||
|  | 
 | ||||||
|  | The user is asked to mark a specified bicycle shop as favourite. They find the big button to mark as favourite at the bottom. | ||||||
|  | 
 | ||||||
|  | When asked to select another feature, they choose a bicycle pump. When hinted that 'they can add this in a different way', they immediately select the heart title icon. | ||||||
|  | 
 | ||||||
|  | When asked to open the list of favourites, they open the 'hamburger'-menu. After a bit of looking, they spot the 'Your favourites'-button. | ||||||
|  | 
 | ||||||
|  | They are a bit confused. The specified bicycle shop is advertised as `building or wall`. | ||||||
|  | 
 | ||||||
|  | The bicycle pump is shown correctly, the icons are clear. When asked to open the popup for one of them, they click directly on the link. | ||||||
|  | 
 | ||||||
|  | ## Surfaced issues | ||||||
|  | 
 | ||||||
|  | Due to the way the title is generated, wrong titles appeared: all titles from all layers are mixed and used as title, if the tags match. As such, the title `building or wall` appeared, as it happened to be on top and the bicycle shop had a `building~*` tag. | ||||||
|  | 
 | ||||||
|  | This was resolved by sorting those titles by popularity. The least occuring tags/titles are placed first, so that the most specific title is shown. This might, in some cases, still result in differing titles (e.g. if something is e.g. both a shop and a café), but this should be exceptional. | ||||||
|  | @ -29,7 +29,8 @@ | ||||||
|             "natural=stone" |             "natural=stone" | ||||||
|           ] |           ] | ||||||
|         }, |         }, | ||||||
|         "climbing=" |         "climbing=", | ||||||
|  |         "sport!=climbing" | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								assets/layers/favourite/favourite.proto.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								assets/layers/favourite/favourite.proto.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | { | ||||||
|  |   "#":"no-translations", | ||||||
|  |   "#dont-translate": "*", | ||||||
|  |   "pointRendering": [ | ||||||
|  |     { | ||||||
|  |       "location": [ | ||||||
|  |         "point", | ||||||
|  |         "centroid" | ||||||
|  |       ], | ||||||
|  |       "marker": [ | ||||||
|  |         { | ||||||
|  |           "icon": { | ||||||
|  |             "render": "heart", | ||||||
|  |             "mappings": [ | ||||||
|  |               { | ||||||
|  |                 "if": "_favourite=no", | ||||||
|  |                 "then": "heart_outline" | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           }, | ||||||
|  |           "color": "red" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "description": { | ||||||
|  |     "en": "A generic map layer which shows locations that a contributor marked as favourite", | ||||||
|  |     "nl": "Een laag met persoonlijke favourieten" | ||||||
|  |   }, | ||||||
|  |   "name": { | ||||||
|  |     "en": "Favourites", | ||||||
|  |     "nl": "Favorieten" | ||||||
|  |   }, | ||||||
|  |   "id": "favourite", | ||||||
|  |   "source": "special", | ||||||
|  |   "isShown": "_favourite=yes", | ||||||
|  |   "minzoom": 0, | ||||||
|  |   "title": { | ||||||
|  |     "render": { | ||||||
|  |       "en": "Favourite location", | ||||||
|  |       "nl": "Favoriete locatie" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "tagRenderings": [ | ||||||
|  |      | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | @ -14,7 +14,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "wikipedialink", |       "id": "wikipedialink", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank' rel='noopener'><img src='./assets/svg/wikipedia.svg' textmode='📖' alt='Wikipedia'/></a>", |       "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank' rel='noopener'><img src='./assets/svg/wikipedia.svg' textmode='📖' alt='Wikipedia'/></a>", | ||||||
|       "condition": { |       "condition": { | ||||||
|  | @ -66,10 +67,23 @@ | ||||||
|       ], |       ], | ||||||
|       "metacondition": "__showTimeSensitiveIcons!=no" |       "metacondition": "__showTimeSensitiveIcons!=no" | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       "id": "open_until", | ||||||
|  |       "labels": [ | ||||||
|  |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|  |       ], | ||||||
|  |       "#": "Titleicon showing 'open until 17:00'", | ||||||
|  |       "icon": { | ||||||
|  |         "class": "w-20 mx-1 flex items-center" | ||||||
|  |       }, | ||||||
|  |       "render": "{opening_hours_state()}" | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       "id": "phonelink", |       "id": "phonelink", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "render": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>", |       "render": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>", | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|  | @ -89,7 +103,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "emaillink", |       "id": "emaillink", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>", |       "render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>", | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|  | @ -109,7 +124,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "websitelink", |       "id": "websitelink", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>", |       "render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>", | ||||||
|       "condition": "website~*" |       "condition": "website~*" | ||||||
|  | @ -117,7 +133,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "smokingicon", |       "id": "smokingicon", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  | @ -140,6 +157,15 @@ | ||||||
|       "render": "{share_link()}", |       "render": "{share_link()}", | ||||||
|       "metacondition": "_supports_sharing=yes" |       "metacondition": "_supports_sharing=yes" | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       "id": "favourite_title_icon", | ||||||
|  |       "labels": [ | ||||||
|  |         "defaults" | ||||||
|  |       ], | ||||||
|  |       "render": { | ||||||
|  |         "*": "{favourite_icon()}" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       "id": "osmlink", |       "id": "osmlink", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|  | @ -162,7 +188,8 @@ | ||||||
|     { |     { | ||||||
|       "id": "dogicon", |       "id": "dogicon", | ||||||
|       "labels": [ |       "labels": [ | ||||||
|         "defaults" |         "defaults", | ||||||
|  |         "in_favourite" | ||||||
|       ], |       ], | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|  | @ -193,6 +220,13 @@ | ||||||
|         "class": "w-20 mx-1 flex items-center" |         "class": "w-20 mx-1 flex items-center" | ||||||
|       }, |       }, | ||||||
|       "render": "{rating()}" |       "render": "{rating()}" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "id": "favourite_icon", | ||||||
|  |       "description": "Only for rendering", | ||||||
|  |       "condition": "_favourite=yes", | ||||||
|  |       "icon": "circle:white;heart:red", | ||||||
|  |       "metacondition": "__showTimeSensitiveIcons!=no" | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								assets/svg/center.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								assets/svg/center.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <svg | ||||||
|  |    width="544.02838" | ||||||
|  |    height="544.02838" | ||||||
|  |    viewBox="0 0 544.02838 544.02838" | ||||||
|  |    version="1.1" | ||||||
|  |    id="svg1" | ||||||
|  |    sodipodi:docname="center.svg" | ||||||
|  |    inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" | ||||||
|  |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg"> | ||||||
|  |   <defs | ||||||
|  |      id="defs1" /> | ||||||
|  |   <sodipodi:namedview | ||||||
|  |      id="namedview1" | ||||||
|  |      pagecolor="#505050" | ||||||
|  |      bordercolor="#eeeeee" | ||||||
|  |      borderopacity="1" | ||||||
|  |      inkscape:showpageshadow="0" | ||||||
|  |      inkscape:pageopacity="0" | ||||||
|  |      inkscape:pagecheckerboard="0" | ||||||
|  |      inkscape:deskcolor="#d1d1d1" | ||||||
|  |      showguides="true" | ||||||
|  |      inkscape:zoom="0.90326851" | ||||||
|  |      inkscape:cx="393.57068" | ||||||
|  |      inkscape:cy="250.756" | ||||||
|  |      inkscape:window-width="1920" | ||||||
|  |      inkscape:window-height="995" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="0" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="svg1"> | ||||||
|  |     <sodipodi:guide | ||||||
|  |        position="171.95879,103.32864" | ||||||
|  |        orientation="0,-1" | ||||||
|  |        id="guide4" | ||||||
|  |        inkscape:locked="false" /> | ||||||
|  |     <sodipodi:guide | ||||||
|  |        position="271.68286,132.35281" | ||||||
|  |        orientation="1,0" | ||||||
|  |        id="guide5" | ||||||
|  |        inkscape:locked="false" /> | ||||||
|  |   </sodipodi:namedview> | ||||||
|  |   <path | ||||||
|  |      d="m 365.63918,111.75001 h -62.375 V 15.9375 c 0,-8.75 -7,-15.9375 -15.625,-15.9375 h -31.1875 c -8.5625,0 -15.625,7.1875 -15.625,15.9375 v 95.81251 h -62.375 l 93.5625,127.75 z" | ||||||
|  |      id="path1" | ||||||
|  |      sodipodi:nodetypes="ccsssscccc" /> | ||||||
|  |   <path | ||||||
|  |      d="m 432.27837,365.63919 v -62.375 h 95.8125 c 8.75,0 15.9375,-7 15.9375,-15.625 v -31.1875 c 0,-8.5625 -7.1875,-15.625 -15.9375,-15.625 h -95.8125 v -62.375 l -127.75,93.5625 z" | ||||||
|  |      id="path1-5" | ||||||
|  |      sodipodi:nodetypes="ccsssscccc" /> | ||||||
|  |   <path | ||||||
|  |      d="m 178.38918,432.27838 h 62.375 v 95.8125 c 0,8.75 7,15.9375 15.625,15.9375 h 31.1875 c 8.5625,0 15.625,-7.1875 15.625,-15.9375 v -95.8125 h 62.375 l -93.5625,-127.75 z" | ||||||
|  |      id="path2" | ||||||
|  |      sodipodi:nodetypes="ccsssscccc" /> | ||||||
|  |   <path | ||||||
|  |      d="m 111.75,178.38919 v 62.375 H 15.9375 c -8.75,0 -15.9375,7 -15.9375,15.625 v 31.1875 c 0,8.5625 7.1875,15.625 15.9375,15.625 H 111.75 v 62.375 l 127.74999,-93.5625 z" | ||||||
|  |      id="path3" | ||||||
|  |      sodipodi:nodetypes="ccsssscccc" /> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										2
									
								
								assets/svg/center.svg.license
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								assets/svg/center.svg.license
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | SPDX-FileCopyrightText: Pieter Vander Vennet | ||||||
|  | SPDX-License-Identifier: CC0-1.0 | ||||||
|  | @ -153,6 +153,14 @@ | ||||||
|       "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" |       "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" | ||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     "path": "center.svg", | ||||||
|  |     "license": "CC0-1.0", | ||||||
|  |     "authors": [ | ||||||
|  |       "Pieter Vander Vennet" | ||||||
|  |     ], | ||||||
|  |     "sources": [] | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     "path": "checkmark.svg", |     "path": "checkmark.svg", | ||||||
|     "license": "CC0-1.0", |     "license": "CC0-1.0", | ||||||
|  |  | ||||||
|  | @ -69,10 +69,12 @@ | ||||||
|         }, |         }, | ||||||
|         "+titleIcons": [ |         "+titleIcons": [ | ||||||
|           { |           { | ||||||
|  |             "id": "climbing_length", | ||||||
|             "render": "<div class='flex' style='word-wrap: normal; padding-right: 0.25rem;'><img src='./assets/themes/climbing/height.svg' style='height: 1.75rem;'/>{climbing:length}m</div>", |             "render": "<div class='flex' style='word-wrap: normal; padding-right: 0.25rem;'><img src='./assets/themes/climbing/height.svg' style='height: 1.75rem;'/>{climbing:length}m</div>", | ||||||
|             "condition": "climbing:length~*" |             "condition": "climbing:length~*" | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|  |             "id": "climbing_bolts", | ||||||
|             "mappings": [ |             "mappings": [ | ||||||
|               { |               { | ||||||
|                 "if": "__bolts_max~*", |                 "if": "__bolts_max~*", | ||||||
|  | @ -95,6 +97,7 @@ | ||||||
|             "render": "<div class='w-8 flex justify-center rounded-right-full climbing-{__difficulty_max:char}'> {__difficulty_max}</div>" |             "render": "<div class='w-8 flex justify-center rounded-right-full climbing-{__difficulty_max:char}'> {__difficulty_max}</div>" | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|  |             "id": "difficulty", | ||||||
|             "render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>", |             "render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>", | ||||||
|             "condition": "__difficulty:char~*" |             "condition": "__difficulty:char~*" | ||||||
|           } |           } | ||||||
|  |  | ||||||
|  | @ -166,31 +166,31 @@ | ||||||
|                 { |                 { | ||||||
|                   "if": "sidewalk:left|right=yes", |                   "if": "sidewalk:left|right=yes", | ||||||
|                   "then": { |                   "then": { | ||||||
|                     "en": "Yes, there is a sidewalk on this side of the road", |                     "en": "There is a sidewalk on this side of the road", | ||||||
|                     "de": "Ja, es gibt einen Bürgersteig auf dieser Straßenseite", |                     "de": "Es gibt einen Bürgersteig auf dieser Straßenseite", | ||||||
|                     "da": "Ja, der er et fortov på denne side af vejen", |                     "da": "Der er et fortov på denne side af vejen", | ||||||
|                     "nl": "Ja, er is een stoep aan deze kant van de weg", |                     "nl": "Er is een stoep aan deze kant van de weg", | ||||||
|                     "fr": "Oui, il y a un trottoir de ce côté de la route", |                     "fr": "Il y a un trottoir de ce côté de la route", | ||||||
|                     "ca": "Sí, hi ha una vorera a aquest costat del carrer", |                     "ca": "Hi ha una vorera a aquest costat del carrer", | ||||||
|                     "es": "Sí, hay una acera en este lado de la calle", |                     "es": "Hay una acera en este lado de la calle", | ||||||
|                     "cs": "Ano, na této straně silnice je chodník", |                     "cs": "Na této straně silnice je chodník", | ||||||
|                     "it": "Sì, c'è un marciapiede su questo lato della strada", |                     "it": "C'è un marciapiede su questo lato della strada", | ||||||
|                     "pl": "Tak, jest chodnik z boku drogi" |                     "pl": "Jest chodnik z boku drogi" | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                   "if": "sidewalk:left|right=no", |                   "if": "sidewalk:left|right=no", | ||||||
|                   "then": { |                   "then": { | ||||||
|                     "en": "No, there is no sidewalk to walk on", |                     "en": "There is no sidewalk to walk on", | ||||||
|                     "de": "Nein, es gibt keinen Bürgersteig für Fußgänger", |                     "de": "Es gibt keinen Bürgersteig für Fußgänger", | ||||||
|                     "da": "Nej, der er ikke noget fortov at gå på", |                     "da": "Der er ikke noget fortov at gå på", | ||||||
|                     "nl": "Nee, er is geen stoep om op te lopen", |                     "nl": "Er is geen stoep om op te lopen", | ||||||
|                     "fr": "Non, il n'y a pas de trottoir où marcher", |                     "fr": "Il n'y a pas de trottoir où marcher", | ||||||
|                     "ca": "No, no hi ha vorera per la que caminar", |                     "ca": "No hi ha vorera per la que caminar", | ||||||
|                     "es": "No, no hay acera por la que caminar", |                     "es": "No hay acera por la que caminar", | ||||||
|                     "cs": "Ne, není tu žádný chodník", |                     "cs": "Není tu žádný chodník", | ||||||
|                     "it": "No, non c'è un marciapiede su cui camminare", |                     "it": "Non c'è un marciapiede su cui camminare", | ||||||
|                     "pl": "Nie, nie ma chodnika, którym można chodzić" |                     "pl": "Nie ma chodnika, którym można chodzić" | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|  |  | ||||||
|  | @ -349,4 +349,4 @@ | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -50,6 +50,22 @@ | ||||||
|         "panelIntro": "<h3>Your personal theme</h3>Activate your favourite layers from all the official themes", |         "panelIntro": "<h3>Your personal theme</h3>Activate your favourite layers from all the official themes", | ||||||
|         "reload": "Reload the data" |         "reload": "Reload the data" | ||||||
|     }, |     }, | ||||||
|  |     "favouritePoi": { | ||||||
|  |         "button": { | ||||||
|  |             "isFavourite": "This location is currently marked as favourite and will show up on all thematic maps of MapComplete you visit.", | ||||||
|  |             "markAsFavouriteTitle": "Mark this location as favourite location", | ||||||
|  |             "markDescription": "Add this location to a personal list of your favourites", | ||||||
|  |             "unmark": "Remove from your personal list of favourites", | ||||||
|  |             "unmarkNotDeleted": "This point will not be deleted and still be visible on the appropriate map for you and others" | ||||||
|  |         }, | ||||||
|  |         "downloadGeojson": "Download your favourites as geojson", | ||||||
|  |         "downloadGpx": "Download your favourites as GPX", | ||||||
|  |         "intro": "You marked {length} locations as a favourite location.", | ||||||
|  |         "introPrivacy": "This list is only visible to you", | ||||||
|  |         "loginToSeeList": "Login to see the list of locations you marked as favourite", | ||||||
|  |         "tab": "Your favourites", | ||||||
|  |         "title": "Your favourite locations" | ||||||
|  |     }, | ||||||
|     "flyer": { |     "flyer": { | ||||||
|         "aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen", |         "aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen", | ||||||
|         "callToAction": "Test it on mapcomplete.org", |         "callToAction": "Test it on mapcomplete.org", | ||||||
|  | @ -404,6 +420,7 @@ | ||||||
|         "key": "Key combination", |         "key": "Key combination", | ||||||
|         "openLayersPanel": "Opens the layers and filters panel", |         "openLayersPanel": "Opens the layers and filters panel", | ||||||
|         "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", |         "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", | ||||||
|  |         "selectFavourites": "Open the favourites page", | ||||||
|         "selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used", |         "selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used", | ||||||
|         "selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers", |         "selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers", | ||||||
|         "selectMapnik": "Set the background layer to OpenStreetMap-carto", |         "selectMapnik": "Set the background layer to OpenStreetMap-carto", | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "mapcomplete", |   "name": "mapcomplete", | ||||||
|   "version": "0.35.2", |   "version": "0.36.0", | ||||||
|   "repository": "https://github.com/pietervdvn/MapComplete", |   "repository": "https://github.com/pietervdvn/MapComplete", | ||||||
|   "description": "A small website to edit OSM easily", |   "description": "A small website to edit OSM easily", | ||||||
|   "bugs": "https://github.com/pietervdvn/MapComplete/issues", |   "bugs": "https://github.com/pietervdvn/MapComplete/issues", | ||||||
|  | @ -65,7 +65,7 @@ | ||||||
|     "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak", |     "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak", | ||||||
|     "optimize-images": "cd assets/generated/ &&  find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", |     "optimize-images": "cd assets/generated/ &&  find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", | ||||||
|     "generate:stats": "vite-node scripts/GenerateSeries.ts", |     "generate:stats": "vite-node scripts/GenerateSeries.ts", | ||||||
|     "reset:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json  && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force", |     "reset:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force", | ||||||
|     "generate": "mkdir -p ./assets/generated; npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run reset:layeroverview; npm run generate:service-worker", |     "generate": "mkdir -p ./assets/generated; npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run reset:layeroverview; npm run generate:service-worker", | ||||||
|     "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", |     "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", | ||||||
|     "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", |     "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", | ||||||
|  |  | ||||||
|  | @ -745,6 +745,10 @@ video { | ||||||
|   top: 2.5rem; |   top: 2.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .left-1\/4 { | ||||||
|  |   left: 25%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .isolate { | .isolate { | ||||||
|   isolation: isolate; |   isolation: isolate; | ||||||
| } | } | ||||||
|  | @ -765,10 +769,6 @@ video { | ||||||
|   float: left; |   float: left; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .m-8 { |  | ||||||
|   margin: 2rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .m-4 { | .m-4 { | ||||||
|   margin: 1rem; |   margin: 1rem; | ||||||
| } | } | ||||||
|  | @ -781,6 +781,10 @@ video { | ||||||
|   margin: 0px; |   margin: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .m-8 { | ||||||
|  |   margin: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .m-2 { | .m-2 { | ||||||
|   margin: 0.5rem; |   margin: 0.5rem; | ||||||
| } | } | ||||||
|  | @ -841,10 +845,6 @@ video { | ||||||
|   margin-right: 3rem; |   margin-right: 3rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .mb-4 { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mt-4 { | .mt-4 { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  | @ -881,6 +881,10 @@ video { | ||||||
|   margin-right: 0.25rem; |   margin-right: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .mb-4 { | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .ml-1 { | .ml-1 { | ||||||
|   margin-left: 0.25rem; |   margin-left: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -1088,6 +1092,10 @@ video { | ||||||
|   height: 2.75rem; |   height: 2.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-5 { | ||||||
|  |   height: 1.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-48 { | .h-48 { | ||||||
|   height: 12rem; |   height: 12rem; | ||||||
| } | } | ||||||
|  | @ -1198,6 +1206,14 @@ video { | ||||||
|   width: 50%; |   width: 50%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .w-14 { | ||||||
|  |   width: 3.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .w-5 { | ||||||
|  |   width: 1.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .w-10 { | .w-10 { | ||||||
|   width: 2.5rem; |   width: 2.5rem; | ||||||
| } | } | ||||||
|  | @ -1289,6 +1305,10 @@ video { | ||||||
|           appearance: none; |           appearance: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .grid-cols-2 { | ||||||
|  |   grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .grid-cols-1 { | .grid-cols-1 { | ||||||
|   grid-template-columns: repeat(1, minmax(0, 1fr)); |   grid-template-columns: repeat(1, minmax(0, 1fr)); | ||||||
| } | } | ||||||
|  | @ -1441,6 +1461,14 @@ video { | ||||||
|   align-self: center; |   align-self: center; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .justify-self-start { | ||||||
|  |   justify-self: start; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .justify-self-end { | ||||||
|  |   justify-self: end; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .overflow-auto { | .overflow-auto { | ||||||
|   overflow: auto; |   overflow: auto; | ||||||
| } | } | ||||||
|  | @ -2335,6 +2363,16 @@ button.disabled:hover, .button.disabled:hover { | ||||||
|   color: unset; |   color: unset; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | button.link { | ||||||
|  |   border: none; | ||||||
|  |   text-decoration: underline; | ||||||
|  |   background-color: unset; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button.link:hover { | ||||||
|  |   color:unset; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .interactive button.disabled svg path, .interactive .button.disabled svg path { | .interactive button.disabled svg path, .interactive .button.disabled svg path { | ||||||
|   fill: var(--interactive-foreground) !important; |   fill: var(--interactive-foreground) !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ mkdir dist 2> /dev/null | ||||||
| mkdir dist/assets 2> /dev/null | mkdir dist/assets 2> /dev/null | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export NODE_OPTIONS="--max-old-space-size=8192" | export NODE_OPTIONS="--max-old-space-size=16384" | ||||||
| 
 | 
 | ||||||
| # This script ends every line with '&&' to chain everything. A failure will thus stop the build | # This script ends every line with '&&' to chain everything. A failure will thus stop the build | ||||||
| npm run generate:editor-layer-index && | npm run generate:editor-layer-index && | ||||||
|  | @ -48,7 +48,7 @@ else | ||||||
|   exit 1 |   exit 1 | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
| export NODE_OPTIONS=--max-old-space-size=7000 | export NODE_OPTIONS=--max-old-space-size=16000 | ||||||
| which vite | which vite | ||||||
| vite build --sourcemap  | vite build --sourcemap  | ||||||
| # Copy the layer files, as these might contain assets (e.g. svgs) | # Copy the layer files, as these might contain assets (e.g. svgs) | ||||||
|  |  | ||||||
							
								
								
									
										304
									
								
								scripts/generateFavouritesLayer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								scripts/generateFavouritesLayer.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,304 @@ | ||||||
|  | import Script from "./Script" | ||||||
|  | import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" | ||||||
|  | import { existsSync, readFileSync, writeFileSync } from "fs" | ||||||
|  | import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" | ||||||
|  | import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts" | ||||||
|  | import { Utils } from "../src/Utils" | ||||||
|  | import { AddEditingElements } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" | ||||||
|  | import { | ||||||
|  |     MappingConfigJson, | ||||||
|  |     QuestionableTagRenderingConfigJson, | ||||||
|  | } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||||
|  | import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson" | ||||||
|  | import { TagUtils } from "../src/Logic/Tags/TagUtils" | ||||||
|  | import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||||
|  | import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" | ||||||
|  | 
 | ||||||
|  | export class GenerateFavouritesLayer extends Script { | ||||||
|  |     private readonly layers: LayerConfigJson[] = [] | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("Prepares the 'favourites'-layer") | ||||||
|  |         const allThemes = new AllKnownLayoutsLazy(false).values() | ||||||
|  |         for (const theme of allThemes) { | ||||||
|  |             if (theme.hideFromOverview) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             for (const layer of theme.layers) { | ||||||
|  |                 if (!layer.source) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (layer.source.geojsonSource) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const layerConfig = AllSharedLayers.getSharedLayersConfigs().get(layer.id) | ||||||
|  |                 if (!layerConfig) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 this.layers.push(layerConfig) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private sortMappings(mappings: MappingConfigJson[]): MappingConfigJson[] { | ||||||
|  |         const sortedMappings: MappingConfigJson[] = [...mappings] | ||||||
|  |         sortedMappings.sort((a, b) => { | ||||||
|  |             const aTag = TagUtils.Tag(a.if) | ||||||
|  |             const bTag = TagUtils.Tag(b.if) | ||||||
|  |             const aPop = TagUtils.GetPopularity(aTag) | ||||||
|  |             const bPop = TagUtils.GetPopularity(bTag) | ||||||
|  |             return aPop - bPop | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         return sortedMappings | ||||||
|  |     } | ||||||
|  |     private addTagRenderings(proto: LayerConfigJson) { | ||||||
|  |         const blacklistedIds = new Set([ | ||||||
|  |             "images", | ||||||
|  |             "questions", | ||||||
|  |             "mapillary", | ||||||
|  |             "leftover-questions", | ||||||
|  |             "last_edit", | ||||||
|  |             "minimap", | ||||||
|  |             "move-button", | ||||||
|  |             "delete-button", | ||||||
|  |             "all-tags", | ||||||
|  |             "all_tags", | ||||||
|  |             ...AddEditingElements.addedElements, | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |         const generatedTagRenderings: (string | QuestionableTagRenderingConfigJson)[] = [] | ||||||
|  |         const trPerId = new Map< | ||||||
|  |             string, | ||||||
|  |             { conditions: TagConfigJson[]; tr: QuestionableTagRenderingConfigJson } | ||||||
|  |         >() | ||||||
|  |         for (const layerConfig of this.layers) { | ||||||
|  |             if (!layerConfig.tagRenderings) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             for (const tagRendering of layerConfig.tagRenderings) { | ||||||
|  |                 if (typeof tagRendering === "string") { | ||||||
|  |                     if (blacklistedIds.has(tagRendering)) { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     generatedTagRenderings.push(tagRendering) | ||||||
|  |                     blacklistedIds.add(tagRendering) | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (tagRendering["builtin"]) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const id = tagRendering.id | ||||||
|  |                 if (blacklistedIds.has(id)) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (trPerId.has(id)) { | ||||||
|  |                     const old = trPerId.get(id).tr | ||||||
|  | 
 | ||||||
|  |                     // We need to figure out if this was a 'recycled' tag rendering or just happens to have the same id
 | ||||||
|  |                     function isSame(fieldName: string) { | ||||||
|  |                         return old[fieldName]?.["en"] === tagRendering[fieldName]?.["en"] | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     const sameQuestion = isSame("question") && isSame("render") | ||||||
|  |                     if (!sameQuestion) { | ||||||
|  |                         const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering) | ||||||
|  |                         newTr.id = layerConfig.id + "_" + newTr.id | ||||||
|  |                         if (blacklistedIds.has(newTr.id)) { | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         newTr.condition = { | ||||||
|  |                             and: Utils.NoNull([newTr.condition, layerConfig.source["osmTags"]]), | ||||||
|  |                         } | ||||||
|  |                         generatedTagRenderings.push(newTr) | ||||||
|  |                         blacklistedIds.add(newTr.id) | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (!trPerId.has(id)) { | ||||||
|  |                     const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering) | ||||||
|  |                     generatedTagRenderings.push(newTr) | ||||||
|  |                     trPerId.set(newTr.id, { tr: newTr, conditions: [] }) | ||||||
|  |                 } | ||||||
|  |                 const conditions = trPerId.get(id).conditions | ||||||
|  |                 if (tagRendering["condition"]) { | ||||||
|  |                     conditions.push({ | ||||||
|  |                         and: [tagRendering["condition"], layerConfig.source["osmTags"]], | ||||||
|  |                     }) | ||||||
|  |                 } else { | ||||||
|  |                     conditions.push(layerConfig.source["osmTags"]) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (const { tr, conditions } of Array.from(trPerId.values())) { | ||||||
|  |             const optimized = TagUtils.optimzeJson({ or: conditions }) | ||||||
|  |             if (optimized === true) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if (optimized === false) { | ||||||
|  |                 throw "Optimized into 'false', this is weird..." | ||||||
|  |             } | ||||||
|  |             tr.condition = optimized | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const allTags: QuestionableTagRenderingConfigJson = { | ||||||
|  |             id: "all-tags", | ||||||
|  |             render: { "*": "{all_tags()}" }, | ||||||
|  | 
 | ||||||
|  |             metacondition: { | ||||||
|  |                 or: [ | ||||||
|  |                     "__featureSwitchIsDebugging=true", | ||||||
|  |                     "mapcomplete-show_tags=full", | ||||||
|  |                     "mapcomplete-show_debug=yes", | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         proto.tagRenderings = [ | ||||||
|  |             "images", | ||||||
|  |             ...generatedTagRenderings, | ||||||
|  |             ...proto.tagRenderings, | ||||||
|  |             "questions", | ||||||
|  |             allTags, | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private addTitleIcons(proto: LayerConfigJson) { | ||||||
|  |         proto.titleIcons = [] | ||||||
|  |         const seenTitleIcons = new Set<string>() | ||||||
|  |         for (const layer of this.layers) { | ||||||
|  |             for (const titleIcon of layer.titleIcons) { | ||||||
|  |                 if (typeof titleIcon === "string") { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (titleIcon["labels"]?.indexOf("defaults") >= 0) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (titleIcon.id === "rating") { | ||||||
|  |                     if (!seenTitleIcons.has("rating")) { | ||||||
|  |                         proto.titleIcons.unshift("icons.rating") | ||||||
|  |                         seenTitleIcons.add("rating") | ||||||
|  |                     } | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if (seenTitleIcons.has(titleIcon.id)) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 seenTitleIcons.add(titleIcon.id) | ||||||
|  |                 console.log("Adding ", titleIcon.id) | ||||||
|  |                 proto.titleIcons.push(titleIcon) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         proto.titleIcons.push("icons.defaults") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private addTitle(proto: LayerConfigJson) { | ||||||
|  |         let mappings: MappingConfigJson[] = [] | ||||||
|  |         for (const layer of this.layers) { | ||||||
|  |             const t = layer.title | ||||||
|  |             const tags: TagConfigJson = layer.source["osmTags"] | ||||||
|  |             if (!t) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if (typeof t === "string") { | ||||||
|  |                 mappings.push({ if: tags, then: t }) | ||||||
|  |             } else if (t["render"] !== undefined || t["mappings"] !== undefined) { | ||||||
|  |                 const tr = <TagRenderingConfigJson>t | ||||||
|  |                 for (let i = 0; i < (tr.mappings ?? []).length; i++) { | ||||||
|  |                     const mapping = tr.mappings[i] | ||||||
|  |                     const optimized = TagUtils.optimzeJson({ | ||||||
|  |                         and: [mapping.if, tags], | ||||||
|  |                     }) | ||||||
|  |                     if (optimized === false) { | ||||||
|  |                         console.warn( | ||||||
|  |                             "The following tags yielded 'false':", | ||||||
|  |                             JSON.stringify(mapping.if), | ||||||
|  |                             JSON.stringify(tags) | ||||||
|  |                         ) | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     if (optimized === true) { | ||||||
|  |                         console.error( | ||||||
|  |                             "The following tags yielded 'false':", | ||||||
|  |                             JSON.stringify(mapping.if), | ||||||
|  |                             JSON.stringify(tags) | ||||||
|  |                         ) | ||||||
|  |                         throw "Tags for title optimized to true" | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (!mapping.then) { | ||||||
|  |                         throw ( | ||||||
|  |                             "The title has a missing 'then' for mapping " + | ||||||
|  |                             i + | ||||||
|  |                             " in layer " + | ||||||
|  |                             layer.id | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                     mappings.push({ | ||||||
|  |                         if: optimized, | ||||||
|  |                         then: mapping.then, | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |                 if (tr.render) { | ||||||
|  |                     mappings.push({ | ||||||
|  |                         if: tags, | ||||||
|  |                         then: <Translatable>tr.render, | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 mappings.push({ if: tags, then: <Record<string, string>>t }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mappings = this.sortMappings(mappings) | ||||||
|  | 
 | ||||||
|  |         if (proto.title["mappings"]) { | ||||||
|  |             mappings.unshift(...proto.title["mappings"]) | ||||||
|  |         } | ||||||
|  |         if (proto.title["render"]) { | ||||||
|  |             mappings.push({ | ||||||
|  |                 if: "id~*", | ||||||
|  |                 then: proto.title["render"], | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (const mapping of mappings) { | ||||||
|  |             const opt = TagUtils.optimzeJson(mapping.if) | ||||||
|  |             if (typeof opt === "boolean") { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             mapping.if = opt | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         proto.title = { | ||||||
|  |             mappings, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async main(args: string[]): Promise<void> { | ||||||
|  |         console.log("Generating the favourite layer: stealing _all_ tagRenderings") | ||||||
|  |         const proto = this.readLayer("favourite/favourite.proto.json") | ||||||
|  |         this.addTagRenderings(proto) | ||||||
|  |         this.addTitle(proto) | ||||||
|  |         this.addTitleIcons(proto) | ||||||
|  |         const targetContent = JSON.stringify(proto, null, "  ") | ||||||
|  |         const path = "./assets/layers/favourite/favourite.json" | ||||||
|  |         if (existsSync(path)) { | ||||||
|  |             if (readFileSync(path, "utf8") === targetContent) { | ||||||
|  |                 return // No need to actually write the file, it is identical
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         writeFileSync(path, targetContent) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private readLayer(path: string): LayerConfigJson { | ||||||
|  |         try { | ||||||
|  |             return JSON.parse(readFileSync("./assets/layers/" + path, "utf8")) | ||||||
|  |         } catch (e) { | ||||||
|  |             console.error("Could not read ./assets/layers/" + path) | ||||||
|  |             throw e | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | new GenerateFavouritesLayer().run() | ||||||
|  | @ -27,7 +27,8 @@ function genImages(dryrun = false) { | ||||||
|         "star_outline", |         "star_outline", | ||||||
|         "star", |         "star", | ||||||
|         "osm_logo_us", |         "osm_logo_us", | ||||||
| 
 |         "triangle", | ||||||
|  |         "teardrop_with_hole_green", | ||||||
|         "SocialImageForeground", |         "SocialImageForeground", | ||||||
|         "wikipedia", |         "wikipedia", | ||||||
|         "Upload", |         "Upload", | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js | ||||||
| import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" | import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" | ||||||
| import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" | import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" | ||||||
| import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" | ||||||
|  | import { GenerateFavouritesLayer } from "./generateFavouritesLayer" | ||||||
| 
 | 
 | ||||||
| // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
 | // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
 | ||||||
| // It spits out an overview of those to be used to load them
 | // It spits out an overview of those to be used to load them
 | ||||||
|  | @ -381,16 +382,11 @@ class LayerOverviewUtils extends Script { | ||||||
|             forceReload |             forceReload | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         writeFileSync( |  | ||||||
|             "./src/assets/generated/known_themes.json", |  | ||||||
|             JSON.stringify({ |  | ||||||
|                 themes: Array.from(sharedThemes.values()), |  | ||||||
|             }) |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         writeFileSync( |         writeFileSync( | ||||||
|             "./src/assets/generated/known_layers.json", |             "./src/assets/generated/known_layers.json", | ||||||
|             JSON.stringify({ layers: Array.from(sharedLayers.values()) }) |             JSON.stringify({ | ||||||
|  |                 layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"), | ||||||
|  |             }) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json" |         const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json" | ||||||
|  | @ -428,6 +424,19 @@ class LayerOverviewUtils extends Script { | ||||||
|             ConversionContext.construct([], []) |             ConversionContext.construct([], []) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |         for (const [_, theme] of sharedThemes) { | ||||||
|  |             theme.layers = theme.layers.filter( | ||||||
|  |                 (l) => Constants.added_by_default.indexOf(l["id"]) < 0 | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         writeFileSync( | ||||||
|  |             "./src/assets/generated/known_themes.json", | ||||||
|  |             JSON.stringify({ | ||||||
|  |                 themes: Array.from(sharedThemes.values()), | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         const end = new Date() |         const end = new Date() | ||||||
|         const millisNeeded = end.getTime() - start.getTime() |         const millisNeeded = end.getTime() - start.getTime() | ||||||
|         if (AllSharedLayers.getSharedLayersConfigs().size == 0) { |         if (AllSharedLayers.getSharedLayersConfigs().size == 0) { | ||||||
|  | @ -791,4 +800,5 @@ class LayerOverviewUtils extends Script { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | new GenerateFavouritesLayer().run() | ||||||
| new LayerOverviewUtils().run() | new LayerOverviewUtils().run() | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils" | ||||||
| import { Utils } from "../src/Utils" | import { Utils } from "../src/Utils" | ||||||
| import { writeFileSync } from "fs" | import { writeFileSync } from "fs" | ||||||
| import ScriptUtils from "./ScriptUtils" | import ScriptUtils from "./ScriptUtils" | ||||||
|  | import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig" | ||||||
|  | import { And } from "../src/Logic/Tags/And" | ||||||
| 
 | 
 | ||||||
| /* Downloads stats on osmSource-tags and keys from tagInfo */ | /* Downloads stats on osmSource-tags and keys from tagInfo */ | ||||||
| 
 | 
 | ||||||
|  | @ -21,7 +23,12 @@ async function main(includeTags = true) { | ||||||
|             continue |             continue | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const sources = TagUtils.Tag(layer.source["osmTags"]) |         const sourcesList = [TagUtils.Tag(layer.source["osmTags"])] | ||||||
|  |         if (layer?.title) { | ||||||
|  |             sourcesList.push(...new TagRenderingConfig(layer.title).usedTags()) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const sources = new And(sourcesList) | ||||||
|         const allKeys = sources.usedKeys() |         const allKeys = sources.usedKeys() | ||||||
|         for (const key of allKeys) { |         for (const key of allKeys) { | ||||||
|             if (!keysAndTags.has(key)) { |             if (!keysAndTags.has(key)) { | ||||||
|  | @ -68,6 +75,8 @@ async function main(includeTags = true) { | ||||||
|         "./src/assets/key_totals.json", |         "./src/assets/key_totals.json", | ||||||
|         JSON.stringify( |         JSON.stringify( | ||||||
|             { |             { | ||||||
|  |                 "#": "Generated with generateStats.ts", | ||||||
|  |                 date: new Date().toISOString(), | ||||||
|                 keys: Utils.MapToObj(keyTotal, (t) => t), |                 keys: Utils.MapToObj(keyTotal, (t) => t), | ||||||
|                 tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)), |                 tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)), | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -1,45 +1,54 @@ | ||||||
| import known_themes from "../assets/generated/known_themes.json" | import known_themes from "../assets/generated/known_themes.json" | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import favourite from "../assets/generated/layers/favourite.json" | ||||||
| import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" | import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" | ||||||
|  | import { AllSharedLayers } from "./AllSharedLayers" | ||||||
|  | import Constants from "../Models/Constants" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Somewhat of a dictionary, which lazily parses needed themes |  * Somewhat of a dictionary, which lazily parses needed themes | ||||||
|  */ |  */ | ||||||
| export class AllKnownLayoutsLazy { | export class AllKnownLayoutsLazy { | ||||||
|     private readonly dict: Map<string, { data: LayoutConfig } | { func: () => LayoutConfig }> = |     private readonly raw: Map<string, LayoutConfigJson> = new Map() | ||||||
|         new Map() |     private readonly dict: Map<string, LayoutConfig> = new Map() | ||||||
|     constructor() { | 
 | ||||||
|  |     constructor(includeFavouriteLayer = true) { | ||||||
|         for (const layoutConfigJson of known_themes["themes"]) { |         for (const layoutConfigJson of known_themes["themes"]) { | ||||||
|             this.dict.set(layoutConfigJson.id, { |             for (const layerId of Constants.added_by_default) { | ||||||
|                 func: () => { |                 if (layerId === "favourite" && favourite.id) { | ||||||
|                     const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) |                     if (includeFavouriteLayer) { | ||||||
|                     for (let i = 0; i < layout.layers.length; i++) { |                         layoutConfigJson.layers.push(favourite) | ||||||
|                         let layer = layout.layers[i] |  | ||||||
|                         if (typeof layer === "string") { |  | ||||||
|                             throw "Layer " + layer + " was not expanded in " + layout.id |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                     return layout |                     continue | ||||||
|                 }, |                 } | ||||||
|             }) |                 const defaultLayer = AllSharedLayers.getSharedLayersConfigs().get(layerId) | ||||||
|  |                 if (defaultLayer === undefined) { | ||||||
|  |                     console.error("Could not find builtin layer", layerId) | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 layoutConfigJson.layers.push(defaultLayer) | ||||||
|  |             } | ||||||
|  |             this.raw.set(layoutConfigJson.id, layoutConfigJson) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public getConfig(key: string): LayoutConfigJson { | ||||||
|  |         return this.raw.get(key) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public get(key: string): LayoutConfig { |     public get(key: string): LayoutConfig { | ||||||
|         const thunk = this.dict.get(key) |         const cached = this.dict.get(key) | ||||||
|         if (thunk === undefined) { |         if (cached !== undefined) { | ||||||
|             return undefined |             return cached | ||||||
|         } |         } | ||||||
|         if (thunk["data"]) { | 
 | ||||||
|             return thunk["data"] |         const layout = new LayoutConfig(this.getConfig(key)) | ||||||
|         } |         this.dict.set(key, layout) | ||||||
|         const layout = thunk["func"]() |  | ||||||
|         this.dict.set(key, { data: layout }) |  | ||||||
|         return layout |         return layout | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public keys() { |     public keys() { | ||||||
|         return this.dict.keys() |         return this.raw.keys() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public values() { |     public values() { | ||||||
|  |  | ||||||
|  | @ -6,13 +6,21 @@ import { Changes } from "../Osm/Changes" | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" |  | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" | import { OsmTags } from "../../Models/OsmFeature" | ||||||
| import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | ||||||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
|  | interface TagsUpdaterState { | ||||||
|  |     selectedElement: UIEventSource<Feature> | ||||||
|  |     featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> } | ||||||
|  |     changes: Changes | ||||||
|  |     osmConnection: OsmConnection | ||||||
|  |     layout: LayoutConfig | ||||||
|  |     osmObjectDownloader: OsmObjectDownloader | ||||||
|  |     indexedFeatures: IndexedFeatureSource | ||||||
|  | } | ||||||
| export default class SelectedElementTagsUpdater { | export default class SelectedElementTagsUpdater { | ||||||
|     private static readonly metatags = new Set([ |     private static readonly metatags = new Set([ | ||||||
|         "timestamp", |         "timestamp", | ||||||
|  | @ -23,38 +31,18 @@ export default class SelectedElementTagsUpdater { | ||||||
|         "id", |         "id", | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
|     private readonly state: { |     constructor(state: TagsUpdaterState) { | ||||||
|         selectedElement: UIEventSource<Feature> |  | ||||||
|         featureProperties: FeaturePropertiesStore |  | ||||||
|         changes: Changes |  | ||||||
|         osmConnection: OsmConnection |  | ||||||
|         layout: LayoutConfig |  | ||||||
|         osmObjectDownloader: OsmObjectDownloader |  | ||||||
|         indexedFeatures: IndexedFeatureSource |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     constructor(state: { |  | ||||||
|         selectedElement: UIEventSource<Feature> |  | ||||||
|         featureProperties: FeaturePropertiesStore |  | ||||||
|         indexedFeatures: IndexedFeatureSource |  | ||||||
|         changes: Changes |  | ||||||
|         osmConnection: OsmConnection |  | ||||||
|         layout: LayoutConfig |  | ||||||
|         osmObjectDownloader: OsmObjectDownloader |  | ||||||
|     }) { |  | ||||||
|         this.state = state |  | ||||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { |         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||||
|             if (!isLoggedIn && !Utils.runningFromConsole) { |             if (!isLoggedIn && !Utils.runningFromConsole) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             this.installCallback() |             this.installCallback(state) | ||||||
|             // We only have to do this once...
 |             // We only have to do this once...
 | ||||||
|             return true |             return true | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private installCallback() { |     private installCallback(state: TagsUpdaterState) { | ||||||
|         const state = this.state |  | ||||||
|         state.selectedElement.addCallbackAndRunD(async (s) => { |         state.selectedElement.addCallbackAndRunD(async (s) => { | ||||||
|             let id = s.properties?.id |             let id = s.properties?.id | ||||||
|             if (!id) { |             if (!id) { | ||||||
|  | @ -94,7 +82,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|                     oldFeature.geometry = newGeometry |                     oldFeature.geometry = newGeometry | ||||||
|                     state.featureProperties.getStore(id)?.ping() |                     state.featureProperties.getStore(id)?.ping() | ||||||
|                 } |                 } | ||||||
|                 this.applyUpdate(latestTags, id) |                 SelectedElementTagsUpdater.applyUpdate(latestTags, id, state) | ||||||
| 
 | 
 | ||||||
|                 console.log("Updated", id) |                 console.log("Updated", id) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|  | @ -102,8 +90,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|     private applyUpdate(latestTags: OsmTags, id: string) { |     public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) { | ||||||
|         const state = this.state |  | ||||||
|         try { |         try { | ||||||
|             const leftRightSensitive = state.layout.isLeftRightSensitive() |             const leftRightSensitive = state.layout.isLeftRightSensitive() | ||||||
| 
 | 
 | ||||||
|  | @ -162,11 +149,16 @@ export default class SelectedElementTagsUpdater { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (somethingChanged) { |             if (somethingChanged) { | ||||||
|                 console.log("Detected upstream changes to the object when opening it, updating...") |                 console.log( | ||||||
|  |                     "Detected upstream changes to the object " + | ||||||
|  |                         id + | ||||||
|  |                         " when opening it, updating..." | ||||||
|  |                 ) | ||||||
|                 currentTagsSource.ping() |                 currentTagsSource.ping() | ||||||
|             } else { |             } else { | ||||||
|                 console.debug("Fetched latest tags for ", id, "but detected no changes") |                 console.debug("Fetched latest tags for ", id, "but detected no changes") | ||||||
|             } |             } | ||||||
|  |             return currentTags | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Updating the tags of selected element ", id, "failed due to", e) |             console.error("Updating the tags of selected element ", id, "failed due to", e) | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										220
									
								
								src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,220 @@ | ||||||
|  | import StaticFeatureSource from "./StaticFeatureSource" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import { Store, Stores, UIEventSource } from "../../UIEventSource" | ||||||
|  | import { OsmConnection } from "../../Osm/OsmConnection" | ||||||
|  | import { OsmId } from "../../../Models/OsmFeature" | ||||||
|  | import { GeoOperations } from "../../GeoOperations" | ||||||
|  | import { IndexedFeatureSource } from "../FeatureSource" | ||||||
|  | import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" | ||||||
|  | import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" | ||||||
|  | import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater" | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Generates the favourites from the preferences and marks them as favourite | ||||||
|  |  */ | ||||||
|  | export default class FavouritesFeatureSource extends StaticFeatureSource { | ||||||
|  |     public static readonly prefix = "mapcomplete-favourite-" | ||||||
|  |     private readonly _osmConnection: OsmConnection | ||||||
|  |     private readonly _detectedIds: Store<string[]> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * All favourites, including the ones which are filtered away because they are already displayed | ||||||
|  |      */ | ||||||
|  |     public readonly allFavourites: Store<Feature[]> | ||||||
|  | 
 | ||||||
|  |     constructor(state: SpecialVisualizationState) { | ||||||
|  |         const features: Store<Feature[]> = Stores.ListStabilized( | ||||||
|  |             state.osmConnection.preferencesHandler.preferences.map((prefs) => { | ||||||
|  |                 const feats: Feature[] = [] | ||||||
|  |                 const allIds = new Set<string>() | ||||||
|  |                 for (const key in prefs) { | ||||||
|  |                     if (!key.startsWith(FavouritesFeatureSource.prefix)) { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     try { | ||||||
|  |                         const feat = FavouritesFeatureSource.ExtractFavourite(key, prefs) | ||||||
|  |                         if (!feat) { | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         feats.push(feat) | ||||||
|  |                         allIds.add(feat.properties.id) | ||||||
|  |                     } catch (e) { | ||||||
|  |                         console.error("Could not create favourite from", key, "due to", e) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 return feats | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         const featuresWithoutAlreadyPresent = features.map((features) => | ||||||
|  |             features.filter( | ||||||
|  |                 (feat) => !state.layout.layers.some((l) => l.id === feat.properties._orig_layer) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         super(featuresWithoutAlreadyPresent) | ||||||
|  |         this.allFavourites = features | ||||||
|  | 
 | ||||||
|  |         this._osmConnection = state.osmConnection | ||||||
|  |         this._detectedIds = Stores.ListStabilized( | ||||||
|  |             features.map((feats) => feats.map((f) => f.properties.id)) | ||||||
|  |         ) | ||||||
|  |         let allFeatures = state.indexedFeatures | ||||||
|  |         this._detectedIds.addCallbackAndRunD((detected) => | ||||||
|  |             this.markFeatures(detected, state.featureProperties, allFeatures) | ||||||
|  |         ) | ||||||
|  |         // We use the indexedFeatureSource as signal to update
 | ||||||
|  |         allFeatures.features.map((_) => | ||||||
|  |             this.markFeatures(this._detectedIds.data, state.featureProperties, allFeatures) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.allFavourites.addCallbackD((features) => { | ||||||
|  |             for (const feature of features) { | ||||||
|  |                 this.updateFeature(feature, state.osmObjectDownloader, state) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private async updateFeature( | ||||||
|  |         feature: Feature, | ||||||
|  |         osmObjectDownloader: OsmObjectDownloader, | ||||||
|  |         state: SpecialVisualizationState | ||||||
|  |     ) { | ||||||
|  |         const id = feature.properties.id | ||||||
|  |         const upstream = await osmObjectDownloader.DownloadObjectAsync(id) | ||||||
|  |         if (upstream === "deleted") { | ||||||
|  |             this.removeFavourite(feature) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         console.log("Updating metadata due to favourite of", id) | ||||||
|  |         const latestTags = SelectedElementTagsUpdater.applyUpdate(upstream.tags, id, state) | ||||||
|  |         this.updatePropertiesOfFavourite(latestTags) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static ExtractFavourite(key: string, prefs: Record<string, string>): Feature { | ||||||
|  |         const id = key.substring(FavouritesFeatureSource.prefix.length) | ||||||
|  |         const osmId = id.replace("-", "/") | ||||||
|  |         if (id.indexOf("-property-") > 0 || id.endsWith("-layer") || id.endsWith("-theme")) { | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|  |         const geometry = <[number, number]>JSON.parse(prefs[key]) | ||||||
|  |         const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id) | ||||||
|  |         properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"] | ||||||
|  |         properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"] | ||||||
|  | 
 | ||||||
|  |         properties.id = osmId | ||||||
|  |         properties._favourite = "yes" | ||||||
|  |         return { | ||||||
|  |             type: "Feature", | ||||||
|  |             properties, | ||||||
|  |             geometry: { | ||||||
|  |                 type: "Point", | ||||||
|  |                 coordinates: geometry, | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static getPropertiesFor( | ||||||
|  |         prefs: Record<string, string>, | ||||||
|  |         id: string | ||||||
|  |     ): Record<string, string> { | ||||||
|  |         const properties: Record<string, string> = {} | ||||||
|  |         const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length | ||||||
|  |         for (const key in prefs) { | ||||||
|  |             if (key.length < minLength) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if (!key.startsWith(FavouritesFeatureSource.prefix + id)) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             const propertyName = key.substring(minLength).replaceAll("__", ":") | ||||||
|  |             properties[propertyName] = prefs[key] | ||||||
|  |         } | ||||||
|  |         return properties | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets all the (normal) properties as the feature is updated | ||||||
|  |      */ | ||||||
|  |     private updatePropertiesOfFavourite(properties: Record<string, string>) { | ||||||
|  |         const id = properties?.id?.replace("/", "-") | ||||||
|  |         if (!id) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         console.log("Updating store for", id) | ||||||
|  |         for (const key in properties) { | ||||||
|  |             const pref = this._osmConnection.GetPreference( | ||||||
|  |                 "favourite-" + id + "-property-" + key.replaceAll(":", "__") | ||||||
|  |             ) | ||||||
|  |             const v = properties[key] | ||||||
|  |             if (v === "" || !v) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             pref.setData("" + v) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public removeFavourite(feature: Feature, tags?: UIEventSource<Record<string, string>>) { | ||||||
|  |         const id = feature.properties.id.replace("/", "-") | ||||||
|  |         const pref = this._osmConnection.GetPreference("favourite-" + id) | ||||||
|  |         this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id) | ||||||
|  |         if (tags) { | ||||||
|  |             delete tags.data._favourite | ||||||
|  |             tags.ping() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public markAsFavourite( | ||||||
|  |         feature: Feature, | ||||||
|  |         layer: string, | ||||||
|  |         theme: string, | ||||||
|  |         tags: UIEventSource<Record<string, string> & { id: OsmId }>, | ||||||
|  |         isFavourite: boolean = true | ||||||
|  |     ) { | ||||||
|  |         { | ||||||
|  |             if (!isFavourite) { | ||||||
|  |                 this.removeFavourite(feature, tags) | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             const id = tags.data.id.replace("/", "-") | ||||||
|  |             const pref = this._osmConnection.GetPreference("favourite-" + id) | ||||||
|  |             const center = GeoOperations.centerpointCoordinates(feature) | ||||||
|  |             pref.setData(JSON.stringify(center)) | ||||||
|  |             this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer) | ||||||
|  |             this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme) | ||||||
|  |             this.updatePropertiesOfFavourite(tags.data) | ||||||
|  |         } | ||||||
|  |         tags.data._favourite = "yes" | ||||||
|  |         tags.ping() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private markFeatures( | ||||||
|  |         detected: string[], | ||||||
|  |         featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }, | ||||||
|  |         allFeatures: IndexedFeatureSource | ||||||
|  |     ) { | ||||||
|  |         const feature = allFeatures.features.data | ||||||
|  |         for (const f of feature) { | ||||||
|  |             const id = f.properties.id | ||||||
|  |             if (!id) { | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             const store = featureProperties.getStore(id) | ||||||
|  |             const origValue = store.data._favourite | ||||||
|  |             if (detected.indexOf(id) >= 0) { | ||||||
|  |                 if (origValue !== "yes") { | ||||||
|  |                     store.data._favourite = "yes" | ||||||
|  |                     store.ping() | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if (origValue) { | ||||||
|  |                     store.data._favourite = "" | ||||||
|  |                     store.ping() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource" | ||||||
| import LayerState from "../../State/LayerState" | import LayerState from "../../State/LayerState" | ||||||
| 
 | 
 | ||||||
| export default class NearbyFeatureSource implements FeatureSource { | export default class NearbyFeatureSource implements FeatureSource { | ||||||
|  |     private readonly _result = new UIEventSource<Feature[]>(undefined) | ||||||
|  | 
 | ||||||
|     public readonly features: Store<Feature[]> |     public readonly features: Store<Feature[]> | ||||||
|     private readonly _targetPoint: Store<{ lon: number; lat: number }> |     private readonly _targetPoint: Store<{ lon: number; lat: number }> | ||||||
|     private readonly _numberOfNeededFeatures: number |     private readonly _numberOfNeededFeatures: number | ||||||
|  |     private readonly _layerState?: LayerState | ||||||
|     private readonly _currentZoom: Store<number> |     private readonly _currentZoom: Store<number> | ||||||
|  |     private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = [] | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         targetPoint: Store<{ lon: number; lat: number }>, |         targetPoint: Store<{ lon: number; lat: number }>, | ||||||
|  | @ -18,43 +22,46 @@ export default class NearbyFeatureSource implements FeatureSource { | ||||||
|         layerState?: LayerState, |         layerState?: LayerState, | ||||||
|         currentZoom?: Store<number> |         currentZoom?: Store<number> | ||||||
|     ) { |     ) { | ||||||
|  |         this._layerState = layerState | ||||||
|         this._targetPoint = targetPoint.stabilized(100) |         this._targetPoint = targetPoint.stabilized(100) | ||||||
|         this._numberOfNeededFeatures = numberOfNeededFeatures |         this._numberOfNeededFeatures = numberOfNeededFeatures | ||||||
|         this._currentZoom = currentZoom.stabilized(500) |         this._currentZoom = currentZoom.stabilized(500) | ||||||
| 
 | 
 | ||||||
|         const allSources: Store<{ feat: Feature; d: number }[]>[] = [] |         this.features = Stores.ListStabilized(this._result) | ||||||
|         let minzoom = 999 |  | ||||||
| 
 |  | ||||||
|         const result = new UIEventSource<Feature[]>(undefined) |  | ||||||
|         this.features = Stores.ListStabilized(result) |  | ||||||
| 
 |  | ||||||
|         function update() { |  | ||||||
|             let features: { feat: Feature; d: number }[] = [] |  | ||||||
|             for (const src of allSources) { |  | ||||||
|                 features.push(...src.data) |  | ||||||
|             } |  | ||||||
|             features.sort((a, b) => a.d - b.d) |  | ||||||
|             if (numberOfNeededFeatures !== undefined) { |  | ||||||
|                 features = features.slice(0, numberOfNeededFeatures) |  | ||||||
|             } |  | ||||||
|             result.setData(features.map((f) => f.feat)) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         sources.forEach((source, layer) => { |         sources.forEach((source, layer) => { | ||||||
|             const flayer = layerState?.filteredLayers.get(layer) |             this.registerSource(source, layer) | ||||||
|             minzoom = Math.min(minzoom, flayer.layerDef.minzoom) |  | ||||||
|             const calcSource = this.createSource( |  | ||||||
|                 source.features, |  | ||||||
|                 flayer.layerDef.minzoom, |  | ||||||
|                 flayer.isDisplayed |  | ||||||
|             ) |  | ||||||
|             calcSource.addCallbackAndRunD((features) => { |  | ||||||
|                 update() |  | ||||||
|             }) |  | ||||||
|             allSources.push(calcSource) |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public registerSource(source: FeatureSource, layerId: string) { | ||||||
|  |         const flayer = this._layerState?.filteredLayers.get(layerId) | ||||||
|  |         if (!flayer) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         const calcSource = this.createSource( | ||||||
|  |             source.features, | ||||||
|  |             flayer.layerDef.minzoom, | ||||||
|  |             flayer.isDisplayed | ||||||
|  |         ) | ||||||
|  |         calcSource.addCallbackAndRunD((features) => { | ||||||
|  |             this.update() | ||||||
|  |         }) | ||||||
|  |         this._allSources.push(calcSource) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private update() { | ||||||
|  |         let features: { feat: Feature; d: number }[] = [] | ||||||
|  |         for (const src of this._allSources) { | ||||||
|  |             features.push(...src.data) | ||||||
|  |         } | ||||||
|  |         features.sort((a, b) => a.d - b.d) | ||||||
|  |         if (this._numberOfNeededFeatures !== undefined) { | ||||||
|  |             features = features.slice(0, this._numberOfNeededFeatures) | ||||||
|  |         } | ||||||
|  |         this._result.setData(features.map((f) => f.feat)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Sorts the given source by distance, slices down to the required number |      * Sorts the given source by distance, slices down to the required number | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -501,147 +501,43 @@ export class GeoOperations { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static IdentifieCommonSegments(coordinatess: [number, number][][]): { |     /** | ||||||
|         originalIndex: number |      * Given a list of points, convert into a GPX-list, e.g. for favourites | ||||||
|         segmentShardWith: number[] |      * @param locations | ||||||
|         coordinates: [] |      * @param title | ||||||
|     }[] { |      */ | ||||||
|         // An edge. Note that the edge might be reversed to fix the sorting condition:  start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
 |     public static toGpxPoints( | ||||||
|         type edge = { |         locations: Feature<Point, { date?: string; altitude?: number | string }>[], | ||||||
|             start: [number, number] |         title?: string | ||||||
|             end: [number, number] |     ) { | ||||||
|             intermediate: [number, number][] |         title = title?.trim() | ||||||
|             members: { index: number; isReversed: boolean }[] |         if (title === undefined || title === "") { | ||||||
|  |             title = "Created with MapComplete" | ||||||
|         } |         } | ||||||
| 
 |         title = Utils.EncodeXmlValue(title) | ||||||
|         // The strategy:
 |         const trackPoints: string[] = [] | ||||||
|         // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
 |         for (const l of locations) { | ||||||
|         // 2. Join these edges back together - as long as their membership groups are the same
 |             let trkpt = `    <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">` | ||||||
|         // 3. Convert to results
 |             for (const key in l.properties) { | ||||||
| 
 |                 const keyCleaned = key.replaceAll(":", "__") | ||||||
|         const allEdgesByKey = new Map<string, edge>() |                 trkpt += `        <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n` | ||||||
| 
 |                 if (key === "website") { | ||||||
|         for (let index = 0; index < coordinatess.length; index++) { |                     trkpt += `        <link>${l.properties[key]}</link>\n` | ||||||
|             const coordinates = coordinatess[index] |  | ||||||
|             for (let i = 0; i < coordinates.length - 1; i++) { |  | ||||||
|                 const c0 = coordinates[i] |  | ||||||
|                 const c1 = coordinates[i + 1] |  | ||||||
|                 const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1]) |  | ||||||
| 
 |  | ||||||
|                 let key: string |  | ||||||
|                 if (isReversed) { |  | ||||||
|                     key = "" + c1 + ";" + c0 |  | ||||||
|                 } else { |  | ||||||
|                     key = "" + c0 + ";" + c1 |  | ||||||
|                 } |                 } | ||||||
|                 const member = { index, isReversed } |  | ||||||
|                 if (allEdgesByKey.has(key)) { |  | ||||||
|                     allEdgesByKey.get(key).members.push(member) |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 let edge: edge |  | ||||||
|                 if (!isReversed) { |  | ||||||
|                     edge = { |  | ||||||
|                         start: c0, |  | ||||||
|                         end: c1, |  | ||||||
|                         members: [member], |  | ||||||
|                         intermediate: [], |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     edge = { |  | ||||||
|                         start: c1, |  | ||||||
|                         end: c0, |  | ||||||
|                         members: [member], |  | ||||||
|                         intermediate: [], |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 allEdgesByKey.set(key, edge) |  | ||||||
|             } |             } | ||||||
|  |             trkpt += "    </wpt>\n" | ||||||
|  |             trackPoints.push(trkpt) | ||||||
|         } |         } | ||||||
| 
 |         const header = | ||||||
|         // Lets merge them back together!
 |             '<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">' | ||||||
| 
 |         return ( | ||||||
|         let didMergeSomething = false |             header + | ||||||
|         let allMergedEdges = Array.from(allEdgesByKey.values()) |             "\n<name>" + | ||||||
|         const allEdgesByStartPoint = new Map<string, edge[]>() |             title + | ||||||
|         for (const edge of allMergedEdges) { |             "</name>\n<trk><trkseg>\n" + | ||||||
|             edge.members.sort((m0, m1) => m0.index - m1.index) |             trackPoints.join("\n") + | ||||||
| 
 |             "\n</trkseg></trk></gpx>" | ||||||
|             const kstart = edge.start + "" |         ) | ||||||
|             if (!allEdgesByStartPoint.has(kstart)) { |  | ||||||
|                 allEdgesByStartPoint.set(kstart, []) |  | ||||||
|             } |  | ||||||
|             allEdgesByStartPoint.get(kstart).push(edge) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function membersAreCompatible(first: edge, second: edge): boolean { |  | ||||||
|             // There must be an exact match between the members
 |  | ||||||
|             if (first.members === second.members) { |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (first.members.length !== second.members.length) { |  | ||||||
|                 return false |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Members are sorted and have the same length, so we can check quickly
 |  | ||||||
|             for (let i = 0; i < first.members.length; i++) { |  | ||||||
|                 const m0 = first.members[i] |  | ||||||
|                 const m1 = second.members[i] |  | ||||||
|                 if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) { |  | ||||||
|                     return false |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Allrigth, they are the same, lets mark this permanently
 |  | ||||||
|             second.members = first.members |  | ||||||
|             return true |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         do { |  | ||||||
|             didMergeSomething = false |  | ||||||
|             // We use 'allMergedEdges' as our running list
 |  | ||||||
|             const consumed = new Set<edge>() |  | ||||||
|             for (const edge of allMergedEdges) { |  | ||||||
|                 // Can we make this edge longer at the end?
 |  | ||||||
|                 if (consumed.has(edge)) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 console.log("Considering edge", edge) |  | ||||||
|                 const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "") |  | ||||||
|                 console.log("Matchign endpoints:", matchingEndEdges) |  | ||||||
|                 if (matchingEndEdges === undefined) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 for (let i = 0; i < matchingEndEdges.length; i++) { |  | ||||||
|                     const endEdge = matchingEndEdges[i] |  | ||||||
| 
 |  | ||||||
|                     if (consumed.has(endEdge)) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (!membersAreCompatible(edge, endEdge)) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     // We can make the segment longer!
 |  | ||||||
|                     didMergeSomething = true |  | ||||||
|                     console.log("Merging ", edge, "with ", endEdge) |  | ||||||
|                     edge.intermediate.push(edge.end) |  | ||||||
|                     edge.end = endEdge.end |  | ||||||
|                     consumed.add(endEdge) |  | ||||||
|                     matchingEndEdges.splice(i, 1) |  | ||||||
|                     break |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge)) |  | ||||||
|         } while (didMergeSomething) |  | ||||||
| 
 |  | ||||||
|         return [] |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -107,7 +107,8 @@ export class ImageUploadManager { | ||||||
|             title, |             title, | ||||||
|             description, |             description, | ||||||
|             file, |             file, | ||||||
|             targetKey |             targetKey, | ||||||
|  |             tags.data["_orig_theme"] | ||||||
|         ) |         ) | ||||||
|         if (!isNaN(Number(featureId))) { |         if (!isNaN(Number(featureId))) { | ||||||
|             // This is a map note
 |             // This is a map note
 | ||||||
|  | @ -126,7 +127,8 @@ export class ImageUploadManager { | ||||||
|         title: string, |         title: string, | ||||||
|         description: string, |         description: string, | ||||||
|         blob: File, |         blob: File, | ||||||
|         targetKey: string | undefined |         targetKey: string | undefined, | ||||||
|  |         theme?: string | ||||||
|     ): Promise<LinkImageAction> { |     ): Promise<LinkImageAction> { | ||||||
|         this.increaseCountFor(this._uploadStarted, featureId) |         this.increaseCountFor(this._uploadStarted, featureId) | ||||||
|         const properties = this._featureProperties.getStore(featureId) |         const properties = this._featureProperties.getStore(featureId) | ||||||
|  | @ -148,7 +150,7 @@ export class ImageUploadManager { | ||||||
|         console.log("Uploading done, creating action for", featureId) |         console.log("Uploading done, creating action for", featureId) | ||||||
|         key = targetKey ?? key |         key = targetKey ?? key | ||||||
|         const action = new LinkImageAction(featureId, key, value, properties, { |         const action = new LinkImageAction(featureId, key, value, properties, { | ||||||
|             theme: this._layout.id, |             theme: theme ?? this._layout.id, | ||||||
|             changeType: "add-image", |             changeType: "add-image", | ||||||
|         }) |         }) | ||||||
|         this.increaseCountFor(this._uploadFinished, featureId) |         this.increaseCountFor(this._uploadFinished, featureId) | ||||||
|  |  | ||||||
|  | @ -12,6 +12,10 @@ export class OsmPreferences { | ||||||
|         "all-osm-preferences", |         "all-osm-preferences", | ||||||
|         {} |         {} | ||||||
|     ) |     ) | ||||||
|  |     /** | ||||||
|  |      * A map containing the individual preference sources | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|     private readonly preferenceSources = new Map<string, UIEventSource<string>>() |     private readonly preferenceSources = new Map<string, UIEventSource<string>>() | ||||||
|     private auth: any |     private auth: any | ||||||
|     private userDetails: UIEventSource<UserDetails> |     private userDetails: UIEventSource<UserDetails> | ||||||
|  | @ -21,7 +25,10 @@ export class OsmPreferences { | ||||||
|         this.auth = auth |         this.auth = auth | ||||||
|         this.userDetails = osmConnection.userDetails |         this.userDetails = osmConnection.userDetails | ||||||
|         const self = this |         const self = this | ||||||
|         osmConnection.OnLoggedIn(() => self.UpdatePreferences()) |         osmConnection.OnLoggedIn(() => { | ||||||
|  |             self.UpdatePreferences(true) | ||||||
|  |             return true | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -72,11 +79,19 @@ export class OsmPreferences { | ||||||
|             let i = 0 |             let i = 0 | ||||||
|             while (str !== "") { |             while (str !== "") { | ||||||
|                 if (str === undefined || str === "undefined") { |                 if (str === undefined || str === "undefined") { | ||||||
|  |                     source.setData(undefined) | ||||||
|                     throw ( |                     throw ( | ||||||
|                         "Got 'undefined' or a literal string containing 'undefined' for a long preference with name " + |                         "Got 'undefined' or a literal string containing 'undefined' for a long preference with name " + | ||||||
|                         key |                         key | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|  |                 if (str === "undefined") { | ||||||
|  |                     source.setData(undefined) | ||||||
|  |                     throw ( | ||||||
|  |                         "Got a literal string containing 'undefined' for a long preference with name " + | ||||||
|  |                         key | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|                 if (i > 100) { |                 if (i > 100) { | ||||||
|                     throw "This long preference is getting very long... " |                     throw "This long preference is getting very long... " | ||||||
|                 } |                 } | ||||||
|  | @ -197,7 +212,7 @@ export class OsmPreferences { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private UpdatePreferences() { |     private UpdatePreferences(forceUpdate?: boolean) { | ||||||
|         const self = this |         const self = this | ||||||
|         this.auth.xhr( |         this.auth.xhr( | ||||||
|             { |             { | ||||||
|  | @ -210,11 +225,22 @@ export class OsmPreferences { | ||||||
|                     return |                     return | ||||||
|                 } |                 } | ||||||
|                 const prefs = value.getElementsByTagName("preference") |                 const prefs = value.getElementsByTagName("preference") | ||||||
|  |                 const seenKeys = new Set<string>() | ||||||
|                 for (let i = 0; i < prefs.length; i++) { |                 for (let i = 0; i < prefs.length; i++) { | ||||||
|                     const pref = prefs[i] |                     const pref = prefs[i] | ||||||
|                     const k = pref.getAttribute("k") |                     const k = pref.getAttribute("k") | ||||||
|                     const v = pref.getAttribute("v") |                     const v = pref.getAttribute("v") | ||||||
|                     self.preferences.data[k] = v |                     self.preferences.data[k] = v | ||||||
|  |                     seenKeys.add(k) | ||||||
|  |                 } | ||||||
|  |                 if (forceUpdate) { | ||||||
|  |                     for (let key in self.preferences.data) { | ||||||
|  |                         if (seenKeys.has(key)) { | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         console.log("Deleting key", key, "as we didn't find it upstream") | ||||||
|  |                         delete self.preferences.data[key] | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // We merge all the preferences: new keys are uploaded
 |                 // We merge all the preferences: new keys are uploaded
 | ||||||
|  | @ -285,4 +311,14 @@ export class OsmPreferences { | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     removeAllWithPrefix(prefix: string) { | ||||||
|  |         for (const key in this.preferences.data) { | ||||||
|  |             if (key.startsWith(prefix)) { | ||||||
|  |                 this.GetPreference(key, "", { prefix: "" }).setData(undefined) | ||||||
|  |                 console.log("Clearing preference", key) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         this.preferences.ping() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -294,6 +294,9 @@ export default class UserRelatedState { | ||||||
|         osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { |         osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { | ||||||
|             for (const k in newPrefs) { |             for (const k in newPrefs) { | ||||||
|                 const v = newPrefs[k] |                 const v = newPrefs[k] | ||||||
|  |                 if (v === "undefined" || !v) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|                 if (k.endsWith("-combined-length")) { |                 if (k.endsWith("-combined-length")) { | ||||||
|                     const l = Number(v) |                     const l = Number(v) | ||||||
|                     const key = k.substring(0, k.length - "length".length) |                     const key = k.substring(0, k.length - "length".length) | ||||||
|  | @ -308,7 +311,6 @@ export default class UserRelatedState { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             amendedPrefs.ping() |             amendedPrefs.ping() | ||||||
|             console.log("Amended prefs are:", amendedPrefs.data) |  | ||||||
|         }) |         }) | ||||||
|         const translationMode = osmConnection.GetPreference("translation-mode") |         const translationMode = osmConnection.GetPreference("translation-mode") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { Or } from "./Or" | ||||||
| import { TagUtils } from "./TagUtils" | import { TagUtils } from "./TagUtils" | ||||||
| import { Tag } from "./Tag" | import { Tag } from "./Tag" | ||||||
| import { RegexTag } from "./RegexTag" | import { RegexTag } from "./RegexTag" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| 
 | 
 | ||||||
| export class And extends TagsFilter { | export class And extends TagsFilter { | ||||||
|     public and: TagsFilter[] |     public and: TagsFilter[] | ||||||
|  | @ -72,6 +73,10 @@ export class And extends TagsFilter { | ||||||
|         return allChoices |         return allChoices | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         return { and: this.and.map((a) => a.asJson()) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { |     asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { | ||||||
|         return this.and |         return this.and | ||||||
|             .map((t) => { |             .map((t) => { | ||||||
|  | @ -228,6 +233,15 @@ export class And extends TagsFilter { | ||||||
|         return And.construct(newAnds) |         return And.construct(newAnds) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * const raw = {"and": [{"or":["leisure=playground","playground!=forest"]},{"or":["leisure=playground","playground!=forest"]}]} | ||||||
|  |      * const parsed = TagUtils.Tag(raw) | ||||||
|  |      * parsed.optimize().asJson() // => {"or":["leisure=playground","playground!=forest"]}
 | ||||||
|  |      * | ||||||
|  |      * const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}] | ||||||
|  |      * const parsed = TagUtils.Tag(raw) | ||||||
|  |      * parsed.optimize().asJson() // => "advertising=screen"
 | ||||||
|  |      */ | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         if (this.and.length === 0) { |         if (this.and.length === 0) { | ||||||
|             return true |             return true | ||||||
|  | @ -289,9 +303,17 @@ export class And extends TagsFilter { | ||||||
|                             optimized.splice(i, 1) |                             optimized.splice(i, 1) | ||||||
|                             i-- |                             i-- | ||||||
|                         } |                         } | ||||||
|                     } else if (v !== opt.value) { |                     } else { | ||||||
|                         // detected an internal conflict
 |                         if (!v.match(opt.value)) { | ||||||
|                         return false |                             // We _know_ that for the key of the RegexTag `opt`, the value will be `v`.
 | ||||||
|  |                             // As such, if `opt.value` cannot match `v`, we detected an internal conflict and can fail
 | ||||||
|  | 
 | ||||||
|  |                             return false | ||||||
|  |                         } else { | ||||||
|  |                             // Another tag already provided a _stricter_ value then this regex, so we can remove this one!
 | ||||||
|  |                             optimized.splice(i, 1) | ||||||
|  |                             i-- | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -369,10 +391,13 @@ export class And extends TagsFilter { | ||||||
|                     const elements = containedOr.or.filter( |                     const elements = containedOr.or.filter( | ||||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) |                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||||
|                     ) |                     ) | ||||||
|                     newOrs.push(Or.construct(elements)) |                     if (elements.length > 0) { | ||||||
|  |                         newOrs.push(Or.construct(elements)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (newOrs.length > 0) { | ||||||
|  |                     commonValues.push(And.construct(newOrs)) | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 commonValues.push(And.construct(newOrs)) |  | ||||||
|                 const result = new Or(commonValues).optimize() |                 const result = new Or(commonValues).optimize() | ||||||
|                 if (result === false) { |                 if (result === false) { | ||||||
|                     return false |                     return false | ||||||
|  |  | ||||||
|  | @ -1,18 +1,23 @@ | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
|  | import { Tag } from "./Tag" | ||||||
| 
 | 
 | ||||||
| export default class ComparingTag implements TagsFilter { | export default class ComparingTag implements TagsFilter { | ||||||
|     private readonly _key: string |     private readonly _key: string | ||||||
|     private readonly _predicate: (value: string) => boolean |     private readonly _predicate: (value: string) => boolean | ||||||
|     private readonly _representation: string |     private readonly _representation: "<" | ">" | "<=" | ">=" | ||||||
|  |     private readonly _boundary: string | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         key: string, |         key: string, | ||||||
|         predicate: (value: string | undefined) => boolean, |         predicate: (value: string | undefined) => boolean, | ||||||
|         representation: string = "" |         representation: "<" | ">" | "<=" | ">=", | ||||||
|  |         boundary: string | ||||||
|     ) { |     ) { | ||||||
|         this._key = key |         this._key = key | ||||||
|         this._predicate = predicate |         this._predicate = predicate | ||||||
|         this._representation = representation |         this._representation = representation | ||||||
|  |         this._boundary = boundary | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asChange(properties: Record<string, string>): { k: string; v: string }[] { |     asChange(properties: Record<string, string>): { k: string; v: string }[] { | ||||||
|  | @ -20,15 +25,64 @@ export default class ComparingTag implements TagsFilter { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { |     asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { | ||||||
|         return this._key + this._representation |         return this._key + this._representation + this._boundary | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         throw "A comparable tag can not be used as overpass filter" |         throw "A comparable tag can not be used as overpass filter" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * const tg = new ComparingTag("key", value => (Number(value) < 42), "<", "42") | ||||||
|  |      * const tg0 = new ComparingTag("key", value => (Number(value) < 42), "<", "42") | ||||||
|  |      * const tg1 = new ComparingTag("key", value => (Number(value) <= 42), "<=", "42") | ||||||
|  |      * const against = new ComparingTag("key", value => (Number(value) > 0), ">", "0") | ||||||
|  |      * tg.shadows(new Tag("key", "41")) // => true
 | ||||||
|  |      * tg.shadows(new Tag("key", "0")) // => true
 | ||||||
|  |      * tg.shadows(new Tag("key", "43")) // => false
 | ||||||
|  |      * tg.shadows(new Tag("key", "0")) // => true
 | ||||||
|  |      * tg.shadows(tg) // => true
 | ||||||
|  |      * tg.shadows(tg0) // => true
 | ||||||
|  |      * tg.shadows(against) // => false
 | ||||||
|  |      * tg1.shadows(tg0) // => true
 | ||||||
|  |      * tg0.shadows(tg1) // => false
 | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|     shadows(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         return other === this |         if (other === this) { | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         if (other instanceof ComparingTag) { | ||||||
|  |             if (other._key !== this._key) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             const selfDesc = this._representation === "<" || this._representation === "<=" | ||||||
|  |             const otherDesc = other._representation === "<" || other._representation === "<=" | ||||||
|  |             if (selfDesc !== otherDesc) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             if ( | ||||||
|  |                 this._boundary === other._boundary && | ||||||
|  |                 this._representation === other._representation | ||||||
|  |             ) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             if (this._predicate(other._boundary)) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (other instanceof Tag) { | ||||||
|  |             if (other.key !== this._key) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             if (this.matchesProperties({ [other.key]: other.value })) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isUsableAsAnswer(): boolean { |     isUsableAsAnswer(): boolean { | ||||||
|  | @ -38,7 +92,7 @@ export default class ComparingTag implements TagsFilter { | ||||||
|     /** |     /** | ||||||
|      * Checks if the properties match |      * Checks if the properties match | ||||||
|      * |      * | ||||||
|      * const t = new ComparingTag("key", (x => Number(x) < 42)) |      * const t = new ComparingTag("key", (x => Number(x) < 42), "<", "42") | ||||||
|      * t.matchesProperties({key: 42}) // => false
 |      * t.matchesProperties({key: 42}) // => false
 | ||||||
|      * t.matchesProperties({key: 41}) // => true
 |      * t.matchesProperties({key: 41}) // => true
 | ||||||
|      * t.matchesProperties({key: 0}) // => true
 |      * t.matchesProperties({key: 0}) // => true
 | ||||||
|  | @ -56,6 +110,10 @@ export default class ComparingTag implements TagsFilter { | ||||||
|         return [] |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         return this._key + this._representation | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         return this |         return this | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
| import { TagUtils } from "./TagUtils" | import { TagUtils } from "./TagUtils" | ||||||
| import { And } from "./And" | import { And } from "./And" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| 
 | 
 | ||||||
| export class Or extends TagsFilter { | export class Or extends TagsFilter { | ||||||
|     public or: TagsFilter[] |     public or: TagsFilter[] | ||||||
|  | @ -27,6 +28,10 @@ export class Or extends TagsFilter { | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         return { or: this.or.map((o) => o.asJson()) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * |      * | ||||||
|      * import {Tag} from "./Tag"; |      * import {Tag} from "./Tag"; | ||||||
|  | @ -157,6 +162,12 @@ export class Or extends TagsFilter { | ||||||
|         return Or.construct(newOrs) |         return Or.construct(newOrs) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * const raw = {"or": [{"and":["leisure=playground","playground!=forest"]},{"and":["leisure=playground","playground!=forest"]}]} | ||||||
|  |      * const parsed = TagUtils.Tag(raw) | ||||||
|  |      * parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
 | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         if (this.or.length === 0) { |         if (this.or.length === 0) { | ||||||
|             return false |             return false | ||||||
|  | @ -174,9 +185,9 @@ export class Or extends TagsFilter { | ||||||
|         const newOrs: TagsFilter[] = [] |         const newOrs: TagsFilter[] = [] | ||||||
|         let containedAnds: And[] = [] |         let containedAnds: And[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if (tf instanceof Or) { |             if (tf["or"]) { | ||||||
|                 // expand all the nested ors...
 |                 // expand all the nested ors...
 | ||||||
|                 newOrs.push(...tf.or) |                 newOrs.push(...tf["or"]) | ||||||
|             } else if (tf instanceof And) { |             } else if (tf instanceof And) { | ||||||
|                 // partition of all the ands
 |                 // partition of all the ands
 | ||||||
|                 containedAnds.push(tf) |                 containedAnds.push(tf) | ||||||
|  | @ -191,7 +202,7 @@ export class Or extends TagsFilter { | ||||||
|                 const cleanedContainedANds: And[] = [] |                 const cleanedContainedANds: And[] = [] | ||||||
|                 outer: for (let containedAnd of containedAnds) { |                 outer: for (let containedAnd of containedAnds) { | ||||||
|                     for (const known of newOrs) { |                     for (const known of newOrs) { | ||||||
|                         // input for optimazation: (K=V | (X=Y & K=V))
 |                         // input for optimization: (K=V | (X=Y & K=V))
 | ||||||
|                         // containedAnd: (X=Y & K=V)
 |                         // containedAnd: (X=Y & K=V)
 | ||||||
|                         // newOrs (and thus known): (K=V) --> false
 |                         // newOrs (and thus known): (K=V) --> false
 | ||||||
|                         const cleaned = containedAnd.removePhraseConsideredKnown(known, false) |                         const cleaned = containedAnd.removePhraseConsideredKnown(known, false) | ||||||
|  | @ -236,16 +247,21 @@ export class Or extends TagsFilter { | ||||||
|                     const elements = containedAnd.and.filter( |                     const elements = containedAnd.and.filter( | ||||||
|                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) |                         (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) | ||||||
|                     ) |                     ) | ||||||
|  |                     if (elements.length == 0) { | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|                     newAnds.push(And.construct(elements)) |                     newAnds.push(And.construct(elements)) | ||||||
|                 } |                 } | ||||||
|  |                 if (newAnds.length > 0) { | ||||||
|  |                     commonValues.push(Or.construct(newAnds)) | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 commonValues.push(Or.construct(newAnds)) |  | ||||||
|                 const result = new And(commonValues).optimize() |                 const result = new And(commonValues).optimize() | ||||||
|                 if (result === true) { |                 if (result === true) { | ||||||
|                     return true |                     return true | ||||||
|                 } else if (result === false) { |                 } else if (result === false) { | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|                 } else { |                 } else if (commonValues.length > 0) { | ||||||
|                     newOrs.push(And.construct(commonValues)) |                     newOrs.push(And.construct(commonValues)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Tag } from "./Tag" | import { Tag } from "./Tag" | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| 
 | 
 | ||||||
| export class RegexTag extends TagsFilter { | export class RegexTag extends TagsFilter { | ||||||
|     public readonly key: RegExp | string |     public readonly key: RegExp | string | ||||||
|  | @ -11,6 +12,9 @@ export class RegexTag extends TagsFilter { | ||||||
|         super() |         super() | ||||||
|         this.key = key |         this.key = key | ||||||
|         this.value = value |         this.value = value | ||||||
|  |         if (this.value instanceof RegExp && ("" + this.value).startsWith("^(^(")) { | ||||||
|  |             throw "Detected a duplicate start marker ^(^( in a regextag:" + this.value | ||||||
|  |         } | ||||||
|         this.invert = invert |         this.invert = invert | ||||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value) |         this.matchesEmpty = RegexTag.doesMatch("", this.value) | ||||||
|     } |     } | ||||||
|  | @ -41,11 +45,21 @@ export class RegexTag extends TagsFilter { | ||||||
|         return possibleRegex.test(fromTag) |         return possibleRegex.test(fromTag) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static source(r: string | RegExp) { |     private static source(r: string | RegExp, includeStartMarker: boolean = true) { | ||||||
|         if (typeof r === "string") { |         if (typeof r === "string") { | ||||||
|             return r |             return r | ||||||
|         } |         } | ||||||
|         return r.source |         if (r === undefined) { | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|  |         const src = r.source | ||||||
|  |         if (includeStartMarker) { | ||||||
|  |             return src | ||||||
|  |         } | ||||||
|  |         if (src.startsWith("^(") && src.endsWith(")$")) { | ||||||
|  |             return src.substring(2, src.length - 2) | ||||||
|  |         } | ||||||
|  |         return src | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -82,6 +96,24 @@ export class RegexTag extends TagsFilter { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * import { TagUtils } from "./TagUtils"; | ||||||
|  |      * | ||||||
|  |      * const t = TagUtils.Tag("a~b") | ||||||
|  |      * t.asJson() // => "a~b"
 | ||||||
|  |      * | ||||||
|  |      * const t = TagUtils.Tag("a=") | ||||||
|  |      * t.asJson() // => "a="
 | ||||||
|  |      */ | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         const v = RegexTag.source(this.value, false) | ||||||
|  |         if (typeof this.key === "string") { | ||||||
|  |             const oper = typeof this.value === "string" ? "=" : "~" | ||||||
|  |             return `${this.key}${this.invert ? "!" : ""}${oper}${v}` | ||||||
|  |         } | ||||||
|  |         return `${this.key.source}${this.invert ? "!" : ""}~~${v}` | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     isUsableAsAnswer(): boolean { |     isUsableAsAnswer(): boolean { | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
|  | @ -293,7 +325,7 @@ export class RegexTag extends TagsFilter { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             return [this.key] |             return [this.key] | ||||||
|         } |         } | ||||||
|         throw "Key cannot be determined as it is a regex" |         return [] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedTags(): { key: string; value: string }[] { |     usedTags(): { key: string; value: string }[] { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
| import { Tag } from "./Tag" | import { Tag } from "./Tag" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The substituting-tag uses the tags of a feature a variables and replaces them. |  * The substituting-tag uses the tags of a feature a variables and replaces them. | ||||||
|  | @ -45,6 +46,10 @@ export default class SubstitutingTag implements TagsFilter { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         return this._key + (this._invert ? "!" : "") + ":=" + this._value | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         throw "A variable with substitution can not be used to query overpass" |         throw "A variable with substitution can not be used to query overpass" | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { TagsFilter } from "./TagsFilter" | import { TagsFilter } from "./TagsFilter" | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
| 
 | 
 | ||||||
| export class Tag extends TagsFilter { | export class Tag extends TagsFilter { | ||||||
|     public key: string |     public key: string | ||||||
|  | @ -67,6 +68,10 @@ export class Tag extends TagsFilter { | ||||||
|         return [`["${this.key}"="${this.value}"]`] |         return [`["${this.key}"="${this.value}"]`] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     asJson(): TagConfigJson { | ||||||
|  |         return this.key + "=" + this.value | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
| 
 | 
 | ||||||
|      const t = new Tag("key", "value") |      const t = new Tag("key", "value") | ||||||
|  |  | ||||||
|  | @ -15,13 +15,14 @@ type Tags = Record<string, string> | ||||||
| export type UploadableTag = Tag | SubstitutingTag | And | export type UploadableTag = Tag | SubstitutingTag | And | ||||||
| 
 | 
 | ||||||
| export class TagUtils { | export class TagUtils { | ||||||
|     public static readonly comparators: ReadonlyArray<[string, (a: number, b: number) => boolean]> = |     public static readonly comparators: ReadonlyArray< | ||||||
|         [ |         ["<" | ">" | "<=" | ">=", (a: number, b: number) => boolean] | ||||||
|             ["<=", (a, b) => a <= b], |     > = [ | ||||||
|             [">=", (a, b) => a >= b], |         ["<=", (a, b) => a <= b], | ||||||
|             ["<", (a, b) => a < b], |         [">=", (a, b) => a >= b], | ||||||
|             [">", (a, b) => a > b], |         ["<", (a, b) => a < b], | ||||||
|         ] |         [">", (a, b) => a > b], | ||||||
|  |     ] | ||||||
|     public static modeDocumentation: Record< |     public static modeDocumentation: Record< | ||||||
|         string, |         string, | ||||||
|         { name: string; docs: string; uploadable?: boolean; overpassSupport: boolean } |         { name: string; docs: string; uploadable?: boolean; overpassSupport: boolean } | ||||||
|  | @ -324,6 +325,14 @@ export class TagUtils { | ||||||
|         return tags |         return tags | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     static optimzeJson(json: TagConfigJson): TagConfigJson | boolean { | ||||||
|  |         const optimized = TagUtils.Tag(json).optimize() | ||||||
|  |         if (optimized === true || optimized === false) { | ||||||
|  |             return optimized | ||||||
|  |         } | ||||||
|  |         return optimized.asJson() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. |      * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. | ||||||
|      * |      * | ||||||
|  | @ -735,11 +744,10 @@ export class TagUtils { | ||||||
|         const tag = json as string |         const tag = json as string | ||||||
|         for (const [operator, comparator] of TagUtils.comparators) { |         for (const [operator, comparator] of TagUtils.comparators) { | ||||||
|             if (tag.indexOf(operator) >= 0) { |             if (tag.indexOf(operator) >= 0) { | ||||||
|                 const split = Utils.SplitFirst(tag, operator) |                 const split = Utils.SplitFirst(tag, operator).map((v) => v.trim()) | ||||||
| 
 |                 let val = Number(split[1]) | ||||||
|                 let val = Number(split[1].trim()) |  | ||||||
|                 if (isNaN(val)) { |                 if (isNaN(val)) { | ||||||
|                     val = new Date(split[1].trim()).getTime() |                     val = new Date(split[1]).getTime() | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 const f = (value: string | number | undefined) => { |                 const f = (value: string | number | undefined) => { | ||||||
|  | @ -762,7 +770,7 @@ export class TagUtils { | ||||||
|                     } |                     } | ||||||
|                     return comparator(b, val) |                     return comparator(b, val) | ||||||
|                 } |                 } | ||||||
|                 return new ComparingTag(split[0], f, operator + val) |                 return new ComparingTag(split[0], f, operator, "" + val) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -861,6 +869,27 @@ export class TagUtils { | ||||||
|         return TagUtils.keyCounts.keys[key] |         return TagUtils.keyCounts.keys[key] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static GetPopularity(tag: TagsFilter): number | undefined { | ||||||
|  |         if (tag instanceof And) { | ||||||
|  |             return Math.min(...Utils.NoNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1 | ||||||
|  |         } | ||||||
|  |         if (tag instanceof Or) { | ||||||
|  |             return Math.max(...Utils.NoNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1 | ||||||
|  |         } | ||||||
|  |         if (tag instanceof Tag) { | ||||||
|  |             return TagUtils.GetCount(tag.key, tag.value) | ||||||
|  |         } | ||||||
|  |         if (tag instanceof RegexTag) { | ||||||
|  |             const key = tag.key | ||||||
|  |             if (key instanceof RegExp || tag.invert || tag.isNegative()) { | ||||||
|  |                 return undefined | ||||||
|  |             } | ||||||
|  |             return TagUtils.GetCount(key) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return undefined | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number { |     private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number { | ||||||
|         const rta = a instanceof RegexTag |         const rta = a instanceof RegexTag | ||||||
|         const rtb = b instanceof RegexTag |         const rtb = b instanceof RegexTag | ||||||
|  |  | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" | ||||||
|  | 
 | ||||||
| export abstract class TagsFilter { | export abstract class TagsFilter { | ||||||
|     abstract asOverpass(): string[] |     abstract asOverpass(): string[] | ||||||
| 
 | 
 | ||||||
|  | @ -17,6 +19,8 @@ export abstract class TagsFilter { | ||||||
|         properties: Record<string, string> |         properties: Record<string, string> | ||||||
|     ): string |     ): string | ||||||
| 
 | 
 | ||||||
|  |     abstract asJson(): TagConfigJson | ||||||
|  | 
 | ||||||
|     abstract usedKeys(): string[] |     abstract usedKeys(): string[] | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ export class MangroveIdentity { | ||||||
|         const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined) |         const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined) | ||||||
|         this.keypair = keypairEventSource |         this.keypair = keypairEventSource | ||||||
|         mangroveIdentity.addCallbackAndRunD(async (data) => { |         mangroveIdentity.addCallbackAndRunD(async (data) => { | ||||||
|             if (data === "") { |             if (!data) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) |             const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ export default class Constants { | ||||||
|         "gps_track", |         "gps_track", | ||||||
|         "range", |         "range", | ||||||
|         "last_click", |         "last_click", | ||||||
|  |         "favourite", | ||||||
|     ] as const |     ] as const | ||||||
|     /** |     /** | ||||||
|      * Special layers which are not included in a theme by default |      * Special layers which are not included in a theme by default | ||||||
|  | @ -131,6 +132,8 @@ export default class Constants { | ||||||
|         "clock", |         "clock", | ||||||
|         "invalid", |         "invalid", | ||||||
|         "close", |         "close", | ||||||
|  |         "heart", | ||||||
|  |         "heart_outline", | ||||||
|     ] as const |     ] as const | ||||||
|     public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons |     public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ export class MenuState { | ||||||
|     public static readonly _menuviewTabs = [ |     public static readonly _menuviewTabs = [ | ||||||
|         "about", |         "about", | ||||||
|         "settings", |         "settings", | ||||||
|  |         "favourites", | ||||||
|         "community", |         "community", | ||||||
|         "privacy", |         "privacy", | ||||||
|         "advanced", |         "advanced", | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | ||||||
| import { ConversionContext } from "./ConversionContext" | import { ConversionContext } from "./ConversionContext" | ||||||
|  | import { T } from "vitest/dist/types-aac763a5" | ||||||
| 
 | 
 | ||||||
| export interface DesugaringContext { | export interface DesugaringContext { | ||||||
|     tagRenderings: Map<string, QuestionableTagRenderingConfigJson> |     tagRenderings: Map<string, QuestionableTagRenderingConfigJson> | ||||||
|  | @ -81,18 +82,36 @@ export class Pure<TIn, TOut> extends Conversion<TIn, TOut> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export class Bypass<T> extends DesugaringStep<T> { | ||||||
|  |     private readonly _applyIf: (t: T) => boolean | ||||||
|  |     private readonly _step: DesugaringStep<T> | ||||||
|  |     constructor(applyIf: (t: T) => boolean, step: DesugaringStep<T>) { | ||||||
|  |         super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass") | ||||||
|  |         this._applyIf = applyIf | ||||||
|  |         this._step = step | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     convert(json: T, context: ConversionContext): T { | ||||||
|  |         if (!this._applyIf(json)) { | ||||||
|  |             return json | ||||||
|  |         } | ||||||
|  |         return this._step.convert(json, context) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class Each<X, Y> extends Conversion<X[], Y[]> { | export class Each<X, Y> extends Conversion<X[], Y[]> { | ||||||
|     private readonly _step: Conversion<X, Y> |     private readonly _step: Conversion<X, Y> | ||||||
|     private readonly _msg: string |     private readonly _msg: string | ||||||
|  |     private readonly _filter: (x: X) => boolean | ||||||
| 
 | 
 | ||||||
|     constructor(step: Conversion<X, Y>, msg?: string) { |     constructor(step: Conversion<X, Y>, options?: { msg?: string }) { | ||||||
|         super( |         super( | ||||||
|             "Applies the given step on every element of the list", |             "Applies the given step on every element of the list", | ||||||
|             [], |             [], | ||||||
|             "OnEach(" + step.name + ")" |             "OnEach(" + step.name + ")" | ||||||
|         ) |         ) | ||||||
|         this._step = step |         this._step = step | ||||||
|         this._msg = msg |         this._msg = options?.msg | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     convert(values: X[], context: ConversionContext): Y[] { |     convert(values: X[], context: ConversionContext): Y[] { | ||||||
|  |  | ||||||
|  | @ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L | ||||||
|             description: trs(t.description, { title: layer.title.render }), |             description: trs(t.description, { title: layer.title.render }), | ||||||
|             source: { |             source: { | ||||||
|                 osmTags: { |                 osmTags: { | ||||||
|                     and: ["id~*"], |                     and: ["id~[0-9]+", "comment_url~.*notes/[0-9]*g.json"], | ||||||
|                 }, |                 }, | ||||||
|                 geoJson: |                 geoJson: | ||||||
|                     "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + |                     "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + | ||||||
|  |  | ||||||
|  | @ -10,7 +10,10 @@ import { | ||||||
|     SetDefault, |     SetDefault, | ||||||
| } from "./Conversion" | } from "./Conversion" | ||||||
| import { LayerConfigJson } from "../Json/LayerConfigJson" | import { LayerConfigJson } from "../Json/LayerConfigJson" | ||||||
| import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" | import { | ||||||
|  |     MinimalTagRenderingConfigJson, | ||||||
|  |     TagRenderingConfigJson, | ||||||
|  | } from "../Json/TagRenderingConfigJson" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import RewritableConfigJson from "../Json/RewritableConfigJson" | import RewritableConfigJson from "../Json/RewritableConfigJson" | ||||||
| import SpecialVisualizations from "../../../UI/SpecialVisualizations" | import SpecialVisualizations from "../../../UI/SpecialVisualizations" | ||||||
|  | @ -563,6 +566,16 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class AddEditingElements extends DesugaringStep<LayerConfigJson> { | export class AddEditingElements extends DesugaringStep<LayerConfigJson> { | ||||||
|  |     static addedElements: string[] = [ | ||||||
|  |         "minimap", | ||||||
|  |         "just_created", | ||||||
|  |         "split_button", | ||||||
|  |         "move_button", | ||||||
|  |         "delete_button", | ||||||
|  |         "last_edit", | ||||||
|  |         "favourite_state", | ||||||
|  |         "all_tags", | ||||||
|  |     ] | ||||||
|     private readonly _desugaring: DesugaringContext |     private readonly _desugaring: DesugaringContext | ||||||
| 
 | 
 | ||||||
|     constructor(desugaring: DesugaringContext) { |     constructor(desugaring: DesugaringContext) { | ||||||
|  | @ -636,6 +649,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> { | ||||||
|             json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit")) |             json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit")) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (!usedSpecialFunctions.has("favourite_status")) { | ||||||
|  |             json.tagRenderings.push({ | ||||||
|  |                 id: "favourite_status", | ||||||
|  |                 render: { "*": "{favourite_status()}" }, | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (!usedSpecialFunctions.has("all_tags")) { |         if (!usedSpecialFunctions.has("all_tags")) { | ||||||
|             const trc: QuestionableTagRenderingConfigJson = { |             const trc: QuestionableTagRenderingConfigJson = { | ||||||
|                 id: "all-tags", |                 id: "all-tags", | ||||||
|  | @ -1190,6 +1210,31 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> { | ||||||
|  |     constructor() { | ||||||
|  |         super( | ||||||
|  |             "Adds the favourite heart to the title and the rendering badges", | ||||||
|  |             [], | ||||||
|  |             "AddFavouriteBadges" | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { | ||||||
|  |         if (json.source === "special" || json.source === "special:library") { | ||||||
|  |             return json | ||||||
|  |         } | ||||||
|  |         const pr = json.pointRendering?.[0] | ||||||
|  |         if (pr) { | ||||||
|  |             pr.iconBadges ??= [] | ||||||
|  |             if (!pr.iconBadges.some((ti) => ti.if === "_favourite=yes")) { | ||||||
|  |                 pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return json | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class AddRatingBadge extends DesugaringStep<LayerConfigJson> { | export class AddRatingBadge extends DesugaringStep<LayerConfigJson> { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super( |         super( | ||||||
|  | @ -1203,6 +1248,10 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> { | ||||||
|         if (!json.tagRenderings) { |         if (!json.tagRenderings) { | ||||||
|             return json |             return json | ||||||
|         } |         } | ||||||
|  |         if (json.titleIcons.some((ti) => ti === "icons.rating" || ti["id"] === "rating")) { | ||||||
|  |             // already added
 | ||||||
|  |             return json | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         const specialVis: Exclude<RenderingSpecification, string>[] = < |         const specialVis: Exclude<RenderingSpecification, string>[] = < | ||||||
|             Exclude<RenderingSpecification, string>[] |             Exclude<RenderingSpecification, string>[] | ||||||
|  | @ -1238,23 +1287,28 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const trId = titleIcon.substring("auto:".length) |             const trId = titleIcon.substring("auto:".length) | ||||||
|             const tr = <QuestionableTagRenderingConfigJson>json.tagRenderings.find((tr) => tr["id"] === trId) |             const tr = <QuestionableTagRenderingConfigJson>( | ||||||
|  |                 json.tagRenderings.find((tr) => tr["id"] === trId) | ||||||
|  |             ) | ||||||
|             if (tr === undefined) { |             if (tr === undefined) { | ||||||
|                 context |                 context.enters("titleIcons", i).err("TagRendering with id " + trId + " not found") | ||||||
|                     .enters("titleIcons", i) |  | ||||||
|                     .err("TagRendering with id " + trId + " not found") |  | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             const mappings: { if: TagConfigJson, then: string }[] = tr.mappings?.filter(m => m.icon !== undefined) |             const mappings: { if: TagConfigJson; then: string }[] = tr.mappings | ||||||
|                 .map(m => { |                 ?.filter((m) => m.icon !== undefined) | ||||||
|  |                 .map((m) => { | ||||||
|                     const path: string = typeof m.icon === "string" ? m.icon : m.icon.path |                     const path: string = typeof m.icon === "string" ? m.icon : m.icon.path | ||||||
|                     const img = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>` |                     const img = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>` | ||||||
|                     return ({ if: m.if, then: img }) |                     return { if: m.if, then: img } | ||||||
|                 }) |                 }) | ||||||
|             if (mappings.length === 0) { |             if (mappings.length === 0) { | ||||||
|                 context |                 context | ||||||
|                     .enters("titleIcons", i) |                     .enters("titleIcons", i) | ||||||
|                     .warn("TagRendering with id " + trId + " does not have any icons, not generating an icon for this") |                     .warn( | ||||||
|  |                         "TagRendering with id " + | ||||||
|  |                             trId + | ||||||
|  |                             " does not have any icons, not generating an icon for this" | ||||||
|  |                     ) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             json.titleIcons[i] = <TagRenderingConfigJson>{ |             json.titleIcons[i] = <TagRenderingConfigJson>{ | ||||||
|  | @ -1292,6 +1346,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> { | ||||||
|             ), |             ), | ||||||
|             new SetDefault("titleIcons", ["icons.defaults"]), |             new SetDefault("titleIcons", ["icons.defaults"]), | ||||||
|             new AddRatingBadge(), |             new AddRatingBadge(), | ||||||
|  |             new AddFavouriteBadges(), | ||||||
|             new AutoTitleIcon(), |             new AutoTitleIcon(), | ||||||
|             new On( |             new On( | ||||||
|                 "titleIcons", |                 "titleIcons", | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion" | import { Bypass, Conversion, DesugaringStep, Each, Fuse, On } from "./Conversion" | ||||||
| import { LayerConfigJson } from "../Json/LayerConfigJson" | import { LayerConfigJson } from "../Json/LayerConfigJson" | ||||||
| import LayerConfig from "../LayerConfig" | import LayerConfig from "../LayerConfig" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
|  | @ -11,7 +11,6 @@ import { TagUtils } from "../../../Logic/Tags/TagUtils" | ||||||
| import { ExtractImages } from "./FixImages" | import { ExtractImages } from "./FixImages" | ||||||
| import { And } from "../../../Logic/Tags/And" | import { And } from "../../../Logic/Tags/And" | ||||||
| import Translations from "../../../UI/i18n/Translations" | import Translations from "../../../UI/i18n/Translations" | ||||||
| import Svg from "../../../Svg" |  | ||||||
| import FilterConfigJson from "../Json/FilterConfigJson" | import FilterConfigJson from "../Json/FilterConfigJson" | ||||||
| import DeleteConfig from "../DeleteConfig" | import DeleteConfig from "../DeleteConfig" | ||||||
| import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" | ||||||
|  | @ -23,7 +22,7 @@ import { TagsFilter } from "../../../Logic/Tags/TagsFilter" | ||||||
| import { Translatable } from "../Json/Translatable" | import { Translatable } from "../Json/Translatable" | ||||||
| import { ConversionContext } from "./ConversionContext" | import { ConversionContext } from "./ConversionContext" | ||||||
| 
 | 
 | ||||||
| class ValidateLanguageCompleteness extends DesugaringStep<any> { | class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> { | ||||||
|     private readonly _languages: string[] |     private readonly _languages: string[] | ||||||
| 
 | 
 | ||||||
|     constructor(...languages: string[]) { |     constructor(...languages: string[]) { | ||||||
|  | @ -35,7 +34,9 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> { | ||||||
|         this._languages = languages ?? ["en"] |         this._languages = languages ?? ["en"] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     convert(obj: any, context: ConversionContext): LayerConfig { |     convert(obj: LayoutConfig, context: ConversionContext): LayoutConfig { | ||||||
|  |         const origLayers = obj.layers | ||||||
|  |         obj.layers = [...obj.layers].filter((l) => l["id"] !== "favourite") | ||||||
|         const translations = Translation.ExtractAllTranslationsFrom(obj) |         const translations = Translation.ExtractAllTranslationsFrom(obj) | ||||||
|         for (const neededLanguage of this._languages) { |         for (const neededLanguage of this._languages) { | ||||||
|             translations |             translations | ||||||
|  | @ -57,7 +58,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> { | ||||||
|                         ) |                         ) | ||||||
|                 }) |                 }) | ||||||
|         } |         } | ||||||
| 
 |         obj.layers = origLayers | ||||||
|         return obj |         return obj | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -276,9 +277,9 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> { | ||||||
|             new On( |             new On( | ||||||
|                 "layers", |                 "layers", | ||||||
|                 new Each( |                 new Each( | ||||||
|                     new Pipe( |                     new Bypass( | ||||||
|                         new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true), |                         (layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0, | ||||||
|                         new Pure((x) => x?.raw) |                         new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  | @ -974,7 +975,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> { | ||||||
|             "Various validation on tagRenderingConfigs", |             "Various validation on tagRenderingConfigs", | ||||||
|             new DetectShadowedMappings(layerConfig), |             new DetectShadowedMappings(layerConfig), | ||||||
|             new DetectConflictingAddExtraTags(), |             new DetectConflictingAddExtraTags(), | ||||||
|             new DetectNonErasedKeysInMappings(), |             // TODO enable   new DetectNonErasedKeysInMappings(),
 | ||||||
|             new DetectMappingsWithImages(doesImageExist), |             new DetectMappingsWithImages(doesImageExist), | ||||||
|             new On("render", new ValidatePossibleLinks()), |             new On("render", new ValidatePossibleLinks()), | ||||||
|             new On("question", new ValidatePossibleLinks()), |             new On("question", new ValidatePossibleLinks()), | ||||||
|  | @ -1356,6 +1357,34 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> { | ||||||
|  |     private readonly validator: ValidateLayer | ||||||
|  |     constructor( | ||||||
|  |         path: string, | ||||||
|  |         isBuiltin: boolean, | ||||||
|  |         doesImageExist: DoesImageExist, | ||||||
|  |         studioValidations: boolean = false, | ||||||
|  |         skipDefaultLayers: boolean = false | ||||||
|  |     ) { | ||||||
|  |         super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") | ||||||
|  |         this.validator = new ValidateLayer( | ||||||
|  |             path, | ||||||
|  |             isBuiltin, | ||||||
|  |             doesImageExist, | ||||||
|  |             studioValidations, | ||||||
|  |             skipDefaultLayers | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { | ||||||
|  |         const prepared = this.validator.convert(json, context) | ||||||
|  |         if (!prepared) { | ||||||
|  |             context.err("Preparing layer failed") | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|  |         return prepared?.raw | ||||||
|  |     } | ||||||
|  | } | ||||||
| export class ValidateLayer extends Conversion< | export class ValidateLayer extends Conversion< | ||||||
|     LayerConfigJson, |     LayerConfigJson, | ||||||
|     { parsed: LayerConfig; raw: LayerConfigJson } |     { parsed: LayerConfig; raw: LayerConfigJson } | ||||||
|  |  | ||||||
|  | @ -245,7 +245,7 @@ export interface LayerConfigJson { | ||||||
|      * Type: icon[] |      * Type: icon[] | ||||||
|      * group: infobox |      * group: infobox | ||||||
|      */ |      */ | ||||||
|     titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"] |     titleIcons?: (string | (TagRenderingConfigJson & { id?: string }))[] | ["defaults"] | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Creates points to render on the map. |      * Creates points to render on the map. | ||||||
|  |  | ||||||
|  | @ -305,6 +305,9 @@ export default class LayoutConfig implements LayoutInformation { | ||||||
|         } |         } | ||||||
|         for (const layer of this.layers) { |         for (const layer of this.layers) { | ||||||
|             if (!layer.source) { |             if (!layer.source) { | ||||||
|  |                 if (layer.isShown?.matchesProperties(tags)) { | ||||||
|  |                     return layer | ||||||
|  |                 } | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (layer.source.osmTags.matchesProperties(tags)) { |             if (layer.source.osmTags.matchesProperties(tags)) { | ||||||
|  |  | ||||||
|  | @ -16,10 +16,10 @@ import { | ||||||
| } from "./Json/QuestionableTagRenderingConfigJson" | } from "./Json/QuestionableTagRenderingConfigJson" | ||||||
| import { FixedUiElement } from "../../UI/Base/FixedUiElement" | import { FixedUiElement } from "../../UI/Base/FixedUiElement" | ||||||
| import { Paragraph } from "../../UI/Base/Paragraph" | import { Paragraph } from "../../UI/Base/Paragraph" | ||||||
| import Svg from "../../Svg" |  | ||||||
| import Validators, { ValidatorType } from "../../UI/InputElement/Validators" | import Validators, { ValidatorType } from "../../UI/InputElement/Validators" | ||||||
| import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" | import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" | ||||||
| import Constants from "../Constants" | import Constants from "../Constants" | ||||||
|  | import { RegexTag } from "../../Logic/Tags/RegexTag" | ||||||
| 
 | 
 | ||||||
| export interface Icon {} | export interface Icon {} | ||||||
| 
 | 
 | ||||||
|  | @ -800,4 +800,25 @@ export default class TagRenderingConfig { | ||||||
|             labels, |             labels, | ||||||
|         ]).SetClass("flex flex-col") |         ]).SetClass("flex flex-col") | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public usedTags(): TagsFilter[] { | ||||||
|  |         const tags: TagsFilter[] = [] | ||||||
|  |         tags.push( | ||||||
|  |             this.metacondition, | ||||||
|  |             this.condition, | ||||||
|  |             this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined, | ||||||
|  |             this.invalidValues | ||||||
|  |         ) | ||||||
|  |         for (const m of this.mappings ?? []) { | ||||||
|  |             tags.push(m.if) | ||||||
|  |             tags.push(m.priorityIf) | ||||||
|  |             tags.push(...(m.addExtraTags ?? [])) | ||||||
|  |             if (typeof m.hideInAnswer !== "boolean") { | ||||||
|  |                 tags.push(m.hideInAnswer) | ||||||
|  |             } | ||||||
|  |             tags.push(m.ifnot) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Utils.NoNull(tags) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -58,6 +58,7 @@ import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLay | ||||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | ||||||
| import { Imgur } from "../Logic/ImageProviders/Imgur" | import { Imgur } from "../Logic/ImageProviders/Imgur" | ||||||
| import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource" | import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource" | ||||||
|  | import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -96,10 +97,11 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     readonly indexedFeatures: IndexedFeatureSource & LayoutSource |     readonly indexedFeatures: IndexedFeatureSource & LayoutSource | ||||||
|     readonly currentView: FeatureSource<Feature<Polygon>> |     readonly currentView: FeatureSource<Feature<Polygon>> | ||||||
|     readonly featuresInView: FeatureSource |     readonly featuresInView: FeatureSource | ||||||
|  |     readonly favourites: FavouritesFeatureSource | ||||||
|     /** |     /** | ||||||
|      * Contains a few (<10) >features that are near the center of the map. |      * Contains a few (<10) >features that are near the center of the map. | ||||||
|      */ |      */ | ||||||
|     readonly closestFeatures: FeatureSource |     readonly closestFeatures: NearbyFeatureSource | ||||||
|     readonly newFeatures: WritableFeatureSource |     readonly newFeatures: WritableFeatureSource | ||||||
|     readonly layerState: LayerState |     readonly layerState: LayerState | ||||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> |     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||||
|  | @ -220,8 +222,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|                 this.fullNodeDatabase |                 this.fullNodeDatabase | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             this.indexedFeatures = layoutSource |  | ||||||
| 
 |  | ||||||
|             let currentViewIndex = 0 |             let currentViewIndex = 0 | ||||||
|             const empty = [] |             const empty = [] | ||||||
|             this.currentView = new StaticFeatureSource( |             this.currentView = new StaticFeatureSource( | ||||||
|  | @ -242,13 +242,13 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) |             this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) | ||||||
| 
 | 
 | ||||||
|             this.dataIsLoading = layoutSource.isLoading |             this.dataIsLoading = layoutSource.isLoading | ||||||
|  |             this.indexedFeatures = layoutSource | ||||||
|  |             this.featureProperties = new FeaturePropertiesStore(layoutSource) | ||||||
| 
 | 
 | ||||||
|             const indexedElements = this.indexedFeatures |  | ||||||
|             this.featureProperties = new FeaturePropertiesStore(indexedElements) |  | ||||||
|             this.changes = new Changes( |             this.changes = new Changes( | ||||||
|                 { |                 { | ||||||
|                     dryRun: this.featureSwitches.featureSwitchIsTesting, |                     dryRun: this.featureSwitches.featureSwitchIsTesting, | ||||||
|                     allElements: indexedElements, |                     allElements: layoutSource, | ||||||
|                     featurePropertiesStore: this.featureProperties, |                     featurePropertiesStore: this.featureProperties, | ||||||
|                     osmConnection: this.osmConnection, |                     osmConnection: this.osmConnection, | ||||||
|                     historicalUserLocations: this.geolocation.historicalUserLocations, |                     historicalUserLocations: this.geolocation.historicalUserLocations, | ||||||
|  | @ -258,7 +258,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.historicalUserLocations = this.geolocation.historicalUserLocations |             this.historicalUserLocations = this.geolocation.historicalUserLocations | ||||||
|             this.newFeatures = new NewGeometryFromChangesFeatureSource( |             this.newFeatures = new NewGeometryFromChangesFeatureSource( | ||||||
|                 this.changes, |                 this.changes, | ||||||
|                 indexedElements, |                 layoutSource, | ||||||
|                 this.featureProperties |                 this.featureProperties | ||||||
|             ) |             ) | ||||||
|             layoutSource.addSource(this.newFeatures) |             layoutSource.addSource(this.newFeatures) | ||||||
|  | @ -327,10 +327,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             return sorted |             return sorted | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const lastClick = (this.lastClickObject = new LastClickFeatureSource( |         this.lastClickObject = new LastClickFeatureSource( | ||||||
|             this.mapProperties.lastClickLocation, |             this.mapProperties.lastClickLocation, | ||||||
|             this.layout |             this.layout | ||||||
|         )) |         ) | ||||||
| 
 | 
 | ||||||
|         this.osmObjectDownloader = new OsmObjectDownloader( |         this.osmObjectDownloader = new OsmObjectDownloader( | ||||||
|             this.osmConnection.Backend(), |             this.osmConnection.Backend(), | ||||||
|  | @ -353,6 +353,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.osmConnection, |             this.osmConnection, | ||||||
|             this.changes |             this.changes | ||||||
|         ) |         ) | ||||||
|  |         this.favourites = new FavouritesFeatureSource(this) | ||||||
| 
 | 
 | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers() |         this.drawSpecialLayers() | ||||||
|  | @ -456,6 +457,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private selectClosestAtCenter(i: number = 0) { |     private selectClosestAtCenter(i: number = 0) { | ||||||
|  |         this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000) | ||||||
|         const toSelect = this.closestFeatures.features.data[i] |         const toSelect = this.closestFeatures.features.data[i] | ||||||
|         if (!toSelect) { |         if (!toSelect) { | ||||||
|             return |             return | ||||||
|  | @ -465,6 +467,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.selectedLayer.setData(layer) |         this.selectedLayer.setData(layer) | ||||||
|         this.selectedElement.setData(toSelect) |         this.selectedElement.setData(toSelect) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     private initHotkeys() { |     private initHotkeys() { | ||||||
|         Hotkeys.RegisterHotkey( |         Hotkeys.RegisterHotkey( | ||||||
|             { nomod: "Escape", onUp: true }, |             { nomod: "Escape", onUp: true }, | ||||||
|  | @ -476,6 +479,15 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |         Hotkeys.RegisterHotkey( | ||||||
|  |             { nomod: "f" }, | ||||||
|  |             Translations.t.hotkeyDocumentation.selectFavourites, | ||||||
|  |             () => { | ||||||
|  |                 this.guistate.menuViewTab.setData("favourites") | ||||||
|  |                 this.guistate.menuIsOpened.setData(true) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { |         this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { | ||||||
|             Hotkeys.RegisterHotkey( |             Hotkeys.RegisterHotkey( | ||||||
|                 { |                 { | ||||||
|  | @ -561,46 +573,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private addLastClick(last_click: LastClickFeatureSource) { |  | ||||||
|         // The last_click gets a _very_ special treatment as it interacts with various parts
 |  | ||||||
| 
 |  | ||||||
|         this.featureProperties.trackFeatureSource(last_click) |  | ||||||
|         this.indexedFeatures.addSource(last_click) |  | ||||||
| 
 |  | ||||||
|         last_click.features.addCallbackAndRunD((features) => { |  | ||||||
|             if (this.selectedLayer.data?.id === "last_click") { |  | ||||||
|                 // The last-click location moved, but we have selected the last click of the previous location
 |  | ||||||
|                 // So, we update _after_ clearing the selection to make sure no stray data is sticking around
 |  | ||||||
|                 this.selectedElement.setData(undefined) |  | ||||||
|                 this.selectedElement.setData(features[0]) |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         new ShowDataLayer(this.map, { |  | ||||||
|             features: new FilteringFeatureSource(this.newPointDialog, last_click), |  | ||||||
|             doShowLayer: this.featureSwitches.featureSwitchEnableLogin, |  | ||||||
|             layer: this.newPointDialog.layerDef, |  | ||||||
|             selectedElement: this.selectedElement, |  | ||||||
|             selectedLayer: this.selectedLayer, |  | ||||||
|             metaTags: this.userRelatedState.preferencesAsTags, |  | ||||||
|             onClick: (feature: Feature) => { |  | ||||||
|                 if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { |  | ||||||
|                     this.map.data.flyTo({ |  | ||||||
|                         zoom: Constants.minZoomLevelToAddNewPoint, |  | ||||||
|                         center: this.mapProperties.lastClickLocation.data, |  | ||||||
|                     }) |  | ||||||
|                     return |  | ||||||
|                 } |  | ||||||
|                 // We first clear the selection to make sure no weird state is around
 |  | ||||||
|                 this.selectedLayer.setData(undefined) |  | ||||||
|                 this.selectedElement.setData(undefined) |  | ||||||
| 
 |  | ||||||
|                 this.selectedElement.setData(feature) |  | ||||||
|                 this.selectedLayer.setData(this.newPointDialog.layerDef) |  | ||||||
|             }, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Add the special layers to the map |      * Add the special layers to the map | ||||||
|      */ |      */ | ||||||
|  | @ -627,7 +599,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|                 ) |                 ) | ||||||
|             ), |             ), | ||||||
|             current_view: this.currentView, |             current_view: this.currentView, | ||||||
|  |             favourite: this.favourites, | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         this.closestFeatures.registerSource(specialLayers.favourite, "favourite") | ||||||
|         if (this.layout?.lockLocation) { |         if (this.layout?.lockLocation) { | ||||||
|             const bbox = new BBox(this.layout.lockLocation) |             const bbox = new BBox(this.layout.lockLocation) | ||||||
|             this.mapProperties.maxbounds.setData(bbox) |             this.mapProperties.maxbounds.setData(bbox) | ||||||
|  | @ -654,21 +629,23 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") |         const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") | ||||||
| 
 |  | ||||||
|         const rangeIsDisplayed = rangeFLayer?.isDisplayed |         const rangeIsDisplayed = rangeFLayer?.isDisplayed | ||||||
| 
 |  | ||||||
|         if ( |         if ( | ||||||
|             !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) |             !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) | ||||||
|         ) { |         ) { | ||||||
|             rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) |             rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // enumarate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
 | ||||||
|         this.layerState.filteredLayers.forEach((flayer) => { |         this.layerState.filteredLayers.forEach((flayer) => { | ||||||
|             const id = flayer.layerDef.id |             const id = flayer.layerDef.id | ||||||
|             const features: FeatureSource = specialLayers[id] |             const features: FeatureSource = specialLayers[id] | ||||||
|             if (features === undefined) { |             if (features === undefined) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|  |             if (id === "favourite") { | ||||||
|  |                 console.log("Matching special layer", id, flayer) | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             this.featureProperties.trackFeatureSource(features) |             this.featureProperties.trackFeatureSource(features) | ||||||
|             new ShowDataLayer(this.map, { |             new ShowDataLayer(this.map, { | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
| 
 | 
 | ||||||
| <button class={clss} on:click={() => osmConnection.AttemptLogin()}> | <button class={clss} on:click={() => osmConnection.AttemptLogin()}> | ||||||
|   <ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} /> |   <ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} /> | ||||||
|   <slot name="message"> |   <slot> | ||||||
|     <Tr t={Translations.t.general.loginWithOpenStreetMap} /> |     <Tr t={Translations.t.general.loginWithOpenStreetMap} /> | ||||||
|   </slot> |   </slot> | ||||||
| </button> | </button> | ||||||
|  |  | ||||||
|  | @ -4,12 +4,12 @@ | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Tr from "./Tr.svelte" |   import Tr from "./Tr.svelte" | ||||||
| 
 | 
 | ||||||
|   export let osmConnection: OsmConnection |   export let osmConnection: OsmConnection; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button | <button | ||||||
|   on:click={() => { |   on:click={() => { | ||||||
|     state.osmConnection.LogOut() |     osmConnection.LogOut() | ||||||
|   }} |   }} | ||||||
| > | > | ||||||
|   <Logout class="h-6 w-6" /> |   <Logout class="h-6 w-6" /> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
|     const uiElem = typeof construct === "function" ? construct() : construct |     const uiElem = typeof construct === "function" ? construct() : construct | ||||||
|     html = uiElem?.ConstructElement() |     html = uiElem?.ConstructElement() | ||||||
|     if (html !== undefined) { |     if (html !== undefined) { | ||||||
|       elem.replaceWith(html) |       elem?.replaceWith(html) | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -121,9 +121,9 @@ export default class UploadTraceToOsmUI extends LoginToggle { | ||||||
|                     ]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"), |                     ]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"), | ||||||
|                     new Toggle( |                     new Toggle( | ||||||
|                         confirmPanel, |                         confirmPanel, | ||||||
|                         new SubtleButton(new SvelteUIElement(Upload), t.title).onClick(() => |                         new SubtleButton(new SvelteUIElement(Upload), t.title) | ||||||
|                             clicked.setData(true) |                             .onClick(() => clicked.setData(true)) | ||||||
|                         ), |                             .SetClass("w-full"), | ||||||
|                         clicked |                         clicked | ||||||
|                     ), |                     ), | ||||||
|                     uploadFinished |                     uploadFinished | ||||||
|  |  | ||||||
							
								
								
									
										83
									
								
								src/UI/Favourites/FavouriteSummary.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/UI/Favourites/FavouriteSummary.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  |   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"; | ||||||
|  |   import type { Feature } from "geojson"; | ||||||
|  |   import { ImmutableStore } from "../../Logic/UIEventSource"; | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations"; | ||||||
|  |   import Center from "../../assets/svg/Center.svelte"; | ||||||
|  | 
 | ||||||
|  |   export let feature: Feature; | ||||||
|  |   let properties: Record<string, string> = feature.properties; | ||||||
|  |   export let state: SpecialVisualizationState; | ||||||
|  |   let tags = state.featureProperties.getStore(properties.id) ?? new ImmutableStore(properties); | ||||||
|  | 
 | ||||||
|  |   const favLayer = state.layerState.filteredLayers.get("favourite"); | ||||||
|  |   const favConfig = favLayer.layerDef; | ||||||
|  |   const titleConfig = favConfig.title; | ||||||
|  | 
 | ||||||
|  |   function center() { | ||||||
|  |     const [lon, lat] = GeoOperations.centerpointCoordinates(feature); | ||||||
|  |     state.mapProperties.location.setData( | ||||||
|  |       { lon, lat } | ||||||
|  |     ); | ||||||
|  |     const z = state.mapProperties.zoom.data | ||||||
|  |     state.mapProperties.zoom.setData( Math.min(17, Math.max(12, z )) ) | ||||||
|  |     state.guistate.menuIsOpened.setData(false); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function select() { | ||||||
|  |     state.selectedLayer.setData(favConfig); | ||||||
|  |     state.selectedElement.setData(feature); | ||||||
|  |     center(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const coord = GeoOperations.centerpointCoordinates(feature); | ||||||
|  |   const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => { | ||||||
|  |     let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat])); | ||||||
|  | 
 | ||||||
|  |     if (meters < 1000) { | ||||||
|  |       return meters + "m"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     meters = Math.round(meters / 100); | ||||||
|  |     const kmStr = "" + meters; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km"; | ||||||
|  |   }); | ||||||
|  |   const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]; | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded grid grid-cols-2 items-center no-weblate"> | ||||||
|  |   <button class="cursor-pointer ml-1 m-0 link justify-self-start" on:click={() => select()}> | ||||||
|  |     <TagRenderingAnswer config={titleConfig} extraClasses="underline" layer={favConfig} selectedElement={feature} | ||||||
|  |                         {tags} /> | ||||||
|  |   </button> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   <div class="flex items-center justify-self-end title-icons links-as-button gap-x-0.5 p-1 pt-0.5 sm:pt-1"> | ||||||
|  |     {#each favConfig.titleIcons as titleIconConfig} | ||||||
|  |       {#if (titleIconBlacklist.indexOf(titleIconConfig.id) < 0) && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)} | ||||||
|  |         <div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}> | ||||||
|  |           <TagRenderingAnswer | ||||||
|  |             config={titleIconConfig} | ||||||
|  |             {tags} | ||||||
|  |             selectedElement={feature} | ||||||
|  |             {state} | ||||||
|  |             layer={favLayer} | ||||||
|  |             extraClasses="h-full justify-center" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       {/if} | ||||||
|  |     {/each} | ||||||
|  | 
 | ||||||
|  |     <button class="p-1" on:click={() => center()}> | ||||||
|  |       <Center class="w-6 h-6" /> | ||||||
|  |     </button> | ||||||
|  |     <div class="w-14"> | ||||||
|  |       {$distance} | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										68
									
								
								src/UI/Favourites/Favourites.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/UI/Favourites/Favourites.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  |   import FavouriteSummary from "./FavouriteSummary.svelte"; | ||||||
|  |   import Translations from "../i18n/Translations"; | ||||||
|  |   import Tr from "../Base/Tr.svelte"; | ||||||
|  |   import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|  |   import { Utils } from "../../Utils"; | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations"; | ||||||
|  |   import type { Feature, LineString, Point } from "geojson"; | ||||||
|  |   import LoginToggle from "../Base/LoginToggle.svelte"; | ||||||
|  |   import LoginButton from "../Base/LoginButton.svelte"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A panel showing all your favourites | ||||||
|  |    */ | ||||||
|  |   export let state: SpecialVisualizationState; | ||||||
|  |   let favourites = state.favourites.allFavourites; | ||||||
|  | 
 | ||||||
|  |   function downloadGeojson() { | ||||||
|  |     const contents = { features: favourites.data, type: "FeatureCollection" }; | ||||||
|  |     Utils.offerContentsAsDownloadableFile( | ||||||
|  |       JSON.stringify(contents), | ||||||
|  |       "mapcomplete-favourites-" + (new Date().toISOString()) + ".geojson", | ||||||
|  |       { | ||||||
|  |         mimetype: "application/vnd.geo+json" | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function downloadGPX() { | ||||||
|  |     const gpx = GeoOperations.toGpxPoints(<Feature<Point>>favourites.data, "MapComplete favourites"); | ||||||
|  |     Utils.offerContentsAsDownloadableFile(gpx, | ||||||
|  |       "mapcomplete-favourites-" + (new Date().toISOString()) + ".gpx", | ||||||
|  |       { | ||||||
|  |         mimetype: "{gpx=application/gpx+xml}" | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  |    | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <LoginToggle {state}> | ||||||
|  |   <div slot="not-logged-in"> | ||||||
|  |     <LoginButton osmConnection={state.osmConnection}> | ||||||
|  |       <Tr t={Translations.t.favouritePoi.loginToSeeList}/> | ||||||
|  |     </LoginButton> | ||||||
|  |   </div> | ||||||
|  |    | ||||||
|  | <div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}> | ||||||
|  |   <Tr t={Translations.t.favouritePoi.intro.Subs({length: $favourites?.length ?? 0})} /> | ||||||
|  |   <Tr t={Translations.t.favouritePoi.privacy} /> | ||||||
|  | 
 | ||||||
|  |   {#each $favourites as feature (feature.properties.id)} | ||||||
|  |     <FavouriteSummary {feature} {state} /> | ||||||
|  |   {/each} | ||||||
|  | 
 | ||||||
|  |   <div class="mt-8"> | ||||||
|  |     <button class="flex p-2" on:click={() => downloadGeojson()}> | ||||||
|  |       <DownloadIcon class="h-6 w-6" /> | ||||||
|  |       <Tr t={Translations.t.favouritePoi.downloadGeojson} /> | ||||||
|  |     </button> | ||||||
|  |     <button class="flex p-2" on:click={() => downloadGPX()}> | ||||||
|  |       <DownloadIcon class="h-6 w-6" /> | ||||||
|  |       <Tr t={Translations.t.favouritePoi.downloadGpx} /> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | </LoginToggle> | ||||||
|  | @ -1,27 +1,7 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig" |   import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"; | ||||||
|   import { Store } from "../../Logic/UIEventSource" |   import { Store } from "../../Logic/UIEventSource"; | ||||||
|   import Pin from "../../assets/svg/Pin.svelte" |   import Icon from "./Icon.svelte"; | ||||||
|   import Square from "../../assets/svg/Square.svelte" |  | ||||||
|   import Circle from "../../assets/svg/Circle.svelte" |  | ||||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte" |  | ||||||
|   import Clock from "../../assets/svg/Clock.svelte" |  | ||||||
|   import Close from "../../assets/svg/Close.svelte" |  | ||||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte" |  | ||||||
|   import Help from "../../assets/svg/Help.svelte" |  | ||||||
|   import Home from "../../assets/svg/Home.svelte" |  | ||||||
|   import Invalid from "../../assets/svg/Invalid.svelte" |  | ||||||
|   import Location from "../../assets/svg/Location.svelte" |  | ||||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte" |  | ||||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte" |  | ||||||
|   import Note from "../../assets/svg/Note.svelte" |  | ||||||
|   import Resolved from "../../assets/svg/Resolved.svelte" |  | ||||||
|   import Ring from "../../assets/svg/Ring.svelte" |  | ||||||
|   import Scissors from "../../assets/svg/Scissors.svelte" |  | ||||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte" |  | ||||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" |  | ||||||
|   import Triangle from "../../assets/svg/Triangle.svelte" |  | ||||||
|   import Icon from "./Icon.svelte" |  | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Renders a single icon. |    * Renders a single icon. | ||||||
|  |  | ||||||
|  | @ -7,9 +7,9 @@ | ||||||
|   /** |   /** | ||||||
|    * Renders a 'marker', which consists of multiple 'icons' |    * Renders a 'marker', which consists of multiple 'icons' | ||||||
|    */ |    */ | ||||||
|   export let marker: IconConfig[] = config?.marker |   export let marker: IconConfig[] = config?.marker; | ||||||
|   export let tags: Store<Record<string, string>> |   export let tags: Store<Record<string, string>> | ||||||
|   export let rotation: TagRenderingConfig |   export let rotation: TagRenderingConfig = undefined; | ||||||
|   let _rotation = rotation |   let _rotation = rotation | ||||||
|     ? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt) |     ? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt) | ||||||
|     : new ImmutableStore(0) |     : new ImmutableStore(0) | ||||||
|  | @ -18,7 +18,9 @@ | ||||||
| {#if marker && marker} | {#if marker && marker} | ||||||
|   <div class="relative h-full w-full" style={`transform: rotate(${$_rotation})`}> |   <div class="relative h-full w-full" style={`transform: rotate(${$_rotation})`}> | ||||||
|     {#each marker as icon} |     {#each marker as icon} | ||||||
|       <DynamicIcon {icon} {tags} /> |       <div class="absolute top-0 left-0 h-full w-full"> | ||||||
|  |         <DynamicIcon {icon} {tags} /> | ||||||
|  |       </div> | ||||||
|     {/each} |     {/each} | ||||||
|   </div> |   </div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -1,27 +1,29 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import Pin from "../../assets/svg/Pin.svelte" |   import Pin from "../../assets/svg/Pin.svelte"; | ||||||
|   import Square from "../../assets/svg/Square.svelte" |   import Square from "../../assets/svg/Square.svelte"; | ||||||
|   import Circle from "../../assets/svg/Circle.svelte" |   import Circle from "../../assets/svg/Circle.svelte"; | ||||||
|   import Checkmark from "../../assets/svg/Checkmark.svelte" |   import Checkmark from "../../assets/svg/Checkmark.svelte"; | ||||||
|   import Clock from "../../assets/svg/Clock.svelte" |   import Clock from "../../assets/svg/Clock.svelte"; | ||||||
|   import Close from "../../assets/svg/Close.svelte" |   import Close from "../../assets/svg/Close.svelte"; | ||||||
|   import Crosshair from "../../assets/svg/Crosshair.svelte" |   import Crosshair from "../../assets/svg/Crosshair.svelte"; | ||||||
|   import Help from "../../assets/svg/Help.svelte" |   import Help from "../../assets/svg/Help.svelte"; | ||||||
|   import Home from "../../assets/svg/Home.svelte" |   import Home from "../../assets/svg/Home.svelte"; | ||||||
|   import Invalid from "../../assets/svg/Invalid.svelte" |   import Invalid from "../../assets/svg/Invalid.svelte"; | ||||||
|   import Location from "../../assets/svg/Location.svelte" |   import Location from "../../assets/svg/Location.svelte"; | ||||||
|   import Location_empty from "../../assets/svg/Location_empty.svelte" |   import Location_empty from "../../assets/svg/Location_empty.svelte"; | ||||||
|   import Location_locked from "../../assets/svg/Location_locked.svelte" |   import Location_locked from "../../assets/svg/Location_locked.svelte"; | ||||||
|   import Note from "../../assets/svg/Note.svelte" |   import Note from "../../assets/svg/Note.svelte"; | ||||||
|   import Resolved from "../../assets/svg/Resolved.svelte" |   import Resolved from "../../assets/svg/Resolved.svelte"; | ||||||
|   import Ring from "../../assets/svg/Ring.svelte" |   import Ring from "../../assets/svg/Ring.svelte"; | ||||||
|   import Scissors from "../../assets/svg/Scissors.svelte" |   import Scissors from "../../assets/svg/Scissors.svelte"; | ||||||
|   import Teardrop from "../../assets/svg/Teardrop.svelte" |   import Teardrop from "../../assets/svg/Teardrop.svelte"; | ||||||
|   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" |   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"; | ||||||
|   import Triangle from "../../assets/svg/Triangle.svelte" |   import Triangle from "../../assets/svg/Triangle.svelte"; | ||||||
|   import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte" |   import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"; | ||||||
|   import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte" |   import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"; | ||||||
|   import Gps_arrow from "../../assets/svg/Gps_arrow.svelte" |   import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"; | ||||||
|  |   import { HeartIcon } from "@babeard/svelte-heroicons/solid"; | ||||||
|  |   import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Renders a single icon. |    * Renders a single icon. | ||||||
|  | @ -29,68 +31,72 @@ | ||||||
|    * Icons -placed on top of each other- form a 'Marker' together |    * Icons -placed on top of each other- form a 'Marker' together | ||||||
|    */ |    */ | ||||||
| 
 | 
 | ||||||
|   export let icon: string | undefined |   export let icon: string | undefined; | ||||||
|   export let color: string | undefined |   export let color: string | undefined = undefined | ||||||
|  |   export let clss: string | undefined = undefined | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if icon} | {#if icon} | ||||||
|   <div class="absolute top-0 left-0 h-full w-full"> |  | ||||||
|     {#if icon === "pin"} |     {#if icon === "pin"} | ||||||
|       <Pin {color} /> |       <Pin {color} class={clss}/> | ||||||
|     {:else if icon === "square"} |     {:else if icon === "square"} | ||||||
|       <Square {color} /> |       <Square {color} class={clss}/> | ||||||
|     {:else if icon === "circle"} |     {:else if icon === "circle"} | ||||||
|       <Circle {color} /> |       <Circle {color} class={clss}/> | ||||||
|     {:else if icon === "checkmark"} |     {:else if icon === "checkmark"} | ||||||
|       <Checkmark {color} /> |       <Checkmark {color} class={clss}/> | ||||||
|     {:else if icon === "clock"} |     {:else if icon === "clock"} | ||||||
|       <Clock {color} /> |       <Clock {color} class={clss}/> | ||||||
|     {:else if icon === "close"} |     {:else if icon === "close"} | ||||||
|       <Close {color} /> |       <Close {color} class={clss}/> | ||||||
|     {:else if icon === "crosshair"} |     {:else if icon === "crosshair"} | ||||||
|       <Crosshair {color} /> |       <Crosshair {color} class={clss}/> | ||||||
|     {:else if icon === "help"} |     {:else if icon === "help"} | ||||||
|       <Help {color} /> |       <Help {color} class={clss}/> | ||||||
|     {:else if icon === "home"} |     {:else if icon === "home"} | ||||||
|       <Home {color} /> |       <Home {color} class={clss}/> | ||||||
|     {:else if icon === "invalid"} |     {:else if icon === "invalid"} | ||||||
|       <Invalid {color} /> |       <Invalid {color} class={clss}/> | ||||||
|     {:else if icon === "location"} |     {:else if icon === "location"} | ||||||
|       <Location {color} /> |       <Location {color} class={clss}/> | ||||||
|     {:else if icon === "location_empty"} |     {:else if icon === "location_empty"} | ||||||
|       <Location_empty {color} /> |       <Location_empty {color} class={clss}/> | ||||||
|     {:else if icon === "location_locked"} |     {:else if icon === "location_locked"} | ||||||
|       <Location_locked {color} /> |       <Location_locked {color} class={clss}/> | ||||||
|     {:else if icon === "note"} |     {:else if icon === "note"} | ||||||
|       <Note {color} /> |       <Note {color} class={clss}/> | ||||||
|     {:else if icon === "resolved"} |     {:else if icon === "resolved"} | ||||||
|       <Resolved {color} /> |       <Resolved {color} class={clss}/> | ||||||
|     {:else if icon === "ring"} |     {:else if icon === "ring"} | ||||||
|       <Ring {color} /> |       <Ring {color} class={clss}/> | ||||||
|     {:else if icon === "scissors"} |     {:else if icon === "scissors"} | ||||||
|       <Scissors {color} /> |       <Scissors {color} class={clss}/> | ||||||
|     {:else if icon === "teardrop"} |     {:else if icon === "teardrop"} | ||||||
|       <Teardrop {color} /> |       <Teardrop {color} class={clss}/> | ||||||
|     {:else if icon === "teardrop_with_hole_green"} |     {:else if icon === "teardrop_with_hole_green"} | ||||||
|       <Teardrop_with_hole_green {color} /> |       <Teardrop_with_hole_green {color} class={clss}/> | ||||||
|     {:else if icon === "triangle"} |     {:else if icon === "triangle"} | ||||||
|       <Triangle {color} /> |       <Triangle {color} class={clss}/> | ||||||
|     {:else if icon === "brick_wall_square"} |     {:else if icon === "brick_wall_square"} | ||||||
|       <Brick_wall_square {color} /> |       <Brick_wall_square {color} class={clss}/> | ||||||
|     {:else if icon === "brick_wall_round"} |     {:else if icon === "brick_wall_round"} | ||||||
|       <Brick_wall_round {color} /> |       <Brick_wall_round {color} class={clss}/> | ||||||
|     {:else if icon === "gps_arrow"} |     {:else if icon === "gps_arrow"} | ||||||
|       <Gps_arrow {color} /> |       <Gps_arrow {color} class={clss}/> | ||||||
|     {:else if icon === "checkmark"} |     {:else if icon === "checkmark"} | ||||||
|       <Checkmark {color} /> |       <Checkmark {color} class={clss}/> | ||||||
|     {:else if icon === "help"} |     {:else if icon === "help"} | ||||||
|       <Help {color} /> |       <Help {color} class={clss}/> | ||||||
|     {:else if icon === "close"} |     {:else if icon === "close"} | ||||||
|       <Close {color} /> |       <Close {color} class={clss}/> | ||||||
|     {:else if icon === "invalid"} |     {:else if icon === "invalid"} | ||||||
|       <Invalid {color} /> |       <Invalid {color} class={clss}/> | ||||||
|  |     {:else if icon === "heart"} | ||||||
|  |       <HeartIcon class={clss}/> | ||||||
|  |     {:else if icon === "heart_outline"} | ||||||
|  |       <HeartOutlineIcon class={clss}/> | ||||||
|     {:else} |     {:else} | ||||||
|       <img class="h-full w-full" src={icon} /> |       <img class={clss ?? "h-full w-full"} src={icon}  aria-hidden="true" | ||||||
|  |            alt="" /> | ||||||
|     {/if} |     {/if} | ||||||
|   </div> |  | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -1,16 +1,18 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import Icon from "./Icon.svelte" |   import Icon from "./Icon.svelte"; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Renders a 'marker', which consists of multiple 'icons' |    * Renders a 'marker', which consists of multiple 'icons' | ||||||
|    */ |    */ | ||||||
|   export let icons: { icon: string; color: string }[] |   export let icons: { icon: string; color: string }[]; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if icons !== undefined && icons.length > 0} | {#if icons !== undefined && icons.length > 0} | ||||||
|   <div class="relative h-full w-full"> |   <div class="relative h-full w-full"> | ||||||
|     {#each icons as icon} |     {#each icons as icon} | ||||||
|       <Icon icon={icon.icon} color={icon.color} /> |       <div class="absolute top-0 left-0 h-full w-full"> | ||||||
|  |         <Icon icon={icon.icon} color={icon.color} /> | ||||||
|  |       </div> | ||||||
|     {/each} |     {/each} | ||||||
|   </div> |   </div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -12,11 +12,9 @@ import { Feature, Point } from "geojson" | ||||||
| import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" | import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import * as range_layer from "../../../assets/layers/range/range.json" | import * as range_layer from "../../../assets/layers/range/range.json" | ||||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" |  | ||||||
| import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
| import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" | import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" | ||||||
| import { CLIENT_RENEG_LIMIT } from "tls" |  | ||||||
| 
 | 
 | ||||||
| class PointRenderingLayer { | class PointRenderingLayer { | ||||||
|     private readonly _config: PointRenderingConfig |     private readonly _config: PointRenderingConfig | ||||||
|  |  | ||||||
							
								
								
									
										50
									
								
								src/UI/OpeningHours/NextChangeViz.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/UI/OpeningHours/NextChangeViz.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | <script lang="ts">/** | ||||||
|  |  * Simple visualisation which shows when the POI opens/closes next. | ||||||
|  |  */ | ||||||
|  | import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  | import { Store, Stores } from "../../Logic/UIEventSource"; | ||||||
|  | import { OH } from "./OpeningHours"; | ||||||
|  | import opening_hours from "opening_hours"; | ||||||
|  | import Clock from "../../assets/svg/Clock.svelte"; | ||||||
|  | import { Utils } from "../../Utils"; | ||||||
|  | import Circle from "../../assets/svg/Circle.svelte"; | ||||||
|  | import Ring from "../../assets/svg/Ring.svelte"; | ||||||
|  | import { twMerge } from "tailwind-merge"; | ||||||
|  | 
 | ||||||
|  | export let state: SpecialVisualizationState; | ||||||
|  | export let tags: Store<Record<string, string>>; | ||||||
|  | export let keyToUse: string = "opening_hours"; | ||||||
|  | export let prefix: string = undefined; | ||||||
|  | export let postfix: string = undefined; | ||||||
|  | let oh: Store<opening_hours | "error" | undefined> = OH.CreateOhObjectStore(tags, keyToUse, prefix, postfix); | ||||||
|  | 
 | ||||||
|  | let currentState = oh.mapD(oh => typeof oh === "string" ? undefined : oh.getState()); | ||||||
|  | let tomorrow = new Date(); | ||||||
|  | tomorrow.setTime(tomorrow.getTime() + 24 * 60 * 60 * 1000); | ||||||
|  | let nextChange = oh | ||||||
|  |   .mapD(oh => typeof oh === "string" ? undefined : oh.getNextChange(new Date(), tomorrow), [Stores.Chronic(5 * 60 * 1000)]) | ||||||
|  |   .mapD(date => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes())); | ||||||
|  | 
 | ||||||
|  | let size = nextChange.map(change => change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4"); | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if $currentState !== undefined} | ||||||
|  |   <div class="relative h-8 w-8"> | ||||||
|  |     {#if $currentState === true} | ||||||
|  |       <Ring class={$size} color="#0f0" style="z-index: 0" /> | ||||||
|  |       <Clock class={$size} color="#0f0" style="z-index: 0" /> | ||||||
|  |     {:else if $currentState === false} | ||||||
|  |       <Circle class={$size} color="#f00" style="z-index: 0" /> | ||||||
|  |       <Clock class={$size} color="#fff" style="z-index: 0" /> | ||||||
|  |     {/if} | ||||||
|  | 
 | ||||||
|  |     {#if $nextChange !== undefined} | ||||||
|  |       <span class="absolute bottom-0 font-bold text-sm" style="z-index: 1; background-color: #ffffff88; margin-top: 3px"> | ||||||
|  |         {$nextChange} | ||||||
|  |       </span> | ||||||
|  |     {/if} | ||||||
|  | 
 | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | {/if} | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import opening_hours from "opening_hours" | import opening_hours from "opening_hours" | ||||||
|  | import { Store } from "../../Logic/UIEventSource" | ||||||
| 
 | 
 | ||||||
| export interface OpeningHour { | export interface OpeningHour { | ||||||
|     weekday: number // 0 is monday, 1 is tuesday, ...
 |     weekday: number // 0 is monday, 1 is tuesday, ...
 | ||||||
|  | @ -494,10 +495,48 @@ This list will be sorted | ||||||
| 
 | 
 | ||||||
|         return [changeHours, changeHourText] |         return [changeHours, changeHourText] | ||||||
|     } |     } | ||||||
|  |     public static CreateOhObjectStore( | ||||||
|  |         tags: Store<Record<string, string>>, | ||||||
|  |         key: string = "opening_hours", | ||||||
|  |         prefixToIgnore?: string, | ||||||
|  |         postfixToIgnore?: string | ||||||
|  |     ): Store<opening_hours | undefined | "error"> { | ||||||
|  |         prefixToIgnore ??= "" | ||||||
|  |         postfixToIgnore ??= "" | ||||||
|  |         const country = tags.map((tags) => tags._country) | ||||||
|  |         return tags | ||||||
|  |             .mapD((tags) => { | ||||||
|  |                 const value: string = tags[key] | ||||||
|  |                 if (value === undefined) { | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|  |                 if ( | ||||||
|  |                     (prefixToIgnore || postfixToIgnore) && | ||||||
|  |                     value.startsWith(prefixToIgnore) && | ||||||
|  |                     value.endsWith(postfixToIgnore) | ||||||
|  |                 ) { | ||||||
|  |                     return value | ||||||
|  |                         .substring(prefixToIgnore.length, value.length - postfixToIgnore.length) | ||||||
|  |                         .trim() | ||||||
|  |                 } | ||||||
|  |                 return value | ||||||
|  |             }) | ||||||
|  |             .mapD( | ||||||
|  |                 (ohtext) => { | ||||||
|  |                     try { | ||||||
|  |                         return OH.CreateOhObject(<any>tags.data, ohtext, country.data) | ||||||
|  |                     } catch (e) { | ||||||
|  |                         return "error" | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 [country] | ||||||
|  |             ) | ||||||
|  |     } | ||||||
|     public static CreateOhObject( |     public static CreateOhObject( | ||||||
|         tags: Record<string, string> & { _lat: number; _lon: number; _country?: string }, |         tags: Record<string, string> & { _lat: number; _lon: number; _country?: string }, | ||||||
|         textToParse: string |         textToParse: string, | ||||||
|  |         country?: string | ||||||
|     ) { |     ) { | ||||||
|         // noinspection JSPotentiallyInvalidConstructorUsage
 |         // noinspection JSPotentiallyInvalidConstructorUsage
 | ||||||
|         return new opening_hours( |         return new opening_hours( | ||||||
|  | @ -506,7 +545,7 @@ This list will be sorted | ||||||
|                 lat: tags._lat, |                 lat: tags._lat, | ||||||
|                 lon: tags._lon, |                 lon: tags._lon, | ||||||
|                 address: { |                 address: { | ||||||
|                     country_code: tags._country?.toLowerCase(), |                     country_code: country.toLowerCase(), | ||||||
|                     state: undefined, |                     state: undefined, | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -3,7 +3,6 @@ import Combine from "../Base/Combine" | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" | import { FixedUiElement } from "../Base/FixedUiElement" | ||||||
| import { OH } from "./OpeningHours" | import { OH } from "./OpeningHours" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import Constants from "../../Models/Constants" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
|  | @ -30,48 +29,20 @@ export default class OpeningHoursVisualization extends Toggle { | ||||||
|         prefix = "", |         prefix = "", | ||||||
|         postfix = "" |         postfix = "" | ||||||
|     ) { |     ) { | ||||||
|         const country = tags.map((tags) => tags._country) |         const openingHoursStore = OH.CreateOhObjectStore(tags, key, prefix, postfix) | ||||||
|         const ohTable = new VariableUiElement( |         const ohTable = new VariableUiElement( | ||||||
|             tags |             openingHoursStore.map((opening_hours_obj) => { | ||||||
|                 .map((tags) => { |                 if (opening_hours_obj === undefined) { | ||||||
|                     const value: string = tags[key] |                     return new FixedUiElement("No opening hours defined with key " + key).SetClass( | ||||||
|                     if (value === undefined) { |                         "alert" | ||||||
|                         return undefined |                     ) | ||||||
|                     } |                 } | ||||||
|                     if (value.startsWith(prefix) && value.endsWith(postfix)) { | 
 | ||||||
|                         return value.substring(prefix.length, value.length - postfix.length).trim() |                 if (opening_hours_obj === "error") { | ||||||
|                     } |                     return Translations.t.general.opening_hours.error_loading | ||||||
|                     return value |                 } | ||||||
|                 }) // This mapping will absorb all other changes to tags in order to prevent regeneration
 |                 return OpeningHoursVisualization.CreateFullVisualisation(opening_hours_obj) | ||||||
|                 .map( |             }) | ||||||
|                     (ohtext) => { |  | ||||||
|                         if (ohtext === undefined) { |  | ||||||
|                             return new FixedUiElement( |  | ||||||
|                                 "No opening hours defined with key " + key |  | ||||||
|                             ).SetClass("alert") |  | ||||||
|                         } |  | ||||||
|                         try { |  | ||||||
|                             return OpeningHoursVisualization.CreateFullVisualisation( |  | ||||||
|                                 OH.CreateOhObject(<any>tags.data, ohtext) |  | ||||||
|                             ) |  | ||||||
|                         } catch (e) { |  | ||||||
|                             console.warn(e, e.stack) |  | ||||||
|                             return new Combine([ |  | ||||||
|                                 Translations.t.general.opening_hours.error_loading, |  | ||||||
|                                 new Toggle( |  | ||||||
|                                     new FixedUiElement(e).SetClass("subtle"), |  | ||||||
|                                     undefined, |  | ||||||
|                                     state?.osmConnection?.userDetails.map( |  | ||||||
|                                         (userdetails) => |  | ||||||
|                                             userdetails.csCount >= |  | ||||||
|                                             Constants.userJourney.tagsVisibleAndWikiLinked |  | ||||||
|                                     ) |  | ||||||
|                                 ), |  | ||||||
|                             ]) |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     [country] |  | ||||||
|                 ) |  | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         super( |         super( | ||||||
|  |  | ||||||
|  | @ -161,7 +161,7 @@ | ||||||
|       2. What do we want to add? |       2. What do we want to add? | ||||||
|       3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) --> |       3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) --> | ||||||
|   <LoginButton osmConnection={state.osmConnection} slot="not-logged-in"> |   <LoginButton osmConnection={state.osmConnection} slot="not-logged-in"> | ||||||
|     <Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> |     <Tr t={Translations.t.general.add.pleaseLogin} /> | ||||||
|   </LoginButton> |   </LoginButton> | ||||||
|   <div class="h-full w-full"> |   <div class="h-full w-full"> | ||||||
|     {#if $zoom < Constants.minZoomLevelToAddNewPoint} |     {#if $zoom < Constants.minZoomLevelToAddNewPoint} | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
|     ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? []) |     ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? []) | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|   const allTags = tags.map((tags) => { |   const allTags = tags.mapD((tags) => { | ||||||
|     const parts: (string | BaseUIElement)[][] = [] |     const parts: (string | BaseUIElement)[][] = [] | ||||||
|     for (const key in tags) { |     for (const key in tags) { | ||||||
|       let v = tags[key] |       let v = tags[key] | ||||||
|  |  | ||||||
|  | @ -31,14 +31,16 @@ export class ExportAsGpxViz implements SpecialVisualization { | ||||||
|                 t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), |                 t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), | ||||||
|                 t.downloadGpxHelper.SetClass("subtle"), |                 t.downloadGpxHelper.SetClass("subtle"), | ||||||
|             ]).SetClass("flex flex-col") |             ]).SetClass("flex flex-col") | ||||||
|         ).onClick(() => { |         ) | ||||||
|             console.log("Exporting as GPX!") |             .SetClass("w-full") | ||||||
|             const tags = tagSource.data |             .onClick(() => { | ||||||
|             const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" |                 console.log("Exporting as GPX!") | ||||||
|             const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title) |                 const tags = tagSource.data | ||||||
|             Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { |                 const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" | ||||||
|                 mimetype: "{gpx=application/gpx+xml}", |                 const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title) | ||||||
|  |                 Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { | ||||||
|  |                     mimetype: "{gpx=application/gpx+xml}", | ||||||
|  |                 }) | ||||||
|             }) |             }) | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,7 +28,6 @@ | ||||||
| 
 | 
 | ||||||
|   const t = Translations.t.image.nearby |   const t = Translations.t.image.nearby | ||||||
|   const c = [lon, lat] |   const c = [lon, lat] | ||||||
|   console.log(">>>", image) |  | ||||||
|   let attributedImage = new AttributedImage({ |   let attributedImage = new AttributedImage({ | ||||||
|     url: image.thumbUrl ?? image.pictureUrl, |     url: image.thumbUrl ?? image.pictureUrl, | ||||||
|     provider: AllImageProviders.byName(image.provider), |     provider: AllImageProviders.byName(image.provider), | ||||||
|  | @ -45,7 +44,7 @@ | ||||||
|     const url = image.osmTags[key] |     const url = image.osmTags[key] | ||||||
|     if (isLinked) { |     if (isLinked) { | ||||||
|       const action = new LinkImageAction(currentTags.id, key, url, tags, { |       const action = new LinkImageAction(currentTags.id, key, url, tags, { | ||||||
|         theme: state.layout.id, |         theme: tags.data._orig_theme ??  state.layout.id, | ||||||
|         changeType: "link-image", |         changeType: "link-image", | ||||||
|       }) |       }) | ||||||
|       state.changes.applyAction(action) |       state.changes.applyAction(action) | ||||||
|  | @ -54,7 +53,7 @@ | ||||||
|         const v = currentTags[k] |         const v = currentTags[k] | ||||||
|         if (v === url) { |         if (v === url) { | ||||||
|           const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { |           const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { | ||||||
|             theme: state.layout.id, |             theme: tags.data._orig_theme ?? state.layout.id, | ||||||
|             changeType: "remove-image", |             changeType: "remove-image", | ||||||
|           }) |           }) | ||||||
|           state.changes.applyAction(action) |           state.changes.applyAction(action) | ||||||
|  |  | ||||||
							
								
								
									
										48
									
								
								src/UI/Popup/MarkAsFavourite.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/UI/Popup/MarkAsFavourite.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  |   import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid"; | ||||||
|  |   import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"; | ||||||
|  |   import Tr from "../Base/Tr.svelte"; | ||||||
|  |   import Translations from "../i18n/Translations"; | ||||||
|  |   import LoginToggle from "../Base/LoginToggle.svelte"; | ||||||
|  |   import type { Feature } from "geojson"; | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A full-blown 'mark as favourite'-button | ||||||
|  |    */ | ||||||
|  |   export let state: SpecialVisualizationState; | ||||||
|  |   export let feature: Feature | ||||||
|  |   export let tags: Record<string, string>; | ||||||
|  |   export let layer: LayerConfig | ||||||
|  |   let isFavourite = tags?.map(tags => tags._favourite === "yes"); | ||||||
|  |   const t = Translations.t.favouritePoi; | ||||||
|  | 
 | ||||||
|  |   function markFavourite(isFavourite: boolean) { | ||||||
|  |     state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <LoginToggle ignoreLoading={true} {state}> | ||||||
|  | {#if $isFavourite}   | ||||||
|  |   <div class="flex h-fit items-start"> | ||||||
|  |     <HeartSolidIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(false)} /> | ||||||
|  |     <div class="flex flex-col w-full"> | ||||||
|  |       <button class="flex flex-col items-start" on:click={() => markFavourite(false)}> | ||||||
|  |         <Tr t={t.button.unmark} /> | ||||||
|  |         <Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted}/> | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |     <Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} /> | ||||||
|  | {:else} | ||||||
|  |   <div class="flex items-start"> | ||||||
|  |     <HeartOutlineIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(true)} /> | ||||||
|  |       <button class="flex w-full flex-col items-start" on:click={() => markFavourite(true)}> | ||||||
|  |         <Tr t={t.button.markAsFavouriteTitle} /> | ||||||
|  |         <Tr cls="normal-font subtle" t={t.button.markDescription}/> | ||||||
|  |       </button> | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  | </LoginToggle> | ||||||
							
								
								
									
										36
									
								
								src/UI/Popup/MarkAsFavouriteMini.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/UI/Popup/MarkAsFavouriteMini.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  |   import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid"; | ||||||
|  |   import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"; | ||||||
|  |   import Translations from "../i18n/Translations"; | ||||||
|  |   import LoginToggle from "../Base/LoginToggle.svelte"; | ||||||
|  |   import type { Feature } from "geojson"; | ||||||
|  |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A small 'mark as favourite'-button to serve as title-icon | ||||||
|  |    */ | ||||||
|  |   export let state: SpecialVisualizationState; | ||||||
|  |   export let feature: Feature; | ||||||
|  |   export let tags: Record<string, string>; | ||||||
|  |   export let layer: LayerConfig; | ||||||
|  |   let isFavourite = tags?.map(tags => tags._favourite === "yes"); | ||||||
|  |   const t = Translations.t.favouritePoi; | ||||||
|  | 
 | ||||||
|  |   function markFavourite(isFavourite: boolean) { | ||||||
|  |     state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <LoginToggle ignoreLoading={true} {state}> | ||||||
|  |   {#if $isFavourite} | ||||||
|  |     <button class="p-0 m-0 h-8 w-8" on:click={() => markFavourite(false)}> | ||||||
|  |       <HeartSolidIcon/> | ||||||
|  |     </button> | ||||||
|  |   {:else} | ||||||
|  |     <button class="p-0 m-0 h-8 w-8 no-image-background"  on:click={() => markFavourite(true)} > | ||||||
|  |       <HeartOutlineIcon/> | ||||||
|  |     </button> | ||||||
|  |   {/if} | ||||||
|  | </LoginToggle> | ||||||
|  | @ -3,16 +3,15 @@ | ||||||
|    * Shows all questions for which the answers are unknown. |    * Shows all questions for which the answers are unknown. | ||||||
|    * The questions can either be shown all at once or one at a time (in which case they can be skipped) |    * The questions can either be shown all at once or one at a time (in which case they can be skipped) | ||||||
|    */ |    */ | ||||||
|   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" |   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" |   import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||||
|   import type { Feature } from "geojson" |   import type { Feature } from "geojson"; | ||||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization" |   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||||
|   import If from "../../Base/If.svelte" |   import TagRenderingQuestion from "./TagRenderingQuestion.svelte"; | ||||||
|   import TagRenderingQuestion from "./TagRenderingQuestion.svelte" |   import Tr from "../../Base/Tr.svelte"; | ||||||
|   import Tr from "../../Base/Tr.svelte" |   import Translations from "../../i18n/Translations.js"; | ||||||
|   import Translations from "../../i18n/Translations.js" |   import { Utils } from "../../../Utils"; | ||||||
|   import { Utils } from "../../../Utils" |  | ||||||
| 
 | 
 | ||||||
|   export let layer: LayerConfig |   export let layer: LayerConfig | ||||||
|   export let tags: UIEventSource<Record<string, string>> |   export let tags: UIEventSource<Record<string, string>> | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ | ||||||
|   onDestroy( |   onDestroy( | ||||||
|     tags.addCallbackAndRun((tags) => { |     tags.addCallbackAndRun((tags) => { | ||||||
|       _tags = tags |       _tags = tags | ||||||
|  |       console.log("Getting render value for", _tags,config) | ||||||
|       trs = Utils.NoNull(config?.GetRenderValues(_tags)) |       trs = Utils.NoNull(config?.GetRenderValues(_tags)) | ||||||
|     }) |     }) | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
|   import Translations from "../../i18n/Translations.js" |   import Translations from "../../i18n/Translations.js" | ||||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
|   import { Utils } from "../../../Utils" |   import { Utils } from "../../../Utils" | ||||||
| 
 |   import { twMerge } from "tailwind-merge" | ||||||
|   export let config: TagRenderingConfig |   export let config: TagRenderingConfig | ||||||
|   export let tags: UIEventSource<Record<string, string>> |   export let tags: UIEventSource<Record<string, string>> | ||||||
|   export let selectedElement: Feature | undefined |   export let selectedElement: Feature | undefined | ||||||
|  | @ -71,7 +71,7 @@ | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div bind:this={htmlElem} class={clss}> | <div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}> | ||||||
|   {#if config.question && (!editingEnabled || $editingEnabled)} |   {#if config.question && (!editingEnabled || $editingEnabled)} | ||||||
|     {#if editMode} |     {#if editMode} | ||||||
|       <TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}> |       <TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}> | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" |   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
|   import { twJoin } from "tailwind-merge" |   import { twJoin } from "tailwind-merge" | ||||||
|  |   import Icon from "../../Map/Icon.svelte"; | ||||||
| 
 | 
 | ||||||
|   export let selectedElement: Feature |   export let selectedElement: Feature | ||||||
|   export let tags: UIEventSource<Record<string, string>> |   export let tags: UIEventSource<Record<string, string>> | ||||||
|  | @ -27,13 +28,8 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if mapping.icon !== undefined} | {#if mapping.icon !== undefined} | ||||||
|   <div class="inline-flex items-center"> |   <div class="inline-flex"> | ||||||
|     <img |     <Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}/> | ||||||
|       class={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")} |  | ||||||
|       src={mapping.icon} |  | ||||||
|       aria-hidden="true" |  | ||||||
|       alt="" |  | ||||||
|     /> |  | ||||||
|     <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} /> |     <SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} /> | ||||||
|   </div> |   </div> | ||||||
| {:else if mapping.then !== undefined} | {:else if mapping.then !== undefined} | ||||||
|  |  | ||||||
|  | @ -1,188 +1,210 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|     import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource" |   import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"; | ||||||
|     import type { SpecialVisualizationState } from "../../SpecialVisualization" |   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||||
|     import Tr from "../../Base/Tr.svelte" |   import Tr from "../../Base/Tr.svelte"; | ||||||
|     import type { Feature } from "geojson" |   import type { Feature } from "geojson"; | ||||||
|     import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig" |   import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||||
|     import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" |   import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||||
|     import { TagsFilter } from "../../../Logic/Tags/TagsFilter" |   import { TagsFilter } from "../../../Logic/Tags/TagsFilter"; | ||||||
|     import FreeformInput from "./FreeformInput.svelte" |   import FreeformInput from "./FreeformInput.svelte"; | ||||||
|     import Translations from "../../i18n/Translations.js" |   import Translations from "../../i18n/Translations.js"; | ||||||
|     import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction" |   import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
|     import { createEventDispatcher, onDestroy } from "svelte" |   import { createEventDispatcher, onDestroy } from "svelte"; | ||||||
|     import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||||
|     import SpecialTranslation from "./SpecialTranslation.svelte" |   import SpecialTranslation from "./SpecialTranslation.svelte"; | ||||||
|     import TagHint from "../TagHint.svelte" |   import TagHint from "../TagHint.svelte"; | ||||||
|     import LoginToggle from "../../Base/LoginToggle.svelte" |   import LoginToggle from "../../Base/LoginToggle.svelte"; | ||||||
|     import SubtleButton from "../../Base/SubtleButton.svelte" |   import SubtleButton from "../../Base/SubtleButton.svelte"; | ||||||
|     import Loading from "../../Base/Loading.svelte" |   import Loading from "../../Base/Loading.svelte"; | ||||||
|     import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte" |   import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"; | ||||||
|     import { Translation } from "../../i18n/Translation" |   import { Translation } from "../../i18n/Translation"; | ||||||
|     import Constants from "../../../Models/Constants" |   import Constants from "../../../Models/Constants"; | ||||||
|     import { Unit } from "../../../Models/Unit" |   import { Unit } from "../../../Models/Unit"; | ||||||
|     import UserRelatedState from "../../../Logic/State/UserRelatedState" |   import UserRelatedState from "../../../Logic/State/UserRelatedState"; | ||||||
|     import { twJoin } from "tailwind-merge" |   import { twJoin } from "tailwind-merge"; | ||||||
|     import { TagUtils } from "../../../Logic/Tags/TagUtils" |   import { TagUtils } from "../../../Logic/Tags/TagUtils"; | ||||||
|     import Search from "../../../assets/svg/Search.svelte" |   import Search from "../../../assets/svg/Search.svelte"; | ||||||
|     import Login from "../../../assets/svg/Login.svelte" |   import Login from "../../../assets/svg/Login.svelte"; | ||||||
| 
 | 
 | ||||||
|     export let config: TagRenderingConfig |   export let config: TagRenderingConfig; | ||||||
|     export let tags: UIEventSource<Record<string, string>> |   export let tags: UIEventSource<Record<string, string>>; | ||||||
|     export let selectedElement: Feature |   export let selectedElement: Feature; | ||||||
|     export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState; | ||||||
|     export let layer: LayerConfig | undefined |   export let layer: LayerConfig | undefined; | ||||||
|  |   export let selectedTags: TagsFilter = undefined; | ||||||
| 
 | 
 | ||||||
|     let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) |   let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined); | ||||||
| 
 | 
 | ||||||
|     let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) |   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||||
| 
 | 
 | ||||||
|     // Will be bound if a freeform is available |   // Will be bound if a freeform is available | ||||||
|     let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) |   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]); | ||||||
|     let selectedMapping: number = undefined |   let selectedMapping: number = undefined; | ||||||
|     let checkedMappings: boolean[] |   let checkedMappings: boolean[]; | ||||||
| 
 | 
 | ||||||
|     /** |   let mappings: Mapping[] = config?.mappings; | ||||||
|      * Prepares and fills the checkedMappings |   let searchTerm: UIEventSource<string> = new UIEventSource(""); | ||||||
|      */ | 
 | ||||||
|     function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) { |   let dispatch = createEventDispatcher<{ | ||||||
|         mappings = confg.mappings?.filter((m) => { |     saved: { | ||||||
|             if (typeof m.hideInAnswer === "boolean") { |       config: TagRenderingConfig | ||||||
|                 return !m.hideInAnswer |       applied: TagsFilter | ||||||
|             } |     } | ||||||
|             return !m.hideInAnswer.matchesProperties(tgs) |   }>(); | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Prepares and fills the checkedMappings | ||||||
|  |    */ | ||||||
|  |   function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) { | ||||||
|  |     mappings = confg.mappings?.filter((m) => { | ||||||
|  |       if (typeof m.hideInAnswer === "boolean") { | ||||||
|  |         return !m.hideInAnswer; | ||||||
|  |       } | ||||||
|  |       return !m.hideInAnswer.matchesProperties(tgs); | ||||||
|  |     }); | ||||||
|  |     // We received a new config -> reinit | ||||||
|  |     unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       confg.mappings?.length > 0 && | ||||||
|  |       confg.multiAnswer && | ||||||
|  |       (checkedMappings === undefined || | ||||||
|  |         checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) | ||||||
|  |     ) { | ||||||
|  |       const seenFreeforms = []; | ||||||
|  |       TagUtils.FlattenMultiAnswer(); | ||||||
|  |       checkedMappings = [ | ||||||
|  |         ...confg.mappings.map((mapping) => { | ||||||
|  |           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs); | ||||||
|  |           if (matches && confg.freeform) { | ||||||
|  |             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()); | ||||||
|  |             seenFreeforms.push(newProps[confg.freeform.key]); | ||||||
|  |           } | ||||||
|  |           return matches; | ||||||
|         }) |         }) | ||||||
|         // We received a new config -> reinit |       ]; | ||||||
|         unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) |  | ||||||
| 
 | 
 | ||||||
|         if ( |       if (tgs !== undefined && confg.freeform) { | ||||||
|             confg.mappings?.length > 0 && |         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []; | ||||||
|             confg.multiAnswer && |         for (const seenFreeform of seenFreeforms) { | ||||||
|             (checkedMappings === undefined || |           if (!seenFreeform) { | ||||||
|                 checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) |             continue; | ||||||
|         ) { |           } | ||||||
|             const seenFreeforms = [] |           const index = unseenFreeformValues.indexOf(seenFreeform); | ||||||
|             TagUtils.FlattenMultiAnswer() |           if (index < 0) { | ||||||
|             checkedMappings = [ |             continue; | ||||||
|                 ...confg.mappings.map((mapping) => { |           } | ||||||
|                     const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs) |           unseenFreeformValues.splice(index, 1); | ||||||
|                     if (matches && confg.freeform) { |  | ||||||
|                         const newProps = TagUtils.changeAsProperties(mapping.if.asChange()) |  | ||||||
|                         seenFreeforms.push(newProps[confg.freeform.key]) |  | ||||||
|                     } |  | ||||||
|                     return matches |  | ||||||
|                 }), |  | ||||||
|             ] |  | ||||||
| 
 |  | ||||||
|             if (tgs !== undefined && confg.freeform) { |  | ||||||
|                 const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [] |  | ||||||
|                 for (const seenFreeform of seenFreeforms) { |  | ||||||
|                     if (!seenFreeform) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     const index = unseenFreeformValues.indexOf(seenFreeform) |  | ||||||
|                     if (index < 0) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     unseenFreeformValues.splice(index, 1) |  | ||||||
|                 } |  | ||||||
|                 // TODO this has _to much_ values |  | ||||||
|                 freeformInput.setData(unseenFreeformValues.join(";")) |  | ||||||
|                 checkedMappings.push(unseenFreeformValues.length > 0) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|         if (confg.freeform?.key) { |         // TODO this has _to much_ values | ||||||
|             if (!confg.multiAnswer) { |         freeformInput.setData(unseenFreeformValues.join(";")); | ||||||
|                 // Somehow, setting multi-answer freeform values is broken if this is not set |         checkedMappings.push(unseenFreeformValues.length > 0); | ||||||
|                 freeformInput.setData(tgs[confg.freeform.key]) |       } | ||||||
|             } |     } | ||||||
|  |     if (confg.freeform?.key) { | ||||||
|  |       if (!confg.multiAnswer) { | ||||||
|  |         // Somehow, setting multi-answer freeform values is broken if this is not set | ||||||
|  |         freeformInput.setData(tgs[confg.freeform.key]); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     } else { | ||||||
|  |       freeformInput.setData(undefined); | ||||||
|  |     } | ||||||
|  |     feedback.setData(undefined); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   $: { | ||||||
|  |     // Even though 'config' is not declared as a store, Svelte uses it as one to update the component | ||||||
|  |     // We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes | ||||||
|  |     initialize($tags, config); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   $: { | ||||||
|  |     try { | ||||||
|  |       selectedTags = config?.constructChangeSpecification( | ||||||
|  |         $freeformInput, | ||||||
|  |         selectedMapping, | ||||||
|  |         checkedMappings, | ||||||
|  |         tags.data | ||||||
|  |       ); | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error("Could not calculate changeSpecification:", e); | ||||||
|  |       selectedTags = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function onSave() { | ||||||
|  |     if (selectedTags === undefined) { | ||||||
|  |       console.log("SelectedTags is undefined, ignoring 'onSave'-event"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) { | ||||||
|  |       /** | ||||||
|  |        * This is a special, priviliged layer. | ||||||
|  |        * We simply apply the tags onto the records | ||||||
|  |        */ | ||||||
|  |       const kv = selectedTags.asChange(tags.data); | ||||||
|  |       for (const { k, v } of kv) { | ||||||
|  |         if (v === undefined || v === "") { | ||||||
|  |           delete tags.data[k]; | ||||||
|         } else { |         } else { | ||||||
|             freeformInput.setData(undefined) |           freeformInput.setData(undefined); | ||||||
|         } |         } | ||||||
|         feedback.setData(undefined) |         feedback.setData(undefined); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     $: { |     dispatch("saved", { config, applied: selectedTags }); | ||||||
|         // Even though 'config' is not declared as a store, Svelte uses it as one to update the component |     const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, { | ||||||
|         // We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes |       theme: tags.data["_orig_theme"] ?? state.layout.id, | ||||||
|         initialize($tags, config) |       changeType: "answer" | ||||||
|  |     }); | ||||||
|  |     freeformInput.setData(undefined); | ||||||
|  |     selectedMapping = undefined; | ||||||
|  |     selectedTags = undefined; | ||||||
|  | 
 | ||||||
|  |     change | ||||||
|  |       .CreateChangeDescriptions() | ||||||
|  |       .then((changes) => state.changes.applyChanges(changes)) | ||||||
|  |       .catch(console.error); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function onInputKeypress(e: Event) { | ||||||
|  |     if (e.key === "Enter") { | ||||||
|  |       onSave(); | ||||||
|     } |     } | ||||||
|     export let selectedTags: TagsFilter = undefined |   } | ||||||
| 
 | 
 | ||||||
|     let mappings: Mapping[] = config?.mappings |   $: { | ||||||
|     let searchTerm: UIEventSource<string> = new UIEventSource("") |     try { | ||||||
| 
 |       selectedTags = config?.constructChangeSpecification( | ||||||
|     $: { |         $freeformInput, | ||||||
|         try { |         selectedMapping, | ||||||
|             selectedTags = config?.constructChangeSpecification( |         checkedMappings, | ||||||
|                 $freeformInput, |         tags.data | ||||||
|                 selectedMapping, |       ); | ||||||
|                 checkedMappings, |     } catch (e) { | ||||||
|                 tags.data, |       console.error("Could not calculate changeSpecification:", e); | ||||||
|             ) |       selectedTags = undefined; | ||||||
|         } catch (e) { |  | ||||||
|             console.error("Could not calculate changeSpecification:", e) |  | ||||||
|             selectedTags = undefined |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     let dispatch = createEventDispatcher<{ |  | ||||||
|         saved: { |  | ||||||
|             config: TagRenderingConfig |  | ||||||
|             applied: TagsFilter |  | ||||||
|         } |  | ||||||
|     }>() |  | ||||||
| 
 | 
 | ||||||
|     function onSave() { |   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false); | ||||||
|         if (selectedTags === undefined) { |   let featureSwitchIsDebugging = | ||||||
|             console.log("SelectedTags is undefined, ignoring 'onSave'-event") |     state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); | ||||||
|             return |   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined); | ||||||
|         } |   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0; | ||||||
|         if (layer === undefined || layer?.source === null) { |   let question = config.question; | ||||||
|             /** |   $: question = config.question; | ||||||
|              * This is a special, priviliged layer. |   if (state?.osmConnection) { | ||||||
|              * We simply apply the tags onto the records |     onDestroy( | ||||||
|              */ |       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||||
|             const kv = selectedTags.asChange(tags.data) |         numberOfCs = ud.csCount; | ||||||
|             for (const { k, v } of kv) { |       }) | ||||||
|                 if (v === undefined || v === "") { |     ); | ||||||
|                     delete tags.data[k] |   } | ||||||
|                 } else { |  | ||||||
|                     tags.data[k] = v |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             tags.ping() |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         dispatch("saved", { config, applied: selectedTags }) |  | ||||||
|         const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, { |  | ||||||
|             theme: state.layout.id, |  | ||||||
|             changeType: "answer", |  | ||||||
|         }) |  | ||||||
|         freeformInput.setData(undefined) |  | ||||||
|         selectedMapping = undefined |  | ||||||
|         selectedTags = undefined |  | ||||||
| 
 |  | ||||||
|         change |  | ||||||
|             .CreateChangeDescriptions() |  | ||||||
|             .then((changes) => state.changes.applyChanges(changes)) |  | ||||||
|             .catch(console.error) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false) |  | ||||||
|     let featureSwitchIsDebugging = |  | ||||||
|         state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) |  | ||||||
|     let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined) |  | ||||||
|     let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0 |  | ||||||
|     let question = config.question |  | ||||||
|     $: question = config.question |  | ||||||
|     if (state?.osmConnection) { |  | ||||||
|         onDestroy( |  | ||||||
|             state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { |  | ||||||
|                 numberOfCs = ud.csCount |  | ||||||
|             }), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if question !== undefined} | {#if question !== undefined} | ||||||
|  | @ -246,9 +268,8 @@ | ||||||
|               bind:group={selectedMapping} |               bind:group={selectedMapping} | ||||||
|               name={"mappings-radio-" + config.id} |               name={"mappings-radio-" + config.id} | ||||||
|               value={i} |               value={i} | ||||||
|               on:keypress={(e) => { |               on:keypress={e => onInputKeypress(e)} | ||||||
|                 if (e.key === "Enter") onSave() | 
 | ||||||
|               }} |  | ||||||
|             /> |             /> | ||||||
|           </TagRenderingMappingInput> |           </TagRenderingMappingInput> | ||||||
|         {/each} |         {/each} | ||||||
|  | @ -259,6 +280,7 @@ | ||||||
|               bind:group={selectedMapping} |               bind:group={selectedMapping} | ||||||
|               name={"mappings-radio-" + config.id} |               name={"mappings-radio-" + config.id} | ||||||
|               value={config.mappings?.length} |               value={config.mappings?.length} | ||||||
|  |               on:keypress={e => onInputKeypress(e)} | ||||||
|             /> |             /> | ||||||
|             <FreeformInput |             <FreeformInput | ||||||
|               {config} |               {config} | ||||||
|  | @ -290,6 +312,7 @@ | ||||||
|               type="checkbox" |               type="checkbox" | ||||||
|               name={"mappings-checkbox-" + config.id + "-" + i} |               name={"mappings-checkbox-" + config.id + "-" + i} | ||||||
|               bind:checked={checkedMappings[i]} |               bind:checked={checkedMappings[i]} | ||||||
|  |               on:keypress={e => onInputKeypress(e)} | ||||||
|             /> |             /> | ||||||
|           </TagRenderingMappingInput> |           </TagRenderingMappingInput> | ||||||
|         {/each} |         {/each} | ||||||
|  | @ -299,6 +322,7 @@ | ||||||
|               type="checkbox" |               type="checkbox" | ||||||
|               name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} |               name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} | ||||||
|               bind:checked={checkedMappings[config.mappings.length]} |               bind:checked={checkedMappings[config.mappings.length]} | ||||||
|  |               on:keypress={e => onInputKeypress(e)} | ||||||
|             /> |             /> | ||||||
|             <FreeformInput |             <FreeformInput | ||||||
|               {config} |               {config} | ||||||
|  | @ -307,7 +331,6 @@ | ||||||
|               {unit} |               {unit} | ||||||
|               feature={selectedElement} |               feature={selectedElement} | ||||||
|               value={freeformInput} |               value={freeformInput} | ||||||
|               on:selected={() => (checkedMappings[config.mappings.length] = true)} |  | ||||||
|               on:submit={onSave} |               on:submit={onSave} | ||||||
|             /> |             /> | ||||||
|           </label> |           </label> | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" | ||||||
| import { RasterLayerPolygon } from "../Models/RasterLayers" | import { RasterLayerPolygon } from "../Models/RasterLayers" | ||||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" | ||||||
| import { OsmTags } from "../Models/OsmFeature" | import { OsmTags } from "../Models/OsmFeature" | ||||||
|  | import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The state needed to render a special Visualisation. |  * The state needed to render a special Visualisation. | ||||||
|  | @ -33,7 +34,6 @@ export interface SpecialVisualizationState { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     readonly indexedFeatures: IndexedFeatureSource |     readonly indexedFeatures: IndexedFeatureSource | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Some features will create a new element that should be displayed. |      * Some features will create a new element that should be displayed. | ||||||
|      * These can be injected by appending them to this featuresource (and pinging it) |      * These can be injected by appending them to this featuresource (and pinging it) | ||||||
|  | @ -59,6 +59,8 @@ export interface SpecialVisualizationState { | ||||||
|     readonly selectedLayer: UIEventSource<LayerConfig> |     readonly selectedLayer: UIEventSource<LayerConfig> | ||||||
|     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> |     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> | ||||||
| 
 | 
 | ||||||
|  |     readonly favourites: FavouritesFeatureSource | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * If data is currently being fetched from external sources |      * If data is currently being fetched from external sources | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -79,6 +79,9 @@ import ThemeViewState from "../Models/ThemeViewState" | ||||||
| import LanguagePicker from "./InputElement/LanguagePicker.svelte" | import LanguagePicker from "./InputElement/LanguagePicker.svelte" | ||||||
| import LogoutButton from "./Base/LogoutButton.svelte" | import LogoutButton from "./Base/LogoutButton.svelte" | ||||||
| import OpenJosm from "./Base/OpenJosm.svelte" | import OpenJosm from "./Base/OpenJosm.svelte" | ||||||
|  | import MarkAsFavourite from "./Popup/MarkAsFavourite.svelte" | ||||||
|  | import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte" | ||||||
|  | import NextChangeViz from "./OpeningHours/NextChangeViz.svelte" | ||||||
| 
 | 
 | ||||||
| class NearbyImageVis implements SpecialVisualization { | class NearbyImageVis implements SpecialVisualization { | ||||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 |     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||||
|  | @ -532,6 +535,9 @@ export default class SpecialVisualizations { | ||||||
|                     feature: Feature, |                     feature: Feature, | ||||||
|                     layer: LayerConfig |                     layer: LayerConfig | ||||||
|                 ): BaseUIElement { |                 ): BaseUIElement { | ||||||
|  |                     if (!layer.deletion) { | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
|                     return new SvelteUIElement(DeleteWizard, { |                     return new SvelteUIElement(DeleteWizard, { | ||||||
|                         tags: tagSource, |                         tags: tagSource, | ||||||
|                         deleteConfig: layer.deletion, |                         deleteConfig: layer.deletion, | ||||||
|  | @ -822,6 +828,46 @@ export default class SpecialVisualizations { | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "opening_hours_state", | ||||||
|  |                 docs: "A small element, showing if the POI is currently open and when the next change is", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "key", | ||||||
|  |                         defaultValue: "opening_hours", | ||||||
|  |                         doc: "The tagkey from which the opening hours are read.", | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "prefix", | ||||||
|  |                         defaultValue: "", | ||||||
|  |                         doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__", | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "postfix", | ||||||
|  |                         defaultValue: "", | ||||||
|  |                         doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |                 needsUrls: [], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tags: UIEventSource<Record<string, string>>, | ||||||
|  |                     args: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): BaseUIElement { | ||||||
|  |                     const keyToUse = args[0] | ||||||
|  |                     const prefix = args[1] | ||||||
|  |                     const postfix = args[2] | ||||||
|  |                     return new SvelteUIElement(NextChangeViz, { | ||||||
|  |                         state, | ||||||
|  |                         keyToUse, | ||||||
|  |                         tags, | ||||||
|  |                         prefix, | ||||||
|  |                         postfix, | ||||||
|  |                     }) | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|             { |             { | ||||||
|                 funcName: "canonical", |                 funcName: "canonical", | ||||||
|                 needsUrls: [], |                 needsUrls: [], | ||||||
|  | @ -872,20 +918,22 @@ export default class SpecialVisualizations { | ||||||
|                             t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), |                             t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), | ||||||
|                             t.downloadGeoJsonHelper.SetClass("subtle"), |                             t.downloadGeoJsonHelper.SetClass("subtle"), | ||||||
|                         ]).SetClass("flex flex-col") |                         ]).SetClass("flex flex-col") | ||||||
|                     ).onClick(() => { |                     ) | ||||||
|                         console.log("Exporting as Geojson") |                         .onClick(() => { | ||||||
|                         const tags = tagSource.data |                             console.log("Exporting as Geojson") | ||||||
|                         const title = |                             const tags = tagSource.data | ||||||
|                             layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" |                             const title = | ||||||
|                         const data = JSON.stringify(feature, null, "  ") |                                 layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" | ||||||
|                         Utils.offerContentsAsDownloadableFile( |                             const data = JSON.stringify(feature, null, "  ") | ||||||
|                             data, |                             Utils.offerContentsAsDownloadableFile( | ||||||
|                             title + "_mapcomplete_export.geojson", |                                 data, | ||||||
|                             { |                                 title + "_mapcomplete_export.geojson", | ||||||
|                                 mimetype: "application/vnd.geo+json", |                                 { | ||||||
|                             } |                                     mimetype: "application/vnd.geo+json", | ||||||
|                         ) |                                 } | ||||||
|                     }) |                             ) | ||||||
|  |                         }) | ||||||
|  |                         .SetClass("w-full") | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|  | @ -1482,7 +1530,7 @@ export default class SpecialVisualizations { | ||||||
|                     const tags = (<ThemeViewState>( |                     const tags = (<ThemeViewState>( | ||||||
|                         state |                         state | ||||||
|                     )).geolocation.currentUserLocation.features.map( |                     )).geolocation.currentUserLocation.features.map( | ||||||
|                         (features) => features[0].properties |                         (features) => features[0]?.properties | ||||||
|                     ) |                     ) | ||||||
|                     return new SvelteUIElement(AllTagsPanel, { |                     return new SvelteUIElement(AllTagsPanel, { | ||||||
|                         state, |                         state, | ||||||
|  | @ -1490,6 +1538,46 @@ export default class SpecialVisualizations { | ||||||
|                     }) |                     }) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "favourite_status", | ||||||
|  |                 needsUrls: [], | ||||||
|  |                 docs: "A button that allows a (logged in) contributor to mark a location as a favourite location", | ||||||
|  |                 args: [], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                     argument: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): BaseUIElement { | ||||||
|  |                     return new SvelteUIElement(MarkAsFavourite, { | ||||||
|  |                         tags: tagSource, | ||||||
|  |                         state, | ||||||
|  |                         layer, | ||||||
|  |                         feature, | ||||||
|  |                     }) | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "favourite_icon", | ||||||
|  |                 needsUrls: [], | ||||||
|  |                 docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon", | ||||||
|  |                 args: [], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                     argument: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): BaseUIElement { | ||||||
|  |                     return new SvelteUIElement(MarkAsFavouriteMini, { | ||||||
|  |                         tags: tagSource, | ||||||
|  |                         state, | ||||||
|  |                         layer, | ||||||
|  |                         feature, | ||||||
|  |                     }) | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         specialVisualizations.push(new AutoApplyButton(specialVisualizations)) |         specialVisualizations.push(new AutoApplyButton(specialVisualizations)) | ||||||
|  |  | ||||||
|  | @ -1,30 +1,30 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|     import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|     import { OsmConnection } from "../../Logic/Osm/OsmConnection" |   import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||||
|     import Marker from "../Map/Marker.svelte" |   import Marker from "../Map/Marker.svelte"; | ||||||
|     import NextButton from "../Base/NextButton.svelte" |   import NextButton from "../Base/NextButton.svelte"; | ||||||
|     import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" |   import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"; | ||||||
|     import { AllSharedLayers } from "../../Customizations/AllSharedLayers" |   import { AllSharedLayers } from "../../Customizations/AllSharedLayers"; | ||||||
|     import { createEventDispatcher } from "svelte" |   import { createEventDispatcher } from "svelte"; | ||||||
| 
 | 
 | ||||||
|     export let info: { id: string; owner: number } |   export let info: { id: string; owner: number }; | ||||||
|     export let category: "layers" | "themes" |   export let category: "layers" | "themes"; | ||||||
|     export let osmConnection: OsmConnection |   export let osmConnection: OsmConnection; | ||||||
|  |   const dispatch = createEventDispatcher<{ layerSelected: string }>(); | ||||||
| 
 | 
 | ||||||
|     let displayName = UIEventSource.FromPromise( |   let displayName = UIEventSource.FromPromise( | ||||||
|         osmConnection.getInformationAboutUser(info.owner), |     osmConnection.getInformationAboutUser(info.owner) | ||||||
|     ).mapD((response) => response.display_name) |   ).mapD((response) => response.display_name); | ||||||
|  |   let selfId = osmConnection.userDetails.mapD((ud) => ud.uid); | ||||||
| 
 | 
 | ||||||
|     let selfId = osmConnection.userDetails.mapD((ud) => ud.uid) |  | ||||||
| 
 | 
 | ||||||
|     function fetchIconDescription(layerId): any { |   function fetchIconDescription(layerId): any { | ||||||
|         if (category === "themes") { |     if (category === "themes") { | ||||||
|             return AllKnownLayouts.allKnownLayouts.get(layerId).icon |       return AllKnownLayouts.allKnownLayouts.get(layerId).icon; | ||||||
|         } |  | ||||||
|         return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon |  | ||||||
|     } |     } | ||||||
|  |     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     const dispatch = createEventDispatcher<{ layerSelected: string }>() |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <NextButton clss="small" on:click={() => dispatch("layerSelected", info)}> | <NextButton clss="small" on:click={() => dispatch("layerSelected", info)}> | ||||||
|  |  | ||||||
|  | @ -1,90 +1,91 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource" |   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||||
|   import { Map as MlMap } from "maplibre-gl" |   import { Map as MlMap } from "maplibre-gl"; | ||||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte" |   import MaplibreMap from "./Map/MaplibreMap.svelte"; | ||||||
|   import FeatureSwitchState from "../Logic/State/FeatureSwitchState" |   import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||||
|   import MapControlButton from "./Base/MapControlButton.svelte" |   import MapControlButton from "./Base/MapControlButton.svelte"; | ||||||
|   import ToSvelte from "./Base/ToSvelte.svelte" |   import ToSvelte from "./Base/ToSvelte.svelte"; | ||||||
|   import If from "./Base/If.svelte" |   import If from "./Base/If.svelte"; | ||||||
|   import { GeolocationControl } from "./BigComponents/GeolocationControl" |   import { GeolocationControl } from "./BigComponents/GeolocationControl"; | ||||||
|   import type { Feature } from "geojson" |   import type { Feature } from "geojson"; | ||||||
|   import SelectedElementView from "./BigComponents/SelectedElementView.svelte" |   import SelectedElementView from "./BigComponents/SelectedElementView.svelte"; | ||||||
|   import LayerConfig from "../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
|   import Filterview from "./BigComponents/Filterview.svelte" |   import Filterview from "./BigComponents/Filterview.svelte"; | ||||||
|   import ThemeViewState from "../Models/ThemeViewState" |   import ThemeViewState from "../Models/ThemeViewState"; | ||||||
|   import type { MapProperties } from "../Models/MapProperties" |   import type { MapProperties } from "../Models/MapProperties"; | ||||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" |   import Geosearch from "./BigComponents/Geosearch.svelte"; | ||||||
|   import Translations from "./i18n/Translations" |   import Translations from "./i18n/Translations"; | ||||||
|   import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" |   import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|   import Tr from "./Base/Tr.svelte" |   import Tr from "./Base/Tr.svelte"; | ||||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" |   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; | ||||||
|   import FloatOver from "./Base/FloatOver.svelte" |   import FloatOver from "./Base/FloatOver.svelte"; | ||||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy" |   import PrivacyPolicy from "./BigComponents/PrivacyPolicy"; | ||||||
|   import Constants from "../Models/Constants" |   import Constants from "../Models/Constants"; | ||||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte" |   import TabbedGroup from "./Base/TabbedGroup.svelte"; | ||||||
|   import UserRelatedState from "../Logic/State/UserRelatedState" |   import UserRelatedState from "../Logic/State/UserRelatedState"; | ||||||
|   import LoginToggle from "./Base/LoginToggle.svelte" |   import LoginToggle from "./Base/LoginToggle.svelte"; | ||||||
|   import LoginButton from "./Base/LoginButton.svelte" |   import LoginButton from "./Base/LoginButton.svelte"; | ||||||
|   import CopyrightPanel from "./BigComponents/CopyrightPanel" |   import CopyrightPanel from "./BigComponents/CopyrightPanel"; | ||||||
|   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" |   import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"; | ||||||
|   import ModalRight from "./Base/ModalRight.svelte" |   import ModalRight from "./Base/ModalRight.svelte"; | ||||||
|   import { Utils } from "../Utils" |   import { Utils } from "../Utils"; | ||||||
|   import Hotkeys from "./Base/Hotkeys" |   import Hotkeys from "./Base/Hotkeys"; | ||||||
|   import { VariableUiElement } from "./Base/VariableUIElement" |   import { VariableUiElement } from "./Base/VariableUIElement"; | ||||||
|   import SvelteUIElement from "./Base/SvelteUIElement" |   import SvelteUIElement from "./Base/SvelteUIElement"; | ||||||
|   import OverlayToggle from "./BigComponents/OverlayToggle.svelte" |   import OverlayToggle from "./BigComponents/OverlayToggle.svelte"; | ||||||
|   import LevelSelector from "./BigComponents/LevelSelector.svelte" |   import LevelSelector from "./BigComponents/LevelSelector.svelte"; | ||||||
|   import ExtraLinkButton from "./BigComponents/ExtraLinkButton" |   import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; | ||||||
|   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" |   import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"; | ||||||
|   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" |   import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"; | ||||||
|   import type { RasterLayerPolygon } from "../Models/RasterLayers" |   import type { RasterLayerPolygon } from "../Models/RasterLayers"; | ||||||
|   import { AvailableRasterLayers } from "../Models/RasterLayers" |   import { AvailableRasterLayers } from "../Models/RasterLayers"; | ||||||
|   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" |   import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"; | ||||||
|   import IfHidden from "./Base/IfHidden.svelte" |   import IfHidden from "./Base/IfHidden.svelte"; | ||||||
|   import { onDestroy } from "svelte" |   import { onDestroy } from "svelte"; | ||||||
|   import MapillaryLink from "./BigComponents/MapillaryLink.svelte" |   import MapillaryLink from "./BigComponents/MapillaryLink.svelte"; | ||||||
|   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" |   import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; | ||||||
|   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" |   import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"; | ||||||
|   import StateIndicator from "./BigComponents/StateIndicator.svelte" |   import StateIndicator from "./BigComponents/StateIndicator.svelte"; | ||||||
|   import ShareScreen from "./BigComponents/ShareScreen.svelte" |   import ShareScreen from "./BigComponents/ShareScreen.svelte"; | ||||||
|   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte" |   import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"; | ||||||
|   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" |   import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"; | ||||||
|   import Cross from "../assets/svg/Cross.svelte" |   import Cross from "../assets/svg/Cross.svelte"; | ||||||
|   import Summary from "./BigComponents/Summary.svelte" |   import Summary from "./BigComponents/Summary.svelte"; | ||||||
|   import Mastodon from "../assets/svg/Mastodon.svelte" |   import LanguagePicker from "./InputElement/LanguagePicker.svelte"; | ||||||
|   import Bug from "../assets/svg/Bug.svelte" |   import Mastodon from "../assets/svg/Mastodon.svelte"; | ||||||
|   import Liberapay from "../assets/svg/Liberapay.svelte" |   import Bug from "../assets/svg/Bug.svelte"; | ||||||
|   import Min from "../assets/svg/Min.svelte" |   import Liberapay from "../assets/svg/Liberapay.svelte"; | ||||||
|   import Plus from "../assets/svg/Plus.svelte" |   import OpenJosm from "./Base/OpenJosm.svelte"; | ||||||
|   import Filter from "../assets/svg/Filter.svelte" |   import Min from "../assets/svg/Min.svelte"; | ||||||
|   import Add from "../assets/svg/Add.svelte" |   import Plus from "../assets/svg/Plus.svelte"; | ||||||
|   import Statistics from "../assets/svg/Statistics.svelte" |   import Filter from "../assets/svg/Filter.svelte"; | ||||||
|   import Community from "../assets/svg/Community.svelte" |   import Add from "../assets/svg/Add.svelte"; | ||||||
|   import Download from "../assets/svg/Download.svelte" |   import Statistics from "../assets/svg/Statistics.svelte"; | ||||||
|   import Share from "../assets/svg/Share.svelte" |   import Community from "../assets/svg/Community.svelte"; | ||||||
|   import LanguagePicker from "./InputElement/LanguagePicker.svelte" |   import Download from "../assets/svg/Download.svelte"; | ||||||
|   import OpenJosm from "./Base/OpenJosm.svelte" |   import Share from "../assets/svg/Share.svelte"; | ||||||
|  |   import Favourites from "./Favourites/Favourites.svelte"; | ||||||
| 
 | 
 | ||||||
|     export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|     let layout = state.layout |     let layout = state.layout | ||||||
| 
 | 
 | ||||||
|     let maplibremap: UIEventSource<MlMap> = state.map |     let maplibremap: UIEventSource<MlMap> = state.map | ||||||
|     let selectedElement: UIEventSource<Feature> = state.selectedElement |     let selectedElement: UIEventSource<Feature> = state.selectedElement | ||||||
|     let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer |     let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer | ||||||
| 
 | 
 | ||||||
|     let currentZoom = state.mapProperties.zoom |   let currentZoom = state.mapProperties.zoom; | ||||||
|     let showCrosshair = state.userRelatedState.showCrosshair |   let showCrosshair = state.userRelatedState.showCrosshair; | ||||||
|     let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation |   let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation; | ||||||
|     let centerFeatures = state.closestFeatures.features |   let centerFeatures = state.closestFeatures.features; | ||||||
|     const selectedElementView = selectedElement.map( |   const selectedElementView = selectedElement.map( | ||||||
|         (selectedElement) => { |     (selectedElement) => { | ||||||
|             // Svelte doesn't properly reload some of the legacy UI-elements |       // Svelte doesn't properly reload some of the legacy UI-elements | ||||||
|             // As such, we _reconstruct_ the selectedElementView every time a new feature is selected |       // As such, we _reconstruct_ the selectedElementView every time a new feature is selected | ||||||
|             // This is a bit wasteful, but until everything is a svelte-component, this should do the trick |       // This is a bit wasteful, but until everything is a svelte-component, this should do the trick | ||||||
|             const layer = selectedLayer.data |       const layer = selectedLayer.data; | ||||||
|             if (selectedElement === undefined || layer === undefined) { |       if (selectedElement === undefined || layer === undefined) { | ||||||
|                 return undefined |         return undefined; | ||||||
|             } |       } | ||||||
| 
 | 
 | ||||||
|             if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { |             if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|  | @ -230,17 +231,15 @@ | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     {#if $arrowKeysWereUsed !== undefined} | 
 | ||||||
|       {#if $centerFeatures.length > 0} |     {#if $arrowKeysWereUsed !== undefined && $centerFeatures?.length > 0} | ||||||
|         <div class="interactive pointer-events-auto p-1"> |       <div class="pointer-events-auto interactive p-1"> | ||||||
|           {#each $centerFeatures as feat, i (feat.properties.id)} |         {#each $centerFeatures as feat, i (feat.properties.id)} | ||||||
|             <div class="flex"> |           <div class="flex"> | ||||||
|               <b>{i + 1}.</b> |           <b>{i+1}.</b><Summary {state} feature={feat}/> | ||||||
|               <Summary {state} feature={feat} /> |           </div> | ||||||
|             </div> |         {/each} | ||||||
|           {/each} |       </div> | ||||||
|         </div> |  | ||||||
|       {/if} |  | ||||||
|     {/if} |     {/if} | ||||||
|     <div class="flex flex-col items-end"> |     <div class="flex flex-col items-end"> | ||||||
|       <!-- bottom right elements --> |       <!-- bottom right elements --> | ||||||
|  | @ -495,22 +494,31 @@ | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div class="flex" slot="title2"> |       <div class="flex" slot="title2"> | ||||||
|  |         <HeartIcon class="h-6 w-6" /> | ||||||
|  |         <Tr t={Translations.t.favouritePoi.tab}/> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div class="flex flex-col m-2" slot="content2"> | ||||||
|  |         <h3> <Tr t={Translations.t.favouritePoi.title}/></h3> | ||||||
|  |         <Favourites {state}/> | ||||||
|  |       </div> | ||||||
|  |       <div class="flex" slot="title3"> | ||||||
|         <Community class="h-6 w-6" /> |         <Community class="h-6 w-6" /> | ||||||
|         <Tr t={Translations.t.communityIndex.title} /> |         <Tr t={Translations.t.communityIndex.title} /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="m-2" slot="content2"> |       <div class="m-2" slot="content3"> | ||||||
|         <CommunityIndexView location={state.mapProperties.location} /> |         <CommunityIndexView location={state.mapProperties.location} /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="flex" slot="title3"> |       <div class="flex" slot="title4"> | ||||||
|         <EyeIcon class="w-6" /> |         <EyeIcon class="w-6" /> | ||||||
|         <Tr t={Translations.t.privacy.title} /> |         <Tr t={Translations.t.privacy.title} /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="m-2" slot="content3"> |       <div class="m-2" slot="content4"> | ||||||
|         <ToSvelte construct={() => new PrivacyPolicy()} /> |         <ToSvelte construct={() => new PrivacyPolicy()} /> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <Tr slot="title4" t={Translations.t.advanced.title} /> |       <Tr slot="title5" t={Translations.t.advanced.title} /> | ||||||
|       <div class="m-2 flex flex-col" slot="content4"> |       <div class="m-2 flex flex-col" slot="content5"> | ||||||
|         <If condition={featureSwitches.featureSwitchEnableLogin}> |         <If condition={featureSwitches.featureSwitchEnableLogin}> | ||||||
|           <OpenIdEditor mapProperties={state.mapProperties} /> |           <OpenIdEditor mapProperties={state.mapProperties} /> | ||||||
|           <OpenJosm {state} /> |           <OpenJosm {state} /> | ||||||
|  |  | ||||||
|  | @ -301,10 +301,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         if (str === undefined || str === null) { |         if (str === undefined || str === null) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  |         if (typeof str !== "string") { | ||||||
|  |             console.error("Not a string:", str) | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|         if (str.length <= l) { |         if (str.length <= l) { | ||||||
|             return str |             return str | ||||||
|         } |         } | ||||||
|         return str.substr(0, l - 3) + "..." |         return str.substr(0, l - 1) + "…" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,229 +1,426 @@ | ||||||
| { | { | ||||||
|  |   "#": "Generated with generateStats.ts", | ||||||
|  |   "date": "2023-12-04T14:42:01.299Z", | ||||||
|   "keys": { |   "keys": { | ||||||
|     "addr:street": 117211930, |     "FIXME": 119237, | ||||||
|     "addr:housenumber": 125040768, |     "access": 20023328, | ||||||
|     "emergency": 1939478, |     "addr:housenumber": 146524978, | ||||||
|     "barrier": 18424246, |     "addr:street": 137485111, | ||||||
|     "tourism": 2683525, |     "advertising": 158347, | ||||||
|     "amenity": 20541353, |     "amenity": 25340913, | ||||||
|     "bench": 894256, |     "area": 1803451, | ||||||
|     "rental": 8838, |     "association": 757, | ||||||
|     "bicycle_rental": 7447, |     "barrier": 23634152, | ||||||
|     "vending": 206755, |     "bench": 1300789, | ||||||
|     "service:bicycle:rental": 3570, |     "bicycle": 7507086, | ||||||
|     "pub": 316, |     "bicycle_rental": 26948, | ||||||
|     "theme": 426, |     "boundary": 2366033, | ||||||
|     "service:bicycle:.*": 0, |     "brand": 2317628, | ||||||
|     "service:bicycle:cleaning": 807, |     "building": 585543589, | ||||||
|     "shop": 5062252, |     "camera:direction": 61201, | ||||||
|     "service:bicycle:retail": 9162, |     "climbing": 9051, | ||||||
|     "network": 2181336, |     "club": 53046, | ||||||
|     "sport": 2194801, |     "construction:amenity": 1943, | ||||||
|     "service:bicycle:repair": 11381, |     "conveying": 27311, | ||||||
|     "association": 369, |     "craft": 296376, | ||||||
|     "ngo": 42, |     "crossing": 8736722, | ||||||
|     "leisure": 7368076, |     "cyclestreet": 12505, | ||||||
|     "club": 38429, |     "cycleway": 1016837, | ||||||
|     "disused:amenity": 40880, |     "direction": 2978834, | ||||||
|     "planned:amenity": 205, |     "disused:amenity": 63413, | ||||||
|     "tileId": 0, |     "dog": 95086, | ||||||
|     "construction:amenity": 1206, |     "door": 280843, | ||||||
|     "cycleway": 906487, |     "drinking_water": 136067, | ||||||
|     "highway": 218189453, |     "emergency": 2542692, | ||||||
|     "bicycle": 6218071, |     "entrance": 3769592, | ||||||
|     "cyclestreet": 8185, |     "fixme": 1746318, | ||||||
|     "camera:direction": 40676, |     "footway": 7540651, | ||||||
|     "direction": 1896015, |     "generator:source": 2387982, | ||||||
|     "access": 16030036, |     "healthcare": 790125, | ||||||
|     "entrance": 2954076, |     "highway": 249307936, | ||||||
|     "name:etymology": 24485, |     "indoor": 562051, | ||||||
|     "memorial": 132172, |     "information": 1073014, | ||||||
|     "indoor": 353116, |     "isced:2011:level": 27, | ||||||
|     "name:etymology:wikidata": 285224, |     "isced:level:2011": 74, | ||||||
|     "landuse": 35524214, |     "landuse": 41730047, | ||||||
|     "name": 88330405, |     "leisure": 8955744, | ||||||
|     "protect_class": 73801, |     "man_made": 6799900, | ||||||
|     "information": 831513, |     "memorial": 209327, | ||||||
|     "man_made": 5116088, |     "motorcar": 621864, | ||||||
|     "boundary": 2142378, |     "name": 98684655, | ||||||
|     "tower:type": 451658, |     "name:etymology": 56375, | ||||||
|     "playground": 109175, |     "name:etymology:wikidata": 1174439, | ||||||
|     "route": 939184, |     "name:nl": 80468, | ||||||
|     "surveillance:type": 116760, |     "natural": 64176097, | ||||||
|     "natural": 52353504, |     "ngo": 57, | ||||||
|     "building": 500469053 |     "office": 1092855, | ||||||
|  |     "parking_space": 600707, | ||||||
|  |     "planned:amenity": 237, | ||||||
|  |     "playground": 182188, | ||||||
|  |     "post_office": 16379, | ||||||
|  |     "protect_class": 83815, | ||||||
|  |     "pub": 324, | ||||||
|  |     "public_transport": 5111577, | ||||||
|  |     "railway": 7068070, | ||||||
|  |     "recycling_type": 385569, | ||||||
|  |     "ref": 18607577, | ||||||
|  |     "rental": 13611, | ||||||
|  |     "route": 1075802, | ||||||
|  |     "service:bicycle:cleaning": 1179, | ||||||
|  |     "service:bicycle:pump": 14053, | ||||||
|  |     "service:bicycle:pump:operational_status": 344, | ||||||
|  |     "service:bicycle:rental": 4599, | ||||||
|  |     "service:bicycle:repair": 15470, | ||||||
|  |     "service:bicycle:retail": 11467, | ||||||
|  |     "service:bicycle:tools": 6227, | ||||||
|  |     "shelter": 1647743, | ||||||
|  |     "shop": 5860878, | ||||||
|  |     "species": 1656206, | ||||||
|  |     "species:wikidata": 107778, | ||||||
|  |     "sport": 2580042, | ||||||
|  |     "subject": 40076, | ||||||
|  |     "surface:colour": 17851, | ||||||
|  |     "surveillance:type": 171923, | ||||||
|  |     "theme": 906, | ||||||
|  |     "toilets": 90842, | ||||||
|  |     "tourism": 3211694, | ||||||
|  |     "tower:type": 596349, | ||||||
|  |     "type": 11757856, | ||||||
|  |     "vending": 252016 | ||||||
|   }, |   }, | ||||||
|   "tags": { |   "tags": { | ||||||
|     "emergency": { |     "advertising": { | ||||||
|       "defibrillator": 51273, |       "billboard": 76420, | ||||||
|       "ambulance_station": 11047, |       "board": 15040, | ||||||
|       "fire_extinguisher": 7355, |       "column": 21212, | ||||||
|       "fire_hydrant": 1598739 |       "flag": 4264, | ||||||
|     }, |       "poster_box": 22932, | ||||||
|     "barrier": { |       "screen": 1352, | ||||||
|       "cycle_barrier": 104166, |       "sculpture": 145, | ||||||
|       "bollard": 502220, |       "sign": 6172, | ||||||
|       "wall": 3535056 |       "tarp": 407, | ||||||
|     }, |       "totem": 7097, | ||||||
|     "tourism": { |       "wall_painting": 132 | ||||||
|       "artwork": 187470, |  | ||||||
|       "map": 51, |  | ||||||
|       "viewpoint": 191765 |  | ||||||
|     }, |     }, | ||||||
|     "amenity": { |     "amenity": { | ||||||
|       "bench": 1736979, |       "animal_shelter": 6056, | ||||||
|       "bicycle_library": 36, |       "atm": 207899, | ||||||
|       "bicycle_rental": 49082, |       "bank": 389470, | ||||||
|       "vending_machine": 201871, |       "bar": 219208, | ||||||
|       "bar": 199662, |       "bench": 2313183, | ||||||
|       "pub": 174979, |       "bicycle_library": 46, | ||||||
|       "cafe": 467521, |       "bicycle_parking": 616881, | ||||||
|       "restaurant": 1211671, |       "bicycle_rental": 63710, | ||||||
|       "bicycle_wash": 44, |       "bicycle_repair_station": 14026, | ||||||
|       "bike_wash": 0, |       "bicycle_wash": 79, | ||||||
|       "bicycle_repair_station": 9247, |       "biergarten": 10323, | ||||||
|       "bicycle_parking": 435959, |       "bike_wash": 1, | ||||||
|       "binoculars": 479, |       "binoculars": 1109, | ||||||
|       "biergarten": 10309, |       "cafe": 530066, | ||||||
|       "charging_station": 65402, |       "car_rental": 26726, | ||||||
|       "drinking_water": 250463, |       "charging_station": 111996, | ||||||
|       "fast_food": 460079, |       "childcare": 50390, | ||||||
|       "fire_station": 122200, |       "clinic": 179739, | ||||||
|       "parking": 4255206, |       "clock": 25274, | ||||||
|       "public_bookcase": 13120, |       "college": 64379, | ||||||
|       "toilets": 350648, |       "dentist": 122076, | ||||||
|       "recycling": 333925, |       "doctors": 166850, | ||||||
|       "waste_basket": 550357, |       "drinking_water": 294750, | ||||||
|       "waste_disposal": 156765 |       "fast_food": 533335, | ||||||
|     }, |       "fire_station": 131842, | ||||||
|     "bench": { |       "hospital": 204756, | ||||||
|       "stand_up_bench": 87, |       "ice_cream": 48853, | ||||||
|       "yes": 524993 |       "kindergarten": 294441, | ||||||
|     }, |       "nightclub": 22779, | ||||||
|     "service:bicycle:rental": { |       "parcel_locker": 44270, | ||||||
|       "yes": 3054 |       "parking": 5158899, | ||||||
|     }, |       "parking_space": 2292063, | ||||||
|     "pub": { |       "pharmacy": 383181, | ||||||
|       "cycling": 9, |       "post_box": 370286, | ||||||
|       "bicycle": 0 |       "post_office": 198908, | ||||||
|     }, |       "pub": 185475, | ||||||
|     "theme": { |       "public_bookcase": 21608, | ||||||
|       "cycling": 8, |       "reception_desk": 2426, | ||||||
|       "bicycle": 16 |       "recycling": 417512, | ||||||
|     }, |       "restaurant": 1346895, | ||||||
|     "service:bicycle:cleaning": { |       "school": 1286594, | ||||||
|       "yes": 607, |       "shelter": 494594, | ||||||
|       "diy": 0 |       "shower": 27029, | ||||||
|     }, |       "ticket_validator": 7730, | ||||||
|     "shop": { |       "toilets": 417991, | ||||||
|       "bicycle": 46488, |       "university": 54299, | ||||||
|       "sports": 37024 |       "vending_machine": 247257, | ||||||
|     }, |       "veterinary": 52813, | ||||||
|     "sport": { |       "waste_basket": 759718, | ||||||
|       "cycling": 6045, |       "waste_disposal": 219245 | ||||||
|       "bicycle": 96 |  | ||||||
|     }, |     }, | ||||||
|     "association": { |     "association": { | ||||||
|       "cycling": 5, |       "bicycle": 47, | ||||||
|       "bicycle": 20 |       "cycling": 5 | ||||||
|     }, |     }, | ||||||
|     "ngo": { |     "barrier": { | ||||||
|       "cycling": 0, |       "bollard": 668017, | ||||||
|       "bicycle": 0 |       "cycle_barrier": 122201, | ||||||
|  |       "kerb": 1178769, | ||||||
|  |       "retaining_wall": 472454, | ||||||
|  |       "wall": 4448788 | ||||||
|     }, |     }, | ||||||
|     "leisure": { |     "bench": { | ||||||
|       "bird_hide": 5669, |       "stand_up_bench": 212, | ||||||
|       "nature_reserve": 117016, |       "yes": 778144 | ||||||
|       "picnic_table": 206322, |  | ||||||
|       "pitch": 1990293, |  | ||||||
|       "playground": 705102 |  | ||||||
|     }, |  | ||||||
|     "club": { |  | ||||||
|       "cycling": 3, |  | ||||||
|       "bicycle": 49 |  | ||||||
|     }, |  | ||||||
|     "disused:amenity": { |  | ||||||
|       "charging_station": 164 |  | ||||||
|     }, |  | ||||||
|     "planned:amenity": { |  | ||||||
|       "charging_station": 115 |  | ||||||
|     }, |  | ||||||
|     "construction:amenity": { |  | ||||||
|       "charging_station": 221 |  | ||||||
|     }, |  | ||||||
|     "cycleway": { |  | ||||||
|       "lane": 314576, |  | ||||||
|       "track": 86541, |  | ||||||
|       "shared_lane": 60824 |  | ||||||
|     }, |  | ||||||
|     "highway": { |  | ||||||
|       "residential": 61321708, |  | ||||||
|       "crossing": 6119521, |  | ||||||
|       "cycleway": 1423789, |  | ||||||
|       "traffic_signals": 1512639, |  | ||||||
|       "tertiary": 7051727, |  | ||||||
|       "unclassified": 15756878, |  | ||||||
|       "secondary": 4486617, |  | ||||||
|       "primary": 3110552, |  | ||||||
|       "footway": 16496620, |  | ||||||
|       "path": 11438303, |  | ||||||
|       "steps": 1327396, |  | ||||||
|       "corridor": 27051, |  | ||||||
|       "pedestrian": 685989, |  | ||||||
|       "bridleway": 102280, |  | ||||||
|       "track": 22670967, |  | ||||||
|       "living_street": 1519108, |  | ||||||
|       "street_lamp": 2811705 |  | ||||||
|     }, |     }, | ||||||
|     "bicycle": { |     "bicycle": { | ||||||
|       "designated": 1110839 |       "designated": 1499247, | ||||||
|     }, |       "no": 1614544, | ||||||
|     "cyclestreet": { |       "yes": 3753651 | ||||||
|       "yes": 8164 |  | ||||||
|     }, |  | ||||||
|     "access": { |  | ||||||
|       "public": 6222, |  | ||||||
|       "yes": 1363526 |  | ||||||
|     }, |  | ||||||
|     "memorial": { |  | ||||||
|       "ghost_bike": 503 |  | ||||||
|     }, |  | ||||||
|     "indoor": { |  | ||||||
|       "door": 9722 |  | ||||||
|     }, |  | ||||||
|     "landuse": { |  | ||||||
|       "grass": 4898559, |  | ||||||
|       "village_green": 104681 |  | ||||||
|     }, |  | ||||||
|     "name": { |  | ||||||
|       "Park Oude God": 1 |  | ||||||
|     }, |  | ||||||
|     "information": { |  | ||||||
|       "board": 242007, |  | ||||||
|       "map": 85912, |  | ||||||
|       "office": 24139, |  | ||||||
|       "visitor_centre": 285 |  | ||||||
|     }, |  | ||||||
|     "man_made": { |  | ||||||
|       "surveillance": 148172, |  | ||||||
|       "watermill": 9699 |  | ||||||
|     }, |     }, | ||||||
|     "boundary": { |     "boundary": { | ||||||
|       "protected_area": 97075 |       "protected_area": 111282 | ||||||
|     }, |     }, | ||||||
|     "tower:type": { |     "climbing": { | ||||||
|       "observation": 19654 |       "area": 191, | ||||||
|  |       "crag": 2873, | ||||||
|  |       "route": 1040, | ||||||
|  |       "site": 14 | ||||||
|     }, |     }, | ||||||
|     "playground": { |     "club": { | ||||||
|       "forest": 56 |       "bicycle": 60, | ||||||
|  |       "climbing": 1, | ||||||
|  |       "cycling": 7 | ||||||
|     }, |     }, | ||||||
|     "surveillance:type": { |     "construction:amenity": { | ||||||
|       "camera": 112963, |       "charging_station": 259 | ||||||
|       "ALPR": 2522, |     }, | ||||||
|       "ANPR": 3 |     "conveying": { | ||||||
|  |       "yes": 12153 | ||||||
|  |     }, | ||||||
|  |     "craft": { | ||||||
|  |       "key_cutter": 3711, | ||||||
|  |       "shoe_repair": 64 | ||||||
|  |     }, | ||||||
|  |     "crossing": { | ||||||
|  |       "traffic_signals": 1408141 | ||||||
|  |     }, | ||||||
|  |     "cyclestreet": { | ||||||
|  |       "yes": 12480 | ||||||
|  |     }, | ||||||
|  |     "cycleway": { | ||||||
|  |       "lane": 300810, | ||||||
|  |       "shared_lane": 71051, | ||||||
|  |       "track": 77166 | ||||||
|  |     }, | ||||||
|  |     "disused:amenity": { | ||||||
|  |       "charging_station": 289, | ||||||
|  |       "drinking_water": 2758 | ||||||
|  |     }, | ||||||
|  |     "dog": { | ||||||
|  |       "unleashed": 727 | ||||||
|  |     }, | ||||||
|  |     "drinking_water": { | ||||||
|  |       "yes": 74561 | ||||||
|  |     }, | ||||||
|  |     "emergency": { | ||||||
|  |       "ambulance_station": 13020, | ||||||
|  |       "defibrillator": 80699, | ||||||
|  |       "fire_extinguisher": 11605, | ||||||
|  |       "fire_hydrant": 1928477 | ||||||
|  |     }, | ||||||
|  |     "footway": { | ||||||
|  |       "crossing": 3111184 | ||||||
|  |     }, | ||||||
|  |     "generator:source": { | ||||||
|  |       "wind": 390537 | ||||||
|  |     }, | ||||||
|  |     "healthcare": { | ||||||
|  |       "physiotherapist": 17548 | ||||||
|  |     }, | ||||||
|  |     "highway": { | ||||||
|  |       "bridleway": 107507, | ||||||
|  |       "bus_stop": 3459595, | ||||||
|  |       "corridor": 46847, | ||||||
|  |       "crossing": 8505991, | ||||||
|  |       "cycleway": 1693405, | ||||||
|  |       "elevator": 39221, | ||||||
|  |       "footway": 21573091, | ||||||
|  |       "living_street": 1753722, | ||||||
|  |       "motorway": 1182914, | ||||||
|  |       "motorway_link": 829035, | ||||||
|  |       "path": 13690001, | ||||||
|  |       "pedestrian": 767066, | ||||||
|  |       "primary": 3462637, | ||||||
|  |       "primary_link": 433106, | ||||||
|  |       "residential": 65553821, | ||||||
|  |       "secondary": 5008689, | ||||||
|  |       "secondary_link": 340521, | ||||||
|  |       "service": 54202864, | ||||||
|  |       "speed_camera": 61915, | ||||||
|  |       "speed_display": 2621, | ||||||
|  |       "steps": 1618344, | ||||||
|  |       "street_lamp": 3879570, | ||||||
|  |       "tertiary": 7809143, | ||||||
|  |       "tertiary_link": 245867, | ||||||
|  |       "track": 25718176, | ||||||
|  |       "traffic_signals": 1709993, | ||||||
|  |       "trunk": 1679773, | ||||||
|  |       "trunk_link": 519826, | ||||||
|  |       "unclassified": 16914480 | ||||||
|  |     }, | ||||||
|  |     "indoor": { | ||||||
|  |       "area": 25332, | ||||||
|  |       "corridor": 17609, | ||||||
|  |       "door": 19157, | ||||||
|  |       "level": 4253, | ||||||
|  |       "room": 157006, | ||||||
|  |       "wall": 32366 | ||||||
|  |     }, | ||||||
|  |     "information": { | ||||||
|  |       "board": 321201, | ||||||
|  |       "guidepost": 520873, | ||||||
|  |       "map": 108166, | ||||||
|  |       "office": 27749, | ||||||
|  |       "route_marker": 59596, | ||||||
|  |       "visitor_centre": 523 | ||||||
|  |     }, | ||||||
|  |     "isced:level:2011": { | ||||||
|  |       "early_childhood": 0 | ||||||
|  |     }, | ||||||
|  |     "landuse": { | ||||||
|  |       "village_green": 102589 | ||||||
|  |     }, | ||||||
|  |     "leisure": { | ||||||
|  |       "bird_hide": 6607, | ||||||
|  |       "dog_park": 21993, | ||||||
|  |       "fitness_centre": 72920, | ||||||
|  |       "fitness_station": 62923, | ||||||
|  |       "hackerspace": 1537, | ||||||
|  |       "nature_reserve": 129575, | ||||||
|  |       "park": 1168747, | ||||||
|  |       "picnic_table": 302582, | ||||||
|  |       "pitch": 2307262, | ||||||
|  |       "playground": 821692, | ||||||
|  |       "sports_centre": 231823, | ||||||
|  |       "track": 124600 | ||||||
|  |     }, | ||||||
|  |     "man_made": { | ||||||
|  |       "surveillance": 205953 | ||||||
|  |     }, | ||||||
|  |     "memorial": { | ||||||
|  |       "ghost_bike": 748, | ||||||
|  |       "plaque": 45536 | ||||||
|  |     }, | ||||||
|  |     "motorcar": { | ||||||
|  |       "no": 270350, | ||||||
|  |       "yes": 190966 | ||||||
|     }, |     }, | ||||||
|     "natural": { |     "natural": { | ||||||
|       "tree": 18245059 |       "cliff": 761375, | ||||||
|  |       "rock": 229114, | ||||||
|  |       "stone": 52141, | ||||||
|  |       "tree": 23309774 | ||||||
|  |     }, | ||||||
|  |     "ngo": { | ||||||
|  |       "bicycle": 0, | ||||||
|  |       "cycling": 0 | ||||||
|  |     }, | ||||||
|  |     "office": { | ||||||
|  |       "government": 250353 | ||||||
|  |     }, | ||||||
|  |     "parking_space": { | ||||||
|  |       "disabled": 161162 | ||||||
|  |     }, | ||||||
|  |     "planned:amenity": { | ||||||
|  |       "charging_station": 72 | ||||||
|  |     }, | ||||||
|  |     "playground": { | ||||||
|  |       "forest": 77 | ||||||
|  |     }, | ||||||
|  |     "post_office": { | ||||||
|  |       "post_partner": 7560 | ||||||
|  |     }, | ||||||
|  |     "pub": { | ||||||
|  |       "bicycle": 0, | ||||||
|  |       "cycling": 12 | ||||||
|  |     }, | ||||||
|  |     "public_transport": { | ||||||
|  |       "platform": 3254387 | ||||||
|  |     }, | ||||||
|  |     "railway": { | ||||||
|  |       "platform": 167408 | ||||||
|  |     }, | ||||||
|  |     "recycling_type": { | ||||||
|  |       "centre": 29508, | ||||||
|  |       "container": 355016 | ||||||
|  |     }, | ||||||
|  |     "route": { | ||||||
|  |       "bus": 272174 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:cleaning": { | ||||||
|  |       "diy": 4, | ||||||
|  |       "yes": 909 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:pump": { | ||||||
|  |       "no": 1548, | ||||||
|  |       "yes": 12452 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:pump:operational_status": { | ||||||
|  |       "broken": 122 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:rental": { | ||||||
|  |       "yes": 3902 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:repair": { | ||||||
|  |       "yes": 15134 | ||||||
|  |     }, | ||||||
|  |     "service:bicycle:tools": { | ||||||
|  |       "no": 354, | ||||||
|  |       "yes": 5872 | ||||||
|  |     }, | ||||||
|  |     "shelter": { | ||||||
|  |       "yes": 884942 | ||||||
|  |     }, | ||||||
|  |     "shop": { | ||||||
|  |       "bicycle": 51336, | ||||||
|  |       "bicycle_rental": 1, | ||||||
|  |       "rental": 5206, | ||||||
|  |       "sports": 40802 | ||||||
|  |     }, | ||||||
|  |     "sport": { | ||||||
|  |       "bicycle": 114, | ||||||
|  |       "climbing": 29028, | ||||||
|  |       "cycling": 8225 | ||||||
|  |     }, | ||||||
|  |     "surface:colour": { | ||||||
|  |       "rainbow": 217 | ||||||
|  |     }, | ||||||
|  |     "surveillance:type": { | ||||||
|  |       "ALPR": 4424, | ||||||
|  |       "ANPR": 3, | ||||||
|  |       "camera": 165247 | ||||||
|  |     }, | ||||||
|  |     "theme": { | ||||||
|  |       "bicycle": 16, | ||||||
|  |       "cycling": 7 | ||||||
|  |     }, | ||||||
|  |     "toilets": { | ||||||
|  |       "yes": 70811 | ||||||
|  |     }, | ||||||
|  |     "tourism": { | ||||||
|  |       "artwork": 245861, | ||||||
|  |       "hotel": 407208, | ||||||
|  |       "map": 51, | ||||||
|  |       "viewpoint": 219932 | ||||||
|  |     }, | ||||||
|  |     "tower:type": { | ||||||
|  |       "observation": 23057 | ||||||
|  |     }, | ||||||
|  |     "type": { | ||||||
|  |       "route": 1005677 | ||||||
|  |     }, | ||||||
|  |     "vending": { | ||||||
|  |       "elongated_coin": 816, | ||||||
|  |       "parcel_pickup;parcel_mail_in": 522, | ||||||
|  |       "parking_tickets": 70753, | ||||||
|  |       "public_transport_tickets": 26895 | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										4
									
								
								src/assets/svg/Center.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/assets/svg/Center.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | <script> | ||||||
|  | export let color = "#000000" | ||||||
|  | </script> | ||||||
|  |  <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown    width="544.02838"    height="544.02838"    viewBox="0 0 544.02838 544.02838"    version="1.1"    id="svg1"    sodipodi:docname="center.svg"    inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"    xmlns="http://www.w3.org/2000/svg"    xmlns:svg="http://www.w3.org/2000/svg">   <defs      id="defs1" />   <sodipodi:namedview      id="namedview1"      pagecolor="#505050"      bordercolor="#eeeeee"      borderopacity="1"      inkscape:showpageshadow="0"      inkscape:pageopacity="0"      inkscape:pagecheckerboard="0"      inkscape:deskcolor="#d1d1d1"      showguides="true"      inkscape:zoom="0.90326851"      inkscape:cx="393.57068"      inkscape:cy="250.756"      inkscape:window-width="1920"      inkscape:window-height="995"      inkscape:window-x="0"      inkscape:window-y="0"      inkscape:window-maximized="1"      inkscape:current-layer="svg1">     <sodipodi:guide        position="171.95879,103.32864"        orientation="0,-1"        id="guide4"        inkscape:locked="false" />     <sodipodi:guide        position="271.68286,132.35281"        orientation="1,0"        id="guide5"        inkscape:locked="false" />   </sodipodi:namedview>   <path      d="m 365.63918,111.75001 h -62.375 V 15.9375 c 0,-8.75 -7,-15.9375 -15.625,-15.9375 h -31.1875 c -8.5625,0 -15.625,7.1875 -15.625,15.9375 v 95.81251 h -62.375 l 93.5625,127.75 z"      id="path1"      sodipodi:nodetypes="ccsssscccc" />   <path      d="m 432.27837,365.63919 v -62.375 h 95.8125 c 8.75,0 15.9375,-7 15.9375,-15.625 v -31.1875 c 0,-8.5625 -7.1875,-15.625 -15.9375,-15.625 h -95.8125 v -62.375 l -127.75,93.5625 z"      id="path1-5"      sodipodi:nodetypes="ccsssscccc" />   <path      d="m 178.38918,432.27838 h 62.375 v 95.8125 c 0,8.75 7,15.9375 15.625,15.9375 h 31.1875 c 8.5625,0 15.625,-7.1875 15.625,-15.9375 v -95.8125 h 62.375 l -93.5625,-127.75 z"      id="path2"      sodipodi:nodetypes="ccsssscccc" />   <path      d="m 111.75,178.38919 v 62.375 H 15.9375 c -8.75,0 -15.9375,7 -15.9375,15.625 v 31.1875 c 0,8.5625 7.1875,15.625 15.9375,15.625 H 111.75 v 62.375 l 127.74999,-93.5625 z"      id="path3"      sodipodi:nodetypes="ccsssscccc" /> </svg>  | ||||||
|  | @ -280,6 +280,16 @@ button.disabled:hover, .button.disabled:hover { | ||||||
|     color: unset; |     color: unset; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | button.link { | ||||||
|  |     border: none; | ||||||
|  |     text-decoration: underline; | ||||||
|  |     background-color: unset; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button.link:hover { | ||||||
|  |     color:unset; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .interactive button.disabled svg path, .interactive .button.disabled svg path { | .interactive button.disabled svg path, .interactive .button.disabled svg path { | ||||||
|     fill: var(--interactive-foreground) !important;; |     fill: var(--interactive-foreground) !important;; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -125,7 +125,21 @@ describe("PrepareTheme", () => { | ||||||
|                 en: "Test layer - please ignore", |                 en: "Test layer - please ignore", | ||||||
|             }, |             }, | ||||||
|             titleIcons: [], |             titleIcons: [], | ||||||
|             pointRendering: [{ location: ["point"], label: "xyz" }], |             pointRendering: [ | ||||||
|  |                 { | ||||||
|  |                     location: ["point"], | ||||||
|  |                     label: "xyz", | ||||||
|  |                     iconBadges: [ | ||||||
|  |                         { | ||||||
|  |                             if: "_favourite=yes", | ||||||
|  |                             then: <any>{ | ||||||
|  |                                 id: "circlewhiteheartred", | ||||||
|  |                                 render: "circle:white;heart:red", | ||||||
|  |                             }, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|             lineRendering: [{ width: 1 }], |             lineRendering: [{ width: 1 }], | ||||||
|         } |         } | ||||||
|         const sharedLayers = constructSharedLayers() |         const sharedLayers = constructSharedLayers() | ||||||
|  | @ -165,7 +179,21 @@ describe("PrepareTheme", () => { | ||||||
|             id: "layer-example", |             id: "layer-example", | ||||||
|             name: null, |             name: null, | ||||||
|             minzoom: 18, |             minzoom: 18, | ||||||
|             pointRendering: [{ location: ["point"], label: "xyz" }], |             pointRendering: [ | ||||||
|  |                 { | ||||||
|  |                     location: ["point"], | ||||||
|  |                     label: "xyz", | ||||||
|  |                     iconBadges: [ | ||||||
|  |                         { | ||||||
|  |                             if: "_favourite=yes", | ||||||
|  |                             then: { | ||||||
|  |                                 id: "circlewhiteheartred", | ||||||
|  |                                 render: "circle:white;heart:red", | ||||||
|  |                             }, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|             lineRendering: [{ width: 1 }], |             lineRendering: [{ width: 1 }], | ||||||
|             titleIcons: [], |             titleIcons: [], | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue