forked from MapComplete/MapComplete
		
	More work on A11y
This commit is contained in:
		
							parent
							
								
									87aee9e2b7
								
							
						
					
					
						commit
						6da72b80ef
					
				
					 28 changed files with 398 additions and 209 deletions
				
			
		|  | @ -139,7 +139,8 @@ | ||||||
|       "condition": "_theme:backgroundLayer=", |       "condition": "_theme:backgroundLayer=", | ||||||
|       "mappings": [ |       "mappings": [ | ||||||
|         { |         { | ||||||
|           "if": "mapcomplete-preferred-background-layer=", |           "if": "mapcomplete-preferred-background-layer=default", | ||||||
|  |           "alsoShowIf": "mapcomplete-preferred-background-layer=", | ||||||
|           "then": { |           "then": { | ||||||
|             "en": "Use the default background layer", |             "en": "Use the default background layer", | ||||||
|             "ca": "Utilitzeu la capa de fons predeterminada", |             "ca": "Utilitzeu la capa de fons predeterminada", | ||||||
|  |  | ||||||
|  | @ -94,10 +94,10 @@ | ||||||
|             "backToSelect": "Vælg en anden kategori", |             "backToSelect": "Vælg en anden kategori", | ||||||
|             "confirmButton": "Tilføj en {category}<br><div class=\"alert\">Din tilføjelse er synlig for alle</div>", |             "confirmButton": "Tilføj en {category}<br><div class=\"alert\">Din tilføjelse er synlig for alle</div>", | ||||||
|             "confirmLocation": "Bekræft dette sted", |             "confirmLocation": "Bekræft dette sted", | ||||||
|             "confirmTitle": "Tilføj en {titel}?", |             "confirmTitle": "Tilføj en {title}?", | ||||||
|             "disableFilters": "Slå alle filtre fra", |             "disableFilters": "Slå alle filtre fra", | ||||||
|             "disableFiltersExplanation": "Nogle elementer kan være skjult af et filter", |             "disableFiltersExplanation": "Nogle elementer kan være skjult af et filter", | ||||||
|             "enableLayer": "Aktivér lag {navn}", |             "enableLayer": "Aktivér lag {name}", | ||||||
|             "hasBeenImported": "Punktet er allerede importeret", |             "hasBeenImported": "Punktet er allerede importeret", | ||||||
|             "import": { |             "import": { | ||||||
|                 "hasBeenImported": "Objektet blev importeret", |                 "hasBeenImported": "Objektet blev importeret", | ||||||
|  | @ -125,8 +125,8 @@ | ||||||
|             "isApplied": "Ændringerne er anvendt" |             "isApplied": "Ændringerne er anvendt" | ||||||
|         }, |         }, | ||||||
|         "attribution": { |         "attribution": { | ||||||
|             "attributionBackgroundLayer": "Det nuværende baggrundslag er {navn}", |             "attributionBackgroundLayer": "Det nuværende baggrundslag er {name}", | ||||||
|             "attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er [navn}: {copyright}", |             "attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er {name}: {copyright}", | ||||||
|             "attributionContent": "<p>Alle data leveres af <a href=\"https://osm.org\" target=\"_blank\">OpenStreetMap</a>, frit genanvendelige under <a href=\"https://osm.org/copyright\" target=\"_blank\">Open DataBase Licensen</a>.</p>", |             "attributionContent": "<p>Alle data leveres af <a href=\"https://osm.org\" target=\"_blank\">OpenStreetMap</a>, frit genanvendelige under <a href=\"https://osm.org/copyright\" target=\"_blank\">Open DataBase Licensen</a>.</p>", | ||||||
|             "attributionTitle": "Meddelelse om tilskrivning", |             "attributionTitle": "Meddelelse om tilskrivning", | ||||||
|             "codeContributionsBy": "MapComplete er lavet af {contributors} og <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} flere bidragsydere</a>", |             "codeContributionsBy": "MapComplete er lavet af {contributors} og <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} flere bidragsydere</a>", | ||||||
|  | @ -253,7 +253,7 @@ | ||||||
|         "pickLanguage": "Vælg et sprog: ", |         "pickLanguage": "Vælg et sprog: ", | ||||||
|         "poweredByOsm": "Drevet af OpenStreetMap", |         "poweredByOsm": "Drevet af OpenStreetMap", | ||||||
|         "questionBox": { |         "questionBox": { | ||||||
|             "answeredMultiple": "Du besvarede [answered} spørgsmål", |             "answeredMultiple": "Du besvarede {answered} spørgsmål", | ||||||
|             "answeredMultipleSkippedMultiple": "Du besvarede {answered} spørgsmål og sprang over {skipped} spørgsmål", |             "answeredMultipleSkippedMultiple": "Du besvarede {answered} spørgsmål og sprang over {skipped} spørgsmål", | ||||||
|             "answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål", |             "answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål", | ||||||
|             "answeredOne": "Du besvarede ét spørgsmål", |             "answeredOne": "Du besvarede ét spørgsmål", | ||||||
|  |  | ||||||
|  | @ -398,17 +398,20 @@ | ||||||
|         "useSearch": "Use the search above to see presets", |         "useSearch": "Use the search above to see presets", | ||||||
|         "useSearchForMore": "Use the search function to search within {total} more values…", |         "useSearchForMore": "Use the search function to search within {total} more values…", | ||||||
|         "visualFeedback": { |         "visualFeedback": { | ||||||
|             "closestFeaturesAre": "{n} features within view", |             "closestFeaturesAre": "{n} features within viewport.", | ||||||
|             "east": "Moving east", |             "east": "Moving east", | ||||||
|             "in": "Zooming in", |             "in": "Zooming in to level {z}", | ||||||
|             "islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.", |             "islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.", | ||||||
|             "locked": "View is now locked to your GPS-location, moving disabled.", |             "locked": "View is now locked to your GPS-location, moving disabled.", | ||||||
|             "navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.", |             "navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.", | ||||||
|             "noCloseFeatures": "No features in view", |             "noCloseFeatures": "No features in view", | ||||||
|             "north": "Moving north", |             "north": "Moving north", | ||||||
|             "out": "Zooming out", |             "oneFeatureInView": "One feature within viewport.", | ||||||
|  |             "out": "Zooming out to level {z}", | ||||||
|             "south": "Moving south", |             "south": "Moving south", | ||||||
|             "unlocked": "Moving enabled.", |             "unlocked": "Moving enabled.", | ||||||
|  |             "viewportCenterCloseToGps": "The map is centered around your location.", | ||||||
|  |             "viewportCenterDetails": "The viewport center is {distance} away and {bearing} from your location.", | ||||||
|             "west": "Moving west" |             "west": "Moving west" | ||||||
|         }, |         }, | ||||||
|         "waitingForGeopermission": "Waiting for your permission to use the geolocation…", |         "waitingForGeopermission": "Waiting for your permission to use the geolocation…", | ||||||
|  |  | ||||||
|  | @ -370,15 +370,15 @@ | ||||||
|         "useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien", |         "useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien", | ||||||
|         "useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…", |         "useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…", | ||||||
|         "visualFeedback": { |         "visualFeedback": { | ||||||
|             "closestFeaturesAre": "{n} object in in beeld", |             "closestFeaturesAre": "{n} object in beeld.", | ||||||
|             "east": "Naar het oosten", |             "east": "Naar het oosten", | ||||||
|             "in": "Aan het inzoomen", |             "in": "Aan het inzoomen naar zoomlevel {z}", | ||||||
|             "islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.", |             "islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.", | ||||||
|             "locked": "Bewegen vergrendeld rond jouw huidige locatie.", |             "locked": "Bewegen vergrendeld rond jouw huidige locatie.", | ||||||
|             "navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.", |             "navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.", | ||||||
|             "noCloseFeatures": "Niet in beeld", |             "noCloseFeatures": "Niet in beeld", | ||||||
|             "north": "Naar het noorden", |             "north": "Naar het noorden", | ||||||
|             "out": "Aan het uitzoomen", |             "out": "Aan het uitzoomen naar zoomlevel {z}", | ||||||
|             "south": "Naar het zuiden", |             "south": "Naar het zuiden", | ||||||
|             "unlocked": "Bewegen ontgrendeld", |             "unlocked": "Bewegen ontgrendeld", | ||||||
|             "west": "Naar het westen" |             "west": "Naar het westen" | ||||||
|  |  | ||||||
|  | @ -1342,10 +1342,6 @@ video { | ||||||
|   resize: both; |   resize: both; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .list-none { |  | ||||||
|   list-style-type: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .appearance-none { | .appearance-none { | ||||||
|   -webkit-appearance: none; |   -webkit-appearance: none; | ||||||
|           appearance: none; |           appearance: none; | ||||||
|  | @ -1906,6 +1902,11 @@ video { | ||||||
|   line-height: 1; |   line-height: 1; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .text-sm { | ||||||
|  |   font-size: 0.875rem; | ||||||
|  |   line-height: 1.25rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .text-3xl { | .text-3xl { | ||||||
|   font-size: 1.875rem; |   font-size: 1.875rem; | ||||||
|   line-height: 2.25rem; |   line-height: 2.25rem; | ||||||
|  | @ -1916,11 +1917,6 @@ video { | ||||||
|   line-height: 2rem; |   line-height: 2rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .text-sm { |  | ||||||
|   font-size: 0.875rem; |  | ||||||
|   line-height: 1.25rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .text-4xl { | .text-4xl { | ||||||
|   font-size: 2.25rem; |   font-size: 2.25rem; | ||||||
|   line-height: 2.5rem; |   line-height: 2.5rem; | ||||||
|  |  | ||||||
|  | @ -288,4 +288,8 @@ export class BBox { | ||||||
|             throw "BBOX has NAN" |             throw "BBOX has NAN" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public overlapsWithFeature(f: Feature) { | ||||||
|  |         return GeoOperations.calculateOverlap(this.asGeoJson({}), [f]).length > 0 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,28 +4,32 @@ import { Feature } from "geojson" | ||||||
| import { GeoOperations } from "../../GeoOperations" | import { GeoOperations } from "../../GeoOperations" | ||||||
| import FilteringFeatureSource from "./FilteringFeatureSource" | import FilteringFeatureSource from "./FilteringFeatureSource" | ||||||
| import LayerState from "../../State/LayerState" | import LayerState from "../../State/LayerState" | ||||||
|  | import { BBox } from "../../BBox" | ||||||
| 
 | 
 | ||||||
| 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 _result = new UIEventSource<Feature[]>(undefined) | ||||||
|     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 _layerState?: LayerState | ||||||
|     private readonly _currentZoom: Store<number> |     private readonly _currentZoom: Store<number> | ||||||
|     private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = [] |     private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = [] | ||||||
| 
 |     private readonly _bounds: Store<BBox> | undefined | ||||||
|     constructor( |     constructor( | ||||||
|         targetPoint: Store<{ lon: number; lat: number }>, |         targetPoint: Store<{ lon: number; lat: number }>, | ||||||
|         sources: ReadonlyMap<string, FilteringFeatureSource>, |         sources: ReadonlyMap<string, FilteringFeatureSource>, | ||||||
|         numberOfNeededFeatures?: number, |         options?: { | ||||||
|         layerState?: LayerState, |             bounds?: Store<BBox> | ||||||
|         currentZoom?: Store<number> |             numberOfNeededFeatures?: number | ||||||
|  |             layerState?: LayerState | ||||||
|  |             currentZoom?: Store<number> | ||||||
|  |         } | ||||||
|     ) { |     ) { | ||||||
|         this._layerState = layerState |         this._layerState = options?.layerState | ||||||
|         this._targetPoint = targetPoint.stabilized(100) |         this._targetPoint = targetPoint.stabilized(100) | ||||||
|         this._numberOfNeededFeatures = numberOfNeededFeatures |         this._numberOfNeededFeatures = options?.numberOfNeededFeatures | ||||||
|         this._currentZoom = currentZoom.stabilized(500) |         this._currentZoom = options?.currentZoom.stabilized(500) | ||||||
|  |         this._bounds = options?.bounds | ||||||
| 
 | 
 | ||||||
|         this.features = Stores.ListStabilized(this._result) |         this.features = Stores.ListStabilized(this._result) | ||||||
| 
 | 
 | ||||||
|  | @ -53,6 +57,10 @@ export default class NearbyFeatureSource implements FeatureSource { | ||||||
|     private update() { |     private update() { | ||||||
|         let features: { feat: Feature; d: number }[] = [] |         let features: { feat: Feature; d: number }[] = [] | ||||||
|         for (const src of this._allSources) { |         for (const src of this._allSources) { | ||||||
|  |             if (src.data === undefined) { | ||||||
|  |                 this._result.setData(undefined) | ||||||
|  |                 return // We cannot yet calculate all the features
 | ||||||
|  |             } | ||||||
|             features.push(...src.data) |             features.push(...src.data) | ||||||
|         } |         } | ||||||
|         features.sort((a, b) => a.d - b.d) |         features.sort((a, b) => a.d - b.d) | ||||||
|  | @ -80,6 +88,15 @@ export default class NearbyFeatureSource implements FeatureSource { | ||||||
|                 if (this._currentZoom.data < minZoom) { |                 if (this._currentZoom.data < minZoom) { | ||||||
|                     return empty |                     return empty | ||||||
|                 } |                 } | ||||||
|  |                 if (this._bounds) { | ||||||
|  |                     const bbox = this._bounds.data | ||||||
|  |                     if (!bbox) { | ||||||
|  |                         // We have a 'bounds' store, but the bounds store itself is still empty
 | ||||||
|  |                         // As such, we cannot yet calculate which features are within the store
 | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
|  |                     feats = feats.filter((f) => bbox.overlapsWithFeature(f)) | ||||||
|  |                 } | ||||||
|                 const point = this._targetPoint.data |                 const point = this._targetPoint.data | ||||||
|                 const lonLat = <[number, number]>[point.lon, point.lat] |                 const lonLat = <[number, number]>[point.lon, point.lat] | ||||||
|                 const withDistance = feats.map((feat) => ({ |                 const withDistance = feats.map((feat) => ({ | ||||||
|  | @ -95,7 +112,7 @@ export default class NearbyFeatureSource implements FeatureSource { | ||||||
|                 } |                 } | ||||||
|                 return withDistance |                 return withDistance | ||||||
|             }, |             }, | ||||||
|             [this._targetPoint, isActive, this._currentZoom] |             [this._targetPoint, isActive, this._currentZoom, this._bounds] | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -172,7 +172,7 @@ export class GeoOperations { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Detect wether or not the given point is located in the feature |      * Detect whether or not the given point is located in the feature | ||||||
|      * |      * | ||||||
|      * // Should work with a normal polygon
 |      * // Should work with a normal polygon
 | ||||||
|      * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; |      * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; | ||||||
|  | @ -985,4 +985,87 @@ export class GeoOperations { | ||||||
| 
 | 
 | ||||||
|         return result |         return result | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * GeoOperations.distanceToHuman(52.8) // => "53m"
 | ||||||
|  |      * GeoOperations.distanceToHuman(2800) // => "2.8km"
 | ||||||
|  |      * GeoOperations.distanceToHuman(12800) // => "13km"
 | ||||||
|  |      * | ||||||
|  |      * @param meters | ||||||
|  |      */ | ||||||
|  |     public static distanceToHuman(meters: number): string { | ||||||
|  |         meters = Math.round(meters) | ||||||
|  |         if (meters < 1000) { | ||||||
|  |             return meters + "m" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (meters >= 10000) { | ||||||
|  |             const km = Math.round(meters / 1000) | ||||||
|  |             return km + "km" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         meters = Math.round(meters / 100) | ||||||
|  |         const kmStr = "" + meters | ||||||
|  | 
 | ||||||
|  |         return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const | ||||||
|  |     private static readonly directionsRelative = [ | ||||||
|  |         "straight", | ||||||
|  |         "slight_right", | ||||||
|  |         "right", | ||||||
|  |         "sharp_right", | ||||||
|  |         "behind", | ||||||
|  |         "sharp_left", | ||||||
|  |         "left", | ||||||
|  |         "slight_left", | ||||||
|  |     ] as const | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * GeoOperations.bearingToHuman(0) // => "N"
 | ||||||
|  |      * GeoOperations.bearingToHuman(-9) // => "N"
 | ||||||
|  |      * GeoOperations.bearingToHuman(-10) // => "N"
 | ||||||
|  |      * GeoOperations.bearingToHuman(-180) // => "S"
 | ||||||
|  |      * GeoOperations.bearingToHuman(181) // => "S"
 | ||||||
|  |      * GeoOperations.bearingToHuman(46) // => "NE"
 | ||||||
|  |      */ | ||||||
|  |     public static bearingToHuman( | ||||||
|  |         bearing: number | ||||||
|  |     ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { | ||||||
|  |         while (bearing < 0) { | ||||||
|  |             bearing += 360 | ||||||
|  |         } | ||||||
|  |         bearing %= 360 | ||||||
|  |         bearing += 22.5 | ||||||
|  |         const segment = Math.floor(bearing / 45) % GeoOperations.directions.length | ||||||
|  |         return GeoOperations.directions[segment] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * GeoOperations.bearingToHuman(0) // => "N"
 | ||||||
|  |      * GeoOperations.bearingToHuman(-10) // => "N"
 | ||||||
|  |      * GeoOperations.bearingToHuman(-180) // => "S"
 | ||||||
|  |      * GeoOperations.bearingToHuman(181) // => "S"
 | ||||||
|  |      * GeoOperations.bearingToHuman(46) // => "NE"
 | ||||||
|  |      */ | ||||||
|  |     public static bearingToHumanRelative( | ||||||
|  |         bearing: number | ||||||
|  |     ): | ||||||
|  |         | "straight" | ||||||
|  |         | "slight_right" | ||||||
|  |         | "right" | ||||||
|  |         | "sharp_right" | ||||||
|  |         | "behind" | ||||||
|  |         | "sharp_left" | ||||||
|  |         | "left" | ||||||
|  |         | "slight_left" { | ||||||
|  |         while (bearing < 0) { | ||||||
|  |             bearing += 360 | ||||||
|  |         } | ||||||
|  |         bearing %= 360 | ||||||
|  |         bearing += 22.5 | ||||||
|  |         const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length | ||||||
|  |         return GeoOperations.directionsRelative[segment] | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,42 +1,14 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | ||||||
| export class ThemeMetaTagging { | export class ThemeMetaTagging { | ||||||
|     public static readonly themeName = "usersettings" |    public static readonly themeName = "usersettings" | ||||||
| 
 | 
 | ||||||
|     public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { |    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )  | ||||||
|             feat.properties._description |       Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )  | ||||||
|                 .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href   }) (feat)  )  | ||||||
|                 ?.at(1) |       Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat)  )  | ||||||
|         ) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )  | ||||||
|         Utils.AddLazyProperty( |       feat.properties['__current_backgroun'] = 'initial_value' | ||||||
|             feat.properties, |    } | ||||||
|             "_d", | } | ||||||
|             () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.href.match(/mastodon|en.osm.town/) !== null |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.getAttribute("rel")?.indexOf("me") >= 0 |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty( |  | ||||||
|             feat.properties, |  | ||||||
|             "_mastodon_candidate", |  | ||||||
|             () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a |  | ||||||
|         ) |  | ||||||
|         feat.properties["__current_backgroun"] = "initial_value" |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -10,6 +10,9 @@ export class Stores { | ||||||
| 
 | 
 | ||||||
|         function run() { |         function run() { | ||||||
|             source.setData(new Date()) |             source.setData(new Date()) | ||||||
|  |             if (Utils.runningFromConsole) { | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|             if (asLong === undefined || asLong()) { |             if (asLong === undefined || asLong()) { | ||||||
|                 window.setTimeout(run, millis) |                 window.setTimeout(run, millis) | ||||||
|             } |             } | ||||||
|  | @ -104,7 +107,8 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|     M |     M | ||||||
|     public mapD<J>( |     public mapD<J>( | ||||||
|         f: (t: Exclude<T, undefined | null>) => J, |         f: (t: Exclude<T, undefined | null>) => J, | ||||||
|         extraStoresToWatch?: Store<any>[] |         extraStoresToWatch?: Store<any>[], | ||||||
|  |         callbackDestroyFunction?: (f: () => void) => void | ||||||
|     ): Store<J> { |     ): Store<J> { | ||||||
|         return this.map((t) => { |         return this.map((t) => { | ||||||
|             if (t === undefined) { |             if (t === undefined) { | ||||||
|  | @ -263,7 +267,7 @@ export abstract class Store<T> implements Readable<T> { | ||||||
|     /** |     /** | ||||||
|      * Converts the uiEventSource into a promise. |      * Converts the uiEventSource into a promise. | ||||||
|      * The promise will return the value of the store if the given condition evaluates to true |      * The promise will return the value of the store if the given condition evaluates to true | ||||||
|      * @param condition: an optional condition, default to 'store.value !== undefined' |      * @param condition an optional condition, default to 'store.value !== undefined' | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public AsPromise(condition?: (t: T) => boolean): Promise<T> { |     public AsPromise(condition?: (t: T) => boolean): Promise<T> { | ||||||
|  | @ -482,7 +486,7 @@ class MappedStore<TIn, T> extends Store<T> { | ||||||
|             stores = [] |             stores = [] | ||||||
|         } |         } | ||||||
|         if (extraStores?.length > 0) { |         if (extraStores?.length > 0) { | ||||||
|             stores.push(...extraStores) |             stores?.push(...extraStores) | ||||||
|         } |         } | ||||||
|         if (this._extraStores?.length > 0) { |         if (this._extraStores?.length > 0) { | ||||||
|             this._extraStores?.forEach((store) => { |             this._extraStores?.forEach((store) => { | ||||||
|  | @ -767,9 +771,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
|     /** |     /** | ||||||
|      * Monoidal map which results in a read-only store |      * Monoidal map which results in a read-only store | ||||||
|      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' |      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' | ||||||
|      * @param f: The transforming function |      * @param f The transforming function | ||||||
|      * @param extraSources: also trigger the update if one of these sources change |      * @param extraSources also trigger the update if one of these sources change | ||||||
|      * @param onDestroy: a callback that can trigger the destroy function |      * @param onDestroy a callback that can trigger the destroy function | ||||||
|      * |      * | ||||||
|      * const src = new UIEventSource<number>(10) |      * const src = new UIEventSource<number>(10) | ||||||
|      * const store = src.map(i => i * 2) |      * const store = src.map(i => i * 2) | ||||||
|  | @ -802,7 +806,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
|      */ |      */ | ||||||
|     public mapD<J>( |     public mapD<J>( | ||||||
|         f: (t: Exclude<T, undefined | null>) => J, |         f: (t: Exclude<T, undefined | null>) => J, | ||||||
|         extraSources: Store<any>[] = [] |         extraSources: Store<any>[] = [], | ||||||
|  |         callbackDestroyFunction?: (f: () => void) => void | ||||||
|     ): Store<J | undefined> { |     ): Store<J | undefined> { | ||||||
|         return new MappedStore( |         return new MappedStore( | ||||||
|             this, |             this, | ||||||
|  | @ -819,17 +824,18 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | ||||||
|             this._callbacks, |             this._callbacks, | ||||||
|             this.data === undefined || this.data === null |             this.data === undefined || this.data === null | ||||||
|                 ? <undefined | null>this.data |                 ? <undefined | null>this.data | ||||||
|                 : f(<any>this.data) |                 : f(<any>this.data), | ||||||
|  |             callbackDestroyFunction | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Two way sync with functions in both directions |      * Two way sync with functions in both directions | ||||||
|      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' |      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' | ||||||
|      * @param f: The transforming function |      * @param f The transforming function | ||||||
|      * @param extraSources: also trigger the update if one of these sources change |      * @param extraSources also trigger the update if one of these sources change | ||||||
|      * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData |      * @param g a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData | ||||||
|      * @param allowUnregister: if set, the update will be halted if no listeners are registered |      * @param allowUnregister if set, the update will be halted if no listeners are registered | ||||||
|      */ |      */ | ||||||
|     public sync<J>( |     public sync<J>( | ||||||
|         f: (t: T) => J, |         f: (t: T) => J, | ||||||
|  |  | ||||||
|  | @ -105,6 +105,12 @@ export interface MappingConfigJson { | ||||||
|      */ |      */ | ||||||
|     hideInAnswer?: boolean | TagConfigJson |     hideInAnswer?: boolean | TagConfigJson | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Also show this 'then'-option if the feature matches these tags. | ||||||
|  |      * Ideal for outdated tags. | ||||||
|  |      */ | ||||||
|  |     alsoShowIf?: TagConfigJson | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * question: What tags should be applied if this mapping is _not_ chosen? |      * question: What tags should be applied if this mapping is _not_ chosen? | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -168,6 +168,7 @@ export interface TagRenderingConfigJson { | ||||||
|          * This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'} |          * This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'} | ||||||
|          */ |          */ | ||||||
|         if: TagConfigJson |         if: TagConfigJson | ||||||
|  | 
 | ||||||
|         /** |         /** | ||||||
|          * question: What text should be shown? |          * question: What text should be shown? | ||||||
|          * |          * | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ export interface Icon {} | ||||||
| 
 | 
 | ||||||
| export interface Mapping { | export interface Mapping { | ||||||
|     readonly if: UploadableTag |     readonly if: UploadableTag | ||||||
|  |     readonly alsoShowIf: Tag | undefined | ||||||
|     readonly ifnot?: UploadableTag |     readonly ifnot?: UploadableTag | ||||||
|     readonly then: TypedTranslation<object> |     readonly then: TypedTranslation<object> | ||||||
|     readonly icon: string |     readonly icon: string | ||||||
|  | @ -383,7 +384,9 @@ export default class TagRenderingConfig { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         const prioritySearch = |         const prioritySearch = | ||||||
|             mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined |             mapping.priorityIf !== undefined | ||||||
|  |                 ? TagUtils.Tag(mapping.priorityIf, `${ctx}.priorityIf`) | ||||||
|  |                 : undefined | ||||||
|         const mp = <Mapping>{ |         const mp = <Mapping>{ | ||||||
|             if: TagUtils.Tag(mapping.if, `${ctx}.if`), |             if: TagUtils.Tag(mapping.if, `${ctx}.if`), | ||||||
|             ifnot: |             ifnot: | ||||||
|  | @ -391,6 +394,10 @@ export default class TagRenderingConfig { | ||||||
|                     ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) |                     ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) | ||||||
|                     : undefined, |                     : undefined, | ||||||
|             then: Translations.T(mapping.then, `${ctx}.then`), |             then: Translations.T(mapping.then, `${ctx}.then`), | ||||||
|  |             alsoShowIf: | ||||||
|  |                 mapping.alsoShowIf !== undefined | ||||||
|  |                     ? TagUtils.Tag(mapping.alsoShowIf, `${ctx}.alsoShowIf`) | ||||||
|  |                     : undefined, | ||||||
|             hideInAnswer, |             hideInAnswer, | ||||||
|             icon, |             icon, | ||||||
|             iconClass, |             iconClass, | ||||||
|  | @ -530,6 +537,9 @@ export default class TagRenderingConfig { | ||||||
|                 if (mapping.if.matchesProperties(tags)) { |                 if (mapping.if.matchesProperties(tags)) { | ||||||
|                     return mapping |                     return mapping | ||||||
|                 } |                 } | ||||||
|  |                 if (mapping.alsoShowIf?.matchesProperties(tags)) { | ||||||
|  |                     return mapping | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -818,6 +828,7 @@ export default class TagRenderingConfig { | ||||||
|         for (const m of this.mappings ?? []) { |         for (const m of this.mappings ?? []) { | ||||||
|             tags.push(m.if) |             tags.push(m.if) | ||||||
|             tags.push(m.priorityIf) |             tags.push(m.priorityIf) | ||||||
|  |             tags.push(m.alsoShowIf) | ||||||
|             tags.push(...(m.addExtraTags ?? [])) |             tags.push(...(m.addExtraTags ?? [])) | ||||||
|             if (typeof m.hideInAnswer !== "boolean") { |             if (typeof m.hideInAnswer !== "boolean") { | ||||||
|                 tags.push(m.hideInAnswer) |                 tags.push(m.hideInAnswer) | ||||||
|  |  | ||||||
|  | @ -119,6 +119,11 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     readonly previewedImage = new UIEventSource<ProvidedImage>(undefined) |     readonly previewedImage = new UIEventSource<ProvidedImage>(undefined) | ||||||
| 
 | 
 | ||||||
|     readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false) |     readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||||
|  |     /** | ||||||
|  |      * When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set. | ||||||
|  |      * This is the 'viewport' which 'closestFeatures' uses to filter wilt | ||||||
|  |      */ | ||||||
|  |     readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined) | ||||||
| 
 | 
 | ||||||
|     readonly lastClickObject: LastClickFeatureSource |     readonly lastClickObject: LastClickFeatureSource | ||||||
|     readonly overlayLayerStates: ReadonlyMap< |     readonly overlayLayerStates: ReadonlyMap< | ||||||
|  | @ -351,9 +356,11 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.closestFeatures = new NearbyFeatureSource( |         this.closestFeatures = new NearbyFeatureSource( | ||||||
|             this.mapProperties.location, |             this.mapProperties.location, | ||||||
|             this.perLayerFiltered, |             this.perLayerFiltered, | ||||||
|             3, |             { | ||||||
|             this.layerState, |                 currentZoom: this.mapProperties.zoom, | ||||||
|             this.mapProperties.zoom |                 layerState: this.layerState, | ||||||
|  |                 bounds: this.visualFeedbackViewportBounds, | ||||||
|  |             } | ||||||
|         ) |         ) | ||||||
|         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView |         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView | ||||||
|         this.imageUploadManager = new ImageUploadManager( |         this.imageUploadManager = new ImageUploadManager( | ||||||
|  | @ -476,8 +483,18 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|      */ |      */ | ||||||
|     private selectClosestAtCenter(i: number = 0) { |     private selectClosestAtCenter(i: number = 0) { | ||||||
|         this.visualFeedback.setData(true) |         this.visualFeedback.setData(true) | ||||||
|         const toSelect = this.closestFeatures.features.data[i] |         const toSelect = this.closestFeatures.features?.data?.[i] | ||||||
|         if (!toSelect) { |         if (!toSelect) { | ||||||
|  |             window.requestAnimationFrame(() => { | ||||||
|  |                 const toSelect = this.closestFeatures.features?.data?.[i] | ||||||
|  |                 if (!toSelect) { | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                 const layer = this.layout.getMatchingLayer(toSelect.properties) | ||||||
|  |                 this.selectedElement.setData(undefined) | ||||||
|  |                 this.selectedLayer.setData(layer) | ||||||
|  |                 this.selectedElement.setData(toSelect) | ||||||
|  |             }) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         const layer = this.layout.getMatchingLayer(toSelect.properties) |         const layer = this.layout.getMatchingLayer(toSelect.properties) | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								src/UI/Base/DirectionIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/UI/Base/DirectionIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * An A11Y feature which indicates how far away and in what direction the feature lies. | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  | 
 | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  |   import { Store } from "../../Logic/UIEventSource" | ||||||
|  |   import type { Feature } from "geojson" | ||||||
|  |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|  |   import Compass_arrow from "../../assets/svg/Compass_arrow.svelte" | ||||||
|  |   import { twMerge } from "tailwind-merge" | ||||||
|  |   import { Orientation } from "../../Sensors/Orientation" | ||||||
|  | 
 | ||||||
|  |   export let state: ThemeViewState | ||||||
|  |   export let feature: Feature | ||||||
|  | 
 | ||||||
|  |   let fcenter = GeoOperations.centerpointCoordinates(feature) | ||||||
|  |   // Bearing and distance relative to the map center | ||||||
|  |   let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map( | ||||||
|  |     (l) => { | ||||||
|  |       let mapCenter = [l.lon, l.lat] | ||||||
|  |       let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter)) | ||||||
|  |       let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter)) | ||||||
|  |       return { bearing, dist } | ||||||
|  |     }, | ||||||
|  |   ) | ||||||
|  |   let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => { | ||||||
|  |     return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter) | ||||||
|  |   }) | ||||||
|  |   let compass = Orientation.singleton.alpha.map(compass => compass ?? 0) | ||||||
|  |   export let size = "w-8 h-8" | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class={twMerge("relative", size)}> | ||||||
|  |   <div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm",size)}> | ||||||
|  |     {GeoOperations.distanceToHuman($bearingAndDist.dist)} | ||||||
|  |   </div> | ||||||
|  |   {#if $bearingFromGps !== undefined} | ||||||
|  |     <div class={twMerge("absolute top-0 left-0 rounded-full border border-gray-500", size)}> | ||||||
|  |       <Compass_arrow class={size} | ||||||
|  |                      style={`transform: rotate( calc( 45deg + ${$bearingFromGps - $compass}deg) );`} /> | ||||||
|  |     </div> | ||||||
|  |   {/if} | ||||||
|  | </div> | ||||||
|  | <span>{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)}</span> | ||||||
|  | @ -47,7 +47,8 @@ export default class Hotkeys { | ||||||
|             onUp?: boolean |             onUp?: boolean | ||||||
|         }, |         }, | ||||||
|         documentation: string | Translation, |         documentation: string | Translation, | ||||||
|         action: () => void | false |         action: () => void | false, | ||||||
|  |         alsoTriggeredOn?: Translation[] | ||||||
|     ) { |     ) { | ||||||
|         const type = key["onUp"] ? "keyup" : "keypress" |         const type = key["onUp"] ? "keyup" : "keypress" | ||||||
|         let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] |         let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| <a | <a | ||||||
|   {href} |   {href} | ||||||
|   aria-label={ariaLabel} |   aria-label={ariaLabel} | ||||||
|  |   title={ariaLabel} | ||||||
|   target={newTab ? "_blank" : undefined} |   target={newTab ? "_blank" : undefined} | ||||||
|   {download} |   {download} | ||||||
|   class={classnames} |   class={classnames} | ||||||
|  |  | ||||||
							
								
								
									
										36
									
								
								src/UI/BigComponents/MapCenterDetails.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/UI/BigComponents/MapCenterDetails.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { Store } from "../../Logic/UIEventSource" | ||||||
|  |   import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|  |   import Tr from "../Base/Tr.svelte" | ||||||
|  |   import Translations from "../i18n/Translations" | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Indicates how far away the viewport center is from the current user location | ||||||
|  |    */ | ||||||
|  |   export let state: ThemeViewState | ||||||
|  |   const t = Translations.t.general.visualFeedback | ||||||
|  |   let map = state.mapProperties | ||||||
|  | 
 | ||||||
|  |   let currentLocation = state.geolocation.geolocationState.currentGPSLocation | ||||||
|  |   let distanceToCurrentLocation: Store<{ distance: string, distanceInMeters: number, bearing: number }> = map.location.mapD(({ lon, lat }) => { | ||||||
|  |     const current = currentLocation.data | ||||||
|  |     if (!current) { | ||||||
|  |       return undefined | ||||||
|  |     } | ||||||
|  |     const gps: [number, number] = [current.longitude, current.latitude] | ||||||
|  |     const mapCenter: [number, number] = [lon, lat] | ||||||
|  |     const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter)) | ||||||
|  |     const distance = GeoOperations.distanceToHuman(distanceInMeters) | ||||||
|  |     const bearing = Math.round(GeoOperations.bearing(gps, mapCenter)) | ||||||
|  |     return { distance, bearing, distanceInMeters } | ||||||
|  |   }, [currentLocation]) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if $currentLocation !== undefined} | ||||||
|  |   {#if $distanceToCurrentLocation.distanceInMeters < 20} | ||||||
|  |     <Tr t={t.viewportCenterCloseToGps} /> | ||||||
|  |   {:else} | ||||||
|  |     <Tr t={t.viewportCenterDetails.Subs($distanceToCurrentLocation)} /> | ||||||
|  |   {/if} | ||||||
|  | {/if} | ||||||
|  | @ -8,8 +8,11 @@ | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import Locale from "../i18n/Locale" |   import Locale from "../i18n/Locale" | ||||||
|  |   import MapCenterDetails from "./MapCenterDetails.svelte" | ||||||
|  |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
| 
 | 
 | ||||||
|   export let mapProperties: MapProperties |   export let state: ThemeViewState | ||||||
|  |   let mapProperties = state.mapProperties | ||||||
|   let lastDisplayed: Date = undefined |   let lastDisplayed: Date = undefined | ||||||
|   let currentLocation: string = undefined |   let currentLocation: string = undefined | ||||||
| 
 | 
 | ||||||
|  | @ -51,8 +54,9 @@ | ||||||
|   <div |   <div | ||||||
|     role="alert" |     role="alert" | ||||||
|     aria-live="assertive" |     aria-live="assertive" | ||||||
|     class="normal-background border-interactive rounded-full px-2" |     class="normal-background border-interactive rounded-full px-2 flex flex-col items-center" | ||||||
|   > |   > | ||||||
|     {currentLocation} |     {currentLocation} | ||||||
|  |     <MapCenterDetails {state}/> | ||||||
|   </div> |   </div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -31,8 +31,11 @@ | ||||||
|     <div class="flex flex-col"> |     <div class="flex flex-col"> | ||||||
|       <!-- Title element--> |       <!-- Title element--> | ||||||
|       <h3> |       <h3> | ||||||
|  |         <a href={`#${$tags.id}`}> | ||||||
|         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> |         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> | ||||||
|  |         </a> | ||||||
|       </h3> |       </h3> | ||||||
|  |        | ||||||
|       <div |       <div | ||||||
|         class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1" |         class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1" | ||||||
|       > |       > | ||||||
|  |  | ||||||
|  | @ -3,8 +3,7 @@ | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" |   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" |   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||||
|   import { GeoOperations } from "../../Logic/GeoOperations" |   import DirectionIndicator from "../Base/DirectionIndicator.svelte" | ||||||
|   import { Store } from "../../Logic/UIEventSource" |  | ||||||
| 
 | 
 | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   export let feature: Feature |   export let feature: Feature | ||||||
|  | @ -13,30 +12,13 @@ | ||||||
|   let tags = state.featureProperties.getStore(id) |   let tags = state.featureProperties.getStore(id) | ||||||
|   let layer: LayerConfig = state.layout.getMatchingLayer(tags.data) |   let layer: LayerConfig = state.layout.getMatchingLayer(tags.data) | ||||||
| 
 | 
 | ||||||
|   function select() { |  | ||||||
|     state.selectedElement.setData(undefined) |  | ||||||
|     state.selectedLayer.setData(layer) |  | ||||||
|     state.selectedElement.setData(feature) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map( |  | ||||||
|     (l) => { |  | ||||||
|       let fcenter = GeoOperations.centerpointCoordinates(feature) |  | ||||||
|       let mapCenter = [l.lon, l.lat] |  | ||||||
| 
 |  | ||||||
|       let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter)) |  | ||||||
|       let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter)) |  | ||||||
|       return { bearing, dist } |  | ||||||
|     } |  | ||||||
|   ) |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="small flex cursor-pointer" on:click={() => select()}> | <a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}> | ||||||
|   <span class="flex"> |   {#if i !== undefined} | ||||||
|     {#if i !== undefined} |     <span class="font-bold">{i + 1}   </span> | ||||||
|       <span class="font-bold">{i + 1}.</span> |   {/if} | ||||||
|     {/if} |   <TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state} | ||||||
|     <TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} /> |                       {tags} /> | ||||||
|     {$bearingAndDist.dist}m {$bearingAndDist.bearing}° |   <DirectionIndicator {feature} {state} /> | ||||||
|   </span> | </a> | ||||||
| </div> |  | ||||||
|  |  | ||||||
|  | @ -7,18 +7,24 @@ | ||||||
|   import ThemeViewState from "../../Models/ThemeViewState" |   import ThemeViewState from "../../Models/ThemeViewState" | ||||||
|   import Summary from "./Summary.svelte" |   import Summary from "./Summary.svelte" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import type { KeyNavigationEvent } from "../../Models/MapProperties" |   import type { KeyNavigationEvent } from "../../Models/MapProperties" | ||||||
|   import type { Feature } from "geojson" |   import MapCenterDetails from "./MapCenterDetails.svelte" | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   export let featuresInViewPort: Store<Feature[]> |  | ||||||
|   console.log("Visual feedback panel:", featuresInViewPort) |  | ||||||
|   const t = Translations.t.general.visualFeedback |   const t = Translations.t.general.visualFeedback | ||||||
|  |   let map = state.mapProperties | ||||||
|   let centerFeatures = state.closestFeatures.features |   let centerFeatures = state.closestFeatures.features | ||||||
|  |   let translationWithLength = centerFeatures.mapD(cf => cf.length).mapD(n => { | ||||||
|  |     if (n === 1) { | ||||||
|  |       return t.oneFeatureInView | ||||||
|  |     } | ||||||
|  |     return t.closestFeaturesAre.Subs({ n }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   let lastAction: UIEventSource<KeyNavigationEvent> = new UIEventSource<KeyNavigationEvent>( |   let lastAction: UIEventSource<KeyNavigationEvent> = new UIEventSource<KeyNavigationEvent>( | ||||||
|     undefined |     undefined, | ||||||
|   ) |   ) | ||||||
|   state.mapProperties.onKeyNavigationEvent((event) => { |   state.mapProperties.onKeyNavigationEvent((event) => { | ||||||
|     lastAction.setData(event) |     lastAction.setData(event) | ||||||
|  | @ -26,17 +32,23 @@ | ||||||
|   lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined)) |   lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined)) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div aria-live="assertive" class="p-1" role="alert"> | <div aria-live="assertive" class="p-1 interactive" role="alert"> | ||||||
|   {#if $lastAction !== undefined} |   {#if $lastAction?.key === "out"} | ||||||
|  |     <Tr t={t.out.Subs({z: map.zoom.data - 1})} /> | ||||||
|  |   {:else if $lastAction?.key === "in"} | ||||||
|  |     <Tr t={t.out.Subs({z: map.zoom.data + 1})} /> | ||||||
|  |   {:else if $lastAction !== undefined} | ||||||
|     <Tr t={t[$lastAction.key]} /> |     <Tr t={t[$lastAction.key]} /> | ||||||
|   {:else if $centerFeatures.length === 0} |   {:else if $centerFeatures?.length === 0} | ||||||
|     <Tr t={t.noCloseFeatures} /> |     <Tr t={t.noCloseFeatures} /> | ||||||
|   {:else} |     <MapCenterDetails {state} /> | ||||||
|  |   {:else if $centerFeatures !== undefined} | ||||||
|     <div class="pointer-events-auto"> |     <div class="pointer-events-auto"> | ||||||
|       <Tr t={t.closestFeaturesAre.Subs({ n: $featuresInViewPort?.length })} /> |       <Tr t={$translationWithLength} /> | ||||||
|       <ol class="list-none"> |       <MapCenterDetails {state} /> | ||||||
|         {#each $centerFeatures as feat, i (feat.properties.id)} |       <ol> | ||||||
|           <li class="flex"> |         {#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)} | ||||||
|  |           <li> | ||||||
|             <Summary {state} feature={feat} {i} /> |             <Summary {state} feature={feat} {i} /> | ||||||
|           </li> |           </li> | ||||||
|         {/each} |         {/each} | ||||||
|  |  | ||||||
|  | @ -33,15 +33,7 @@ | ||||||
|   const coord = GeoOperations.centerpointCoordinates(feature) |   const coord = GeoOperations.centerpointCoordinates(feature) | ||||||
|   const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => { |   const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => { | ||||||
|     let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat])) |     let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat])) | ||||||
| 
 |     return GeoOperations.distanceToHuman(meters) | ||||||
|     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"] |   const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"] | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -952,17 +952,21 @@ export class ToTextualDescription { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (OH.weekdaysIdentical(ranges, 0, 4)) { |         if (OH.weekdaysIdentical(ranges, 0, 4)) { | ||||||
|             result.push( |             if (ranges[0].length > 0) { | ||||||
|                 t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) }) |                 result.push( | ||||||
|             ) |                     t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) }) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             addRange(0, 4) |             addRange(0, 4) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (OH.weekdaysIdentical(ranges, 5, 6)) { |         if (OH.weekdaysIdentical(ranges, 5, 6)) { | ||||||
|             result.push( |             if (ranges[5].length > 0) { | ||||||
|                 t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) }) |                 result.push( | ||||||
|             ) |                     t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) }) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             addRange(5, 6) |             addRange(5, 6) | ||||||
|         } |         } | ||||||
|  | @ -983,7 +987,6 @@ export class ToTextualDescription { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createRangeFor(range: OpeningRange): Translation { |     private static createRangeFor(range: OpeningRange): Translation { | ||||||
|         console.log(">>>", range) |  | ||||||
|         return Translations.t.general.opening_hours.ranges.Subs({ |         return Translations.t.general.opening_hours.ranges.Subs({ | ||||||
|             starttime: ToTextualDescription.timeString(range.startDate), |             starttime: ToTextualDescription.timeString(range.startDate), | ||||||
|             endtime: ToTextualDescription.timeString(range.endDate), |             endtime: ToTextualDescription.timeString(range.endDate), | ||||||
|  | @ -991,6 +994,9 @@ export class ToTextualDescription { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createRangesFor(ranges: OpeningRange[]): Translation { |     private static createRangesFor(ranges: OpeningRange[]): Translation { | ||||||
|  |         if (ranges.length === 0) { | ||||||
|  |             //    return undefined
 | ||||||
|  |         } | ||||||
|         let tr = ToTextualDescription.createRangeFor(ranges[0]) |         let tr = ToTextualDescription.createRangeFor(ranges[0]) | ||||||
|         for (let i = 1; i < ranges.length; i++) { |         for (let i = 1; i < ranges.length; i++) { | ||||||
|             tr = Translations.t.general.opening_hours.rangescombined.Subs({ |             tr = Translations.t.general.opening_hours.rangescombined.Subs({ | ||||||
|  |  | ||||||
|  | @ -9,8 +9,9 @@ | ||||||
|   export let score: number |   export let score: number | ||||||
|   export let cutoff: number |   export let cutoff: number | ||||||
|   export let starSize = "w-h h-4" |   export let starSize = "w-h h-4" | ||||||
|  |   export let i: number | ||||||
| 
 | 
 | ||||||
|   let dispatch = createEventDispatcher<{ hover: { score: number } }>() |   let dispatch = createEventDispatcher<{ hover: { score: number },  click: { score: number } }>() | ||||||
|   let container: HTMLElement |   let container: HTMLElement | ||||||
| 
 | 
 | ||||||
|   function getScore(e: MouseEvent): number { |   function getScore(e: MouseEvent): number { | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ | ||||||
|    * Number between 0 and 100. Every 10 points, another half star is added |    * Number between 0 and 100. Every 10 points, another half star is added | ||||||
|    */ |    */ | ||||||
|   export let score: number |   export let score: number | ||||||
|   let dispatch = createEventDispatcher<{ hover: number; click: number }>() |  | ||||||
| 
 | 
 | ||||||
|   let cutoffs = [20, 40, 60, 80, 100] |   let cutoffs = [20, 40, 60, 80, 100] | ||||||
|   export let starSize = "w-h h-4" |   export let starSize = "w-h h-4" | ||||||
|  | @ -14,8 +13,8 @@ | ||||||
| 
 | 
 | ||||||
| {#if score !== undefined} | {#if score !== undefined} | ||||||
|   <div class="flex" on:mouseout> |   <div class="flex" on:mouseout> | ||||||
|     {#each cutoffs as cutoff} |     {#each cutoffs as cutoff, i} | ||||||
|       <StarElement {score} {cutoff} {starSize} on:hover on:click /> |       <StarElement {score} {i} {cutoff} {starSize} on:hover on:click /> | ||||||
|     {/each} |     {/each} | ||||||
|   </div> |   </div> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -13,13 +13,7 @@ | ||||||
|   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 { |   import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||||
|     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" | ||||||
|  | @ -72,9 +66,6 @@ | ||||||
|   import FilterPanel from "./BigComponents/FilterPanel.svelte" |   import FilterPanel from "./BigComponents/FilterPanel.svelte" | ||||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" |   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" | ||||||
|   import { BBox } from "../Logic/BBox" |   import { BBox } from "../Logic/BBox" | ||||||
|   import { GeoOperations } from "../Logic/GeoOperations" |  | ||||||
|   import ShowDataLayer from "./Map/ShowDataLayer" |  | ||||||
|   import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" |  | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|  | @ -102,37 +93,38 @@ | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => |   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => | ||||||
|     state.layout.getMatchingLayer(element.properties) |     state.layout.getMatchingLayer(element.properties), | ||||||
|   ) |   ) | ||||||
|   let currentZoom = state.mapProperties.zoom |   let currentZoom = state.mapProperties.zoom | ||||||
|   let showCrosshair = state.userRelatedState.showCrosshair |   let showCrosshair = state.userRelatedState.showCrosshair | ||||||
|   let visualFeedback = state.visualFeedback |   let visualFeedback = state.visualFeedback | ||||||
|   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) |   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) | ||||||
|   let featuresInViewPort: UIEventSource<Feature[]> = new UIEventSource<Feature[]>(undefined) |  | ||||||
|   viewport.addCallbackAndRunD((viewport) => { |  | ||||||
|     state.featuresInView.features.addCallbackAndRunD((features: Feature[]) => { |  | ||||||
|       const rect = viewport.getBoundingClientRect() |  | ||||||
|       const mlmap = state.map.data |  | ||||||
|       if (!mlmap) { |  | ||||||
|         return undefined |  | ||||||
|       } |  | ||||||
|       const topLeft = mlmap.unproject([rect.left, rect.top]) |  | ||||||
|       const bottomRight = mlmap.unproject([rect.right, rect.bottom]) |  | ||||||
|       const bbox = new BBox([ |  | ||||||
|         [topLeft.lng, topLeft.lat], |  | ||||||
|         [bottomRight.lng, bottomRight.lat], |  | ||||||
|       ]) |  | ||||||
|       const bboxGeo = bbox.asGeoJson({}) |  | ||||||
|       console.log("BBOX:", bboxGeo) |  | ||||||
| 
 |  | ||||||
|       const filtered = features.filter((f: Feature) => { |  | ||||||
|         console.log(f, bboxGeo) |  | ||||||
|         return GeoOperations.calculateOverlap(bboxGeo, [f]).length > 0 |  | ||||||
|       }) |  | ||||||
|       featuresInViewPort.setData(filtered) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
|   let mapproperties: MapProperties = state.mapProperties |   let mapproperties: MapProperties = state.mapProperties | ||||||
|  | 
 | ||||||
|  |   function updateViewport() { | ||||||
|  |     const rect = viewport.data?.getBoundingClientRect() | ||||||
|  |     if (!rect) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     const mlmap = state.map.data | ||||||
|  |     if (!mlmap) { | ||||||
|  |       return undefined | ||||||
|  |     } | ||||||
|  |     const topLeft = mlmap.unproject([rect.left, rect.top]) | ||||||
|  |     const bottomRight = mlmap.unproject([rect.right, rect.bottom]) | ||||||
|  |     const bbox = new BBox([ | ||||||
|  |       [topLeft.lng, topLeft.lat], | ||||||
|  |       [bottomRight.lng, bottomRight.lat], | ||||||
|  |     ]) | ||||||
|  |     state.visualFeedbackViewportBounds.setData(bbox) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   viewport.addCallbackAndRunD(_ => { | ||||||
|  |     updateViewport() | ||||||
|  |   }) | ||||||
|  |   mapproperties.bounds.addCallbackAndRunD(_ => { | ||||||
|  |     updateViewport() | ||||||
|  |   }) | ||||||
|   let featureSwitches: FeatureSwitchState = state.featureSwitches |   let featureSwitches: FeatureSwitchState = state.featureSwitches | ||||||
|   let availableLayers = state.availableLayers |   let availableLayers = state.availableLayers | ||||||
|   let currentViewLayer = layout.layers.find((l) => l.id === "current_view") |   let currentViewLayer = layout.layers.find((l) => l.id === "current_view") | ||||||
|  | @ -142,7 +134,7 @@ | ||||||
|   onDestroy( |   onDestroy( | ||||||
|     rasterLayer.addCallbackAndRunD((l) => { |     rasterLayer.addCallbackAndRunD((l) => { | ||||||
|       rasterLayerName = l.properties.name |       rasterLayerName = l.properties.name | ||||||
|     }) |     }), | ||||||
|   ) |   ) | ||||||
|   let previewedImage = state.previewedImage |   let previewedImage = state.previewedImage | ||||||
| 
 | 
 | ||||||
|  | @ -173,8 +165,14 @@ | ||||||
| 
 | 
 | ||||||
| <div class="pointer-events-none absolute top-0 left-0 w-full"> | <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||||
|   <!-- Top components --> |   <!-- Top components --> | ||||||
|   <If condition={state.featureSwitches.featureSwitchSearch}> | 
 | ||||||
|     <div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2"> |   <div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2 flex flex-col"> | ||||||
|  |     <If condition={state.visualFeedback}> | ||||||
|  |       <div class="w-fit"> | ||||||
|  |         <VisualFeedbackPanel {state} /> | ||||||
|  |       </div> | ||||||
|  |     </If> | ||||||
|  |     <If condition={state.featureSwitches.featureSwitchSearch}> | ||||||
|       <Geosearch |       <Geosearch | ||||||
|         bounds={state.mapProperties.bounds} |         bounds={state.mapProperties.bounds} | ||||||
|         on:searchCompleted={() => { |         on:searchCompleted={() => { | ||||||
|  | @ -183,8 +181,8 @@ | ||||||
|         perLayer={state.perLayer} |         perLayer={state.perLayer} | ||||||
|         selectedElement={state.selectedElement} |         selectedElement={state.selectedElement} | ||||||
|       /> |       /> | ||||||
|     </div> |     </If> | ||||||
|   </If> |   </div> | ||||||
|   <div class="float-left m-1 flex flex-col sm:mt-2"> |   <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||||
|     <MapControlButton |     <MapControlButton | ||||||
|       on:click={() => state.guistate.themeIsOpened.setData(true)} |       on:click={() => state.guistate.themeIsOpened.setData(true)} | ||||||
|  | @ -229,7 +227,7 @@ | ||||||
|     <!-- Flex and w-full are needed for the positioning --> |     <!-- Flex and w-full are needed for the positioning --> | ||||||
|     <!-- Centermessage --> |     <!-- Centermessage --> | ||||||
|     <StateIndicator {state} /> |     <StateIndicator {state} /> | ||||||
|     <ReverseGeocoding mapProperties={mapproperties} /> |     <ReverseGeocoding {state} /> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  | @ -280,9 +278,6 @@ | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <If condition={state.visualFeedback}> |  | ||||||
|       <VisualFeedbackPanel {state} {featuresInViewPort} /> |  | ||||||
|     </If> |  | ||||||
| 
 | 
 | ||||||
|     <div class="flex flex-col items-end"> |     <div class="flex flex-col items-end"> | ||||||
|       <!-- bottom right elements --> |       <!-- bottom right elements --> | ||||||
|  |  | ||||||
|  | @ -1,28 +1,21 @@ | ||||||
| import { Translation } from "../UI/i18n/Translation" | import { Translation } from "../UI/i18n/Translation" | ||||||
| import Locale from "../UI/i18n/Locale" | import { Store } from "../Logic/UIEventSource" | ||||||
| 
 | 
 | ||||||
| export function ariaLabel(htmlElement: Element, t: Translation) { | export function ariaLabel(htmlElement: Element, t: Translation) { | ||||||
|  |     ariaLabelStore(htmlElement, t?.current) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function ariaLabelStore(htmlElement: Element, t: Store<string>) { | ||||||
|     if (!t) { |     if (!t) { | ||||||
|         return |         return | ||||||
|     } |     } | ||||||
|     let destroy: () => void = undefined |     let destroy: () => void = undefined | ||||||
| 
 | 
 | ||||||
|     Locale.language.map((language) => { |     t?.mapD( | ||||||
|         if (!t.translations[language]) { |  | ||||||
|             console.log( |  | ||||||
|                 "No aria label in", |  | ||||||
|                 language, |  | ||||||
|                 "for", |  | ||||||
|                 t.context, |  | ||||||
|                 "; en is", |  | ||||||
|                 t.translations["en"] |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     t.current.map( |  | ||||||
|         (label) => { |         (label) => { | ||||||
|             htmlElement.setAttribute("aria-label", label) |             htmlElement.setAttribute("aria-label", label) | ||||||
|  |             // Set the tooltip, which is the 'title' attribute of an html-element
 | ||||||
|  |             htmlElement.setAttribute("title", label) | ||||||
|         }, |         }, | ||||||
|         [], |         [], | ||||||
|         (f) => { |         (f) => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue