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=", | ||||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "mapcomplete-preferred-background-layer=", | ||||
|           "if": "mapcomplete-preferred-background-layer=default", | ||||
|           "alsoShowIf": "mapcomplete-preferred-background-layer=", | ||||
|           "then": { | ||||
|             "en": "Use the default background layer", | ||||
|             "ca": "Utilitzeu la capa de fons predeterminada", | ||||
|  |  | |||
|  | @ -94,10 +94,10 @@ | |||
|             "backToSelect": "Vælg en anden kategori", | ||||
|             "confirmButton": "Tilføj en {category}<br><div class=\"alert\">Din tilføjelse er synlig for alle</div>", | ||||
|             "confirmLocation": "Bekræft dette sted", | ||||
|             "confirmTitle": "Tilføj en {titel}?", | ||||
|             "confirmTitle": "Tilføj en {title}?", | ||||
|             "disableFilters": "Slå alle filtre fra", | ||||
|             "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", | ||||
|             "import": { | ||||
|                 "hasBeenImported": "Objektet blev importeret", | ||||
|  | @ -125,8 +125,8 @@ | |||
|             "isApplied": "Ændringerne er anvendt" | ||||
|         }, | ||||
|         "attribution": { | ||||
|             "attributionBackgroundLayer": "Det nuværende baggrundslag er {navn}", | ||||
|             "attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er [navn}: {copyright}", | ||||
|             "attributionBackgroundLayer": "Det nuværende baggrundslag er {name}", | ||||
|             "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>", | ||||
|             "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>", | ||||
|  | @ -253,7 +253,7 @@ | |||
|         "pickLanguage": "Vælg et sprog: ", | ||||
|         "poweredByOsm": "Drevet af OpenStreetMap", | ||||
|         "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", | ||||
|             "answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål", | ||||
|             "answeredOne": "Du besvarede ét spørgsmål", | ||||
|  |  | |||
|  | @ -398,17 +398,20 @@ | |||
|         "useSearch": "Use the search above to see presets", | ||||
|         "useSearchForMore": "Use the search function to search within {total} more values…", | ||||
|         "visualFeedback": { | ||||
|             "closestFeaturesAre": "{n} features within view", | ||||
|             "closestFeaturesAre": "{n} features within viewport.", | ||||
|             "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.", | ||||
|             "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.", | ||||
|             "noCloseFeatures": "No features in view", | ||||
|             "north": "Moving north", | ||||
|             "out": "Zooming out", | ||||
|             "oneFeatureInView": "One feature within viewport.", | ||||
|             "out": "Zooming out to level {z}", | ||||
|             "south": "Moving south", | ||||
|             "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" | ||||
|         }, | ||||
|         "waitingForGeopermission": "Waiting for your permission to use the geolocation…", | ||||
|  |  | |||
|  | @ -370,15 +370,15 @@ | |||
|         "useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien", | ||||
|         "useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…", | ||||
|         "visualFeedback": { | ||||
|             "closestFeaturesAre": "{n} object in in beeld", | ||||
|             "closestFeaturesAre": "{n} object in beeld.", | ||||
|             "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.", | ||||
|             "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.", | ||||
|             "noCloseFeatures": "Niet in beeld", | ||||
|             "north": "Naar het noorden", | ||||
|             "out": "Aan het uitzoomen", | ||||
|             "out": "Aan het uitzoomen naar zoomlevel {z}", | ||||
|             "south": "Naar het zuiden", | ||||
|             "unlocked": "Bewegen ontgrendeld", | ||||
|             "west": "Naar het westen" | ||||
|  |  | |||
|  | @ -1342,10 +1342,6 @@ video { | |||
|   resize: both; | ||||
| } | ||||
| 
 | ||||
| .list-none { | ||||
|   list-style-type: none; | ||||
| } | ||||
| 
 | ||||
| .appearance-none { | ||||
|   -webkit-appearance: none; | ||||
|           appearance: none; | ||||
|  | @ -1906,6 +1902,11 @@ video { | |||
|   line-height: 1; | ||||
| } | ||||
| 
 | ||||
| .text-sm { | ||||
|   font-size: 0.875rem; | ||||
|   line-height: 1.25rem; | ||||
| } | ||||
| 
 | ||||
| .text-3xl { | ||||
|   font-size: 1.875rem; | ||||
|   line-height: 2.25rem; | ||||
|  | @ -1916,11 +1917,6 @@ video { | |||
|   line-height: 2rem; | ||||
| } | ||||
| 
 | ||||
| .text-sm { | ||||
|   font-size: 0.875rem; | ||||
|   line-height: 1.25rem; | ||||
| } | ||||
| 
 | ||||
| .text-4xl { | ||||
|   font-size: 2.25rem; | ||||
|   line-height: 2.5rem; | ||||
|  |  | |||
|  | @ -288,4 +288,8 @@ export class BBox { | |||
|             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 FilteringFeatureSource from "./FilteringFeatureSource" | ||||
| import LayerState from "../../State/LayerState" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export default class NearbyFeatureSource implements FeatureSource { | ||||
|     private readonly _result = new UIEventSource<Feature[]>(undefined) | ||||
| 
 | ||||
|     public readonly features: Store<Feature[]> | ||||
|     private readonly _result = new UIEventSource<Feature[]>(undefined) | ||||
|     private readonly _targetPoint: Store<{ lon: number; lat: number }> | ||||
|     private readonly _numberOfNeededFeatures: number | ||||
|     private readonly _layerState?: LayerState | ||||
|     private readonly _currentZoom: Store<number> | ||||
|     private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = [] | ||||
| 
 | ||||
|     private readonly _bounds: Store<BBox> | undefined | ||||
|     constructor( | ||||
|         targetPoint: Store<{ lon: number; lat: number }>, | ||||
|         sources: ReadonlyMap<string, FilteringFeatureSource>, | ||||
|         numberOfNeededFeatures?: number, | ||||
|         layerState?: LayerState, | ||||
|         currentZoom?: Store<number> | ||||
|         options?: { | ||||
|             bounds?: Store<BBox> | ||||
|             numberOfNeededFeatures?: number | ||||
|             layerState?: LayerState | ||||
|             currentZoom?: Store<number> | ||||
|         } | ||||
|     ) { | ||||
|         this._layerState = layerState | ||||
|         this._layerState = options?.layerState | ||||
|         this._targetPoint = targetPoint.stabilized(100) | ||||
|         this._numberOfNeededFeatures = numberOfNeededFeatures | ||||
|         this._currentZoom = currentZoom.stabilized(500) | ||||
|         this._numberOfNeededFeatures = options?.numberOfNeededFeatures | ||||
|         this._currentZoom = options?.currentZoom.stabilized(500) | ||||
|         this._bounds = options?.bounds | ||||
| 
 | ||||
|         this.features = Stores.ListStabilized(this._result) | ||||
| 
 | ||||
|  | @ -53,6 +57,10 @@ export default class NearbyFeatureSource implements FeatureSource { | |||
|     private update() { | ||||
|         let features: { feat: Feature; d: number }[] = [] | ||||
|         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.sort((a, b) => a.d - b.d) | ||||
|  | @ -80,6 +88,15 @@ export default class NearbyFeatureSource implements FeatureSource { | |||
|                 if (this._currentZoom.data < minZoom) { | ||||
|                     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 lonLat = <[number, number]>[point.lon, point.lat] | ||||
|                 const withDistance = feats.map((feat) => ({ | ||||
|  | @ -95,7 +112,7 @@ export default class NearbyFeatureSource implements FeatureSource { | |||
|                 } | ||||
|                 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
 | ||||
|      * 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 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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" | ||||
| /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | ||||
| export class ThemeMetaTagging { | ||||
|     public static readonly themeName = "usersettings" | ||||
|    public static readonly themeName = "usersettings" | ||||
| 
 | ||||
|     public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { | ||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => | ||||
|             feat.properties._description | ||||
|                 .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) | ||||
|                 ?.at(1) | ||||
|         ) | ||||
|         Utils.AddLazyProperty( | ||||
|             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" | ||||
|     } | ||||
| } | ||||
|    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { | ||||
|       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )  | ||||
|       Utils.AddLazyProperty(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() { | ||||
|             source.setData(new Date()) | ||||
|             if (Utils.runningFromConsole) { | ||||
|                 return | ||||
|             } | ||||
|             if (asLong === undefined || asLong()) { | ||||
|                 window.setTimeout(run, millis) | ||||
|             } | ||||
|  | @ -104,7 +107,8 @@ export abstract class Store<T> implements Readable<T> { | |||
|     M | ||||
|     public mapD<J>( | ||||
|         f: (t: Exclude<T, undefined | null>) => J, | ||||
|         extraStoresToWatch?: Store<any>[] | ||||
|         extraStoresToWatch?: Store<any>[], | ||||
|         callbackDestroyFunction?: (f: () => void) => void | ||||
|     ): Store<J> { | ||||
|         return this.map((t) => { | ||||
|             if (t === undefined) { | ||||
|  | @ -263,7 +267,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|     /** | ||||
|      * Converts the uiEventSource into a promise. | ||||
|      * 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 | ||||
|      */ | ||||
|     public AsPromise(condition?: (t: T) => boolean): Promise<T> { | ||||
|  | @ -482,7 +486,7 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|             stores = [] | ||||
|         } | ||||
|         if (extraStores?.length > 0) { | ||||
|             stores.push(...extraStores) | ||||
|             stores?.push(...extraStores) | ||||
|         } | ||||
|         if (this._extraStores?.length > 0) { | ||||
|             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 | ||||
|      * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' | ||||
|      * @param f: The transforming function | ||||
|      * @param extraSources: also trigger the update if one of these sources change | ||||
|      * @param onDestroy: a callback that can trigger the destroy function | ||||
|      * @param f The transforming function | ||||
|      * @param extraSources also trigger the update if one of these sources change | ||||
|      * @param onDestroy a callback that can trigger the destroy function | ||||
|      * | ||||
|      * const src = new UIEventSource<number>(10) | ||||
|      * const store = src.map(i => i * 2) | ||||
|  | @ -802,7 +806,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|      */ | ||||
|     public mapD<J>( | ||||
|         f: (t: Exclude<T, undefined | null>) => J, | ||||
|         extraSources: Store<any>[] = [] | ||||
|         extraSources: Store<any>[] = [], | ||||
|         callbackDestroyFunction?: (f: () => void) => void | ||||
|     ): Store<J | undefined> { | ||||
|         return new MappedStore( | ||||
|             this, | ||||
|  | @ -819,17 +824,18 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|             this._callbacks, | ||||
|             this.data === undefined || this.data === null | ||||
|                 ? <undefined | null>this.data | ||||
|                 : f(<any>this.data) | ||||
|                 : f(<any>this.data), | ||||
|             callbackDestroyFunction | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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)' | ||||
|      * @param f: The transforming function | ||||
|      * @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 allowUnregister: if set, the update will be halted if no listeners are registered | ||||
|      * @param f The transforming function | ||||
|      * @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 allowUnregister if set, the update will be halted if no listeners are registered | ||||
|      */ | ||||
|     public sync<J>( | ||||
|         f: (t: T) => J, | ||||
|  |  | |||
|  | @ -105,6 +105,12 @@ export interface MappingConfigJson { | |||
|      */ | ||||
|     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? | ||||
|      * | ||||
|  |  | |||
|  | @ -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}'} | ||||
|          */ | ||||
|         if: TagConfigJson | ||||
| 
 | ||||
|         /** | ||||
|          * question: What text should be shown? | ||||
|          * | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ export interface Icon {} | |||
| 
 | ||||
| export interface Mapping { | ||||
|     readonly if: UploadableTag | ||||
|     readonly alsoShowIf: Tag | undefined | ||||
|     readonly ifnot?: UploadableTag | ||||
|     readonly then: TypedTranslation<object> | ||||
|     readonly icon: string | ||||
|  | @ -383,7 +384,9 @@ export default class TagRenderingConfig { | |||
|             } | ||||
|         } | ||||
|         const prioritySearch = | ||||
|             mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined | ||||
|             mapping.priorityIf !== undefined | ||||
|                 ? TagUtils.Tag(mapping.priorityIf, `${ctx}.priorityIf`) | ||||
|                 : undefined | ||||
|         const mp = <Mapping>{ | ||||
|             if: TagUtils.Tag(mapping.if, `${ctx}.if`), | ||||
|             ifnot: | ||||
|  | @ -391,6 +394,10 @@ export default class TagRenderingConfig { | |||
|                     ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) | ||||
|                     : undefined, | ||||
|             then: Translations.T(mapping.then, `${ctx}.then`), | ||||
|             alsoShowIf: | ||||
|                 mapping.alsoShowIf !== undefined | ||||
|                     ? TagUtils.Tag(mapping.alsoShowIf, `${ctx}.alsoShowIf`) | ||||
|                     : undefined, | ||||
|             hideInAnswer, | ||||
|             icon, | ||||
|             iconClass, | ||||
|  | @ -530,6 +537,9 @@ export default class TagRenderingConfig { | |||
|                 if (mapping.if.matchesProperties(tags)) { | ||||
|                     return mapping | ||||
|                 } | ||||
|                 if (mapping.alsoShowIf?.matchesProperties(tags)) { | ||||
|                     return mapping | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -818,6 +828,7 @@ export default class TagRenderingConfig { | |||
|         for (const m of this.mappings ?? []) { | ||||
|             tags.push(m.if) | ||||
|             tags.push(m.priorityIf) | ||||
|             tags.push(m.alsoShowIf) | ||||
|             tags.push(...(m.addExtraTags ?? [])) | ||||
|             if (typeof m.hideInAnswer !== "boolean") { | ||||
|                 tags.push(m.hideInAnswer) | ||||
|  |  | |||
|  | @ -119,6 +119,11 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly previewedImage = new UIEventSource<ProvidedImage>(undefined) | ||||
| 
 | ||||
|     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 overlayLayerStates: ReadonlyMap< | ||||
|  | @ -351,9 +356,11 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.closestFeatures = new NearbyFeatureSource( | ||||
|             this.mapProperties.location, | ||||
|             this.perLayerFiltered, | ||||
|             3, | ||||
|             this.layerState, | ||||
|             this.mapProperties.zoom | ||||
|             { | ||||
|                 currentZoom: this.mapProperties.zoom, | ||||
|                 layerState: this.layerState, | ||||
|                 bounds: this.visualFeedbackViewportBounds, | ||||
|             } | ||||
|         ) | ||||
|         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView | ||||
|         this.imageUploadManager = new ImageUploadManager( | ||||
|  | @ -476,8 +483,18 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|      */ | ||||
|     private selectClosestAtCenter(i: number = 0) { | ||||
|         this.visualFeedback.setData(true) | ||||
|         const toSelect = this.closestFeatures.features.data[i] | ||||
|         const toSelect = this.closestFeatures.features?.data?.[i] | ||||
|         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 | ||||
|         } | ||||
|         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 | ||||
|         }, | ||||
|         documentation: string | Translation, | ||||
|         action: () => void | false | ||||
|         action: () => void | false, | ||||
|         alsoTriggeredOn?: Translation[] | ||||
|     ) { | ||||
|         const type = key["onUp"] ? "keyup" : "keypress" | ||||
|         let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
| <a | ||||
|   {href} | ||||
|   aria-label={ariaLabel} | ||||
|   title={ariaLabel} | ||||
|   target={newTab ? "_blank" : undefined} | ||||
|   {download} | ||||
|   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 Translations from "../i18n/Translations" | ||||
|   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 currentLocation: string = undefined | ||||
| 
 | ||||
|  | @ -51,8 +54,9 @@ | |||
|   <div | ||||
|     role="alert" | ||||
|     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} | ||||
|     <MapCenterDetails {state}/> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -31,8 +31,11 @@ | |||
|     <div class="flex flex-col"> | ||||
|       <!-- Title element--> | ||||
|       <h3> | ||||
|         <a href={`#${$tags.id}`}> | ||||
|         <TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} /> | ||||
|         </a> | ||||
|       </h3> | ||||
|        | ||||
|       <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" | ||||
|       > | ||||
|  |  | |||
|  | @ -3,8 +3,7 @@ | |||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||
|   import { GeoOperations } from "../../Logic/GeoOperations" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import DirectionIndicator from "../Base/DirectionIndicator.svelte" | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let feature: Feature | ||||
|  | @ -13,30 +12,13 @@ | |||
|   let tags = state.featureProperties.getStore(id) | ||||
|   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> | ||||
| 
 | ||||
| <div class="small flex cursor-pointer" on:click={() => select()}> | ||||
|   <span class="flex"> | ||||
|     {#if i !== undefined} | ||||
|       <span class="font-bold">{i + 1}.</span> | ||||
|     {/if} | ||||
|     <TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} /> | ||||
|     {$bearingAndDist.dist}m {$bearingAndDist.bearing}° | ||||
|   </span> | ||||
| </div> | ||||
| <a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}> | ||||
|   {#if i !== undefined} | ||||
|     <span class="font-bold">{i + 1}   </span> | ||||
|   {/if} | ||||
|   <TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state} | ||||
|                       {tags} /> | ||||
|   <DirectionIndicator {feature} {state} /> | ||||
| </a> | ||||
|  |  | |||
|  | @ -7,18 +7,24 @@ | |||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Summary from "./Summary.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 { Feature } from "geojson" | ||||
|   import MapCenterDetails from "./MapCenterDetails.svelte" | ||||
| 
 | ||||
|   export let state: ThemeViewState | ||||
|   export let featuresInViewPort: Store<Feature[]> | ||||
|   console.log("Visual feedback panel:", featuresInViewPort) | ||||
|   const t = Translations.t.general.visualFeedback | ||||
|   let map = state.mapProperties | ||||
|   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>( | ||||
|     undefined | ||||
|     undefined, | ||||
|   ) | ||||
|   state.mapProperties.onKeyNavigationEvent((event) => { | ||||
|     lastAction.setData(event) | ||||
|  | @ -26,17 +32,23 @@ | |||
|   lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined)) | ||||
| </script> | ||||
| 
 | ||||
| <div aria-live="assertive" class="p-1" role="alert"> | ||||
|   {#if $lastAction !== undefined} | ||||
| <div aria-live="assertive" class="p-1 interactive" role="alert"> | ||||
|   {#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]} /> | ||||
|   {:else if $centerFeatures.length === 0} | ||||
|   {:else if $centerFeatures?.length === 0} | ||||
|     <Tr t={t.noCloseFeatures} /> | ||||
|   {:else} | ||||
|     <MapCenterDetails {state} /> | ||||
|   {:else if $centerFeatures !== undefined} | ||||
|     <div class="pointer-events-auto"> | ||||
|       <Tr t={t.closestFeaturesAre.Subs({ n: $featuresInViewPort?.length })} /> | ||||
|       <ol class="list-none"> | ||||
|         {#each $centerFeatures as feat, i (feat.properties.id)} | ||||
|           <li class="flex"> | ||||
|       <Tr t={$translationWithLength} /> | ||||
|       <MapCenterDetails {state} /> | ||||
|       <ol> | ||||
|         {#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)} | ||||
|           <li> | ||||
|             <Summary {state} feature={feat} {i} /> | ||||
|           </li> | ||||
|         {/each} | ||||
|  |  | |||
|  | @ -33,15 +33,7 @@ | |||
|   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" | ||||
|     return GeoOperations.distanceToHuman(meters) | ||||
|   }) | ||||
|   const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"] | ||||
| </script> | ||||
|  |  | |||
|  | @ -952,17 +952,21 @@ export class ToTextualDescription { | |||
|         } | ||||
| 
 | ||||
|         if (OH.weekdaysIdentical(ranges, 0, 4)) { | ||||
|             result.push( | ||||
|                 t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) }) | ||||
|             ) | ||||
|             if (ranges[0].length > 0) { | ||||
|                 result.push( | ||||
|                     t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) }) | ||||
|                 ) | ||||
|             } | ||||
|         } else { | ||||
|             addRange(0, 4) | ||||
|         } | ||||
| 
 | ||||
|         if (OH.weekdaysIdentical(ranges, 5, 6)) { | ||||
|             result.push( | ||||
|                 t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) }) | ||||
|             ) | ||||
|             if (ranges[5].length > 0) { | ||||
|                 result.push( | ||||
|                     t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) }) | ||||
|                 ) | ||||
|             } | ||||
|         } else { | ||||
|             addRange(5, 6) | ||||
|         } | ||||
|  | @ -983,7 +987,6 @@ export class ToTextualDescription { | |||
|     } | ||||
| 
 | ||||
|     private static createRangeFor(range: OpeningRange): Translation { | ||||
|         console.log(">>>", range) | ||||
|         return Translations.t.general.opening_hours.ranges.Subs({ | ||||
|             starttime: ToTextualDescription.timeString(range.startDate), | ||||
|             endtime: ToTextualDescription.timeString(range.endDate), | ||||
|  | @ -991,6 +994,9 @@ export class ToTextualDescription { | |||
|     } | ||||
| 
 | ||||
|     private static createRangesFor(ranges: OpeningRange[]): Translation { | ||||
|         if (ranges.length === 0) { | ||||
|             //    return undefined
 | ||||
|         } | ||||
|         let tr = ToTextualDescription.createRangeFor(ranges[0]) | ||||
|         for (let i = 1; i < ranges.length; i++) { | ||||
|             tr = Translations.t.general.opening_hours.rangescombined.Subs({ | ||||
|  |  | |||
|  | @ -9,8 +9,9 @@ | |||
|   export let score: number | ||||
|   export let cutoff: number | ||||
|   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 | ||||
| 
 | ||||
|   function getScore(e: MouseEvent): number { | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
|    * Number between 0 and 100. Every 10 points, another half star is added | ||||
|    */ | ||||
|   export let score: number | ||||
|   let dispatch = createEventDispatcher<{ hover: number; click: number }>() | ||||
| 
 | ||||
|   let cutoffs = [20, 40, 60, 80, 100] | ||||
|   export let starSize = "w-h h-4" | ||||
|  | @ -14,8 +13,8 @@ | |||
| 
 | ||||
| {#if score !== undefined} | ||||
|   <div class="flex" on:mouseout> | ||||
|     {#each cutoffs as cutoff} | ||||
|       <StarElement {score} {cutoff} {starSize} on:hover on:click /> | ||||
|     {#each cutoffs as cutoff, i} | ||||
|       <StarElement {score} {i} {cutoff} {starSize} on:hover on:click /> | ||||
|     {/each} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -13,13 +13,7 @@ | |||
|   import type { MapProperties } from "../Models/MapProperties" | ||||
|   import Geosearch from "./BigComponents/Geosearch.svelte" | ||||
|   import Translations from "./i18n/Translations" | ||||
|   import { | ||||
|     CogIcon, | ||||
|     EyeIcon, | ||||
|     HeartIcon, | ||||
|     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 CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" | ||||
|   import FloatOver from "./Base/FloatOver.svelte" | ||||
|  | @ -72,9 +66,6 @@ | |||
|   import FilterPanel from "./BigComponents/FilterPanel.svelte" | ||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" | ||||
|   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 | ||||
|   let layout = state.layout | ||||
|  | @ -102,37 +93,38 @@ | |||
|   }) | ||||
| 
 | ||||
|   let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => | ||||
|     state.layout.getMatchingLayer(element.properties) | ||||
|     state.layout.getMatchingLayer(element.properties), | ||||
|   ) | ||||
|   let currentZoom = state.mapProperties.zoom | ||||
|   let showCrosshair = state.userRelatedState.showCrosshair | ||||
|   let visualFeedback = state.visualFeedback | ||||
|   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 | ||||
| 
 | ||||
|   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 availableLayers = state.availableLayers | ||||
|   let currentViewLayer = layout.layers.find((l) => l.id === "current_view") | ||||
|  | @ -142,7 +134,7 @@ | |||
|   onDestroy( | ||||
|     rasterLayer.addCallbackAndRunD((l) => { | ||||
|       rasterLayerName = l.properties.name | ||||
|     }) | ||||
|     }), | ||||
|   ) | ||||
|   let previewedImage = state.previewedImage | ||||
| 
 | ||||
|  | @ -173,8 +165,14 @@ | |||
| 
 | ||||
| <div class="pointer-events-none absolute top-0 left-0 w-full"> | ||||
|   <!-- 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 | ||||
|         bounds={state.mapProperties.bounds} | ||||
|         on:searchCompleted={() => { | ||||
|  | @ -183,8 +181,8 @@ | |||
|         perLayer={state.perLayer} | ||||
|         selectedElement={state.selectedElement} | ||||
|       /> | ||||
|     </div> | ||||
|   </If> | ||||
|     </If> | ||||
|   </div> | ||||
|   <div class="float-left m-1 flex flex-col sm:mt-2"> | ||||
|     <MapControlButton | ||||
|       on:click={() => state.guistate.themeIsOpened.setData(true)} | ||||
|  | @ -229,7 +227,7 @@ | |||
|     <!-- Flex and w-full are needed for the positioning --> | ||||
|     <!-- Centermessage --> | ||||
|     <StateIndicator {state} /> | ||||
|     <ReverseGeocoding mapProperties={mapproperties} /> | ||||
|     <ReverseGeocoding {state} /> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
|  | @ -280,9 +278,6 @@ | |||
|         </a> | ||||
|       </div> | ||||
|     </div> | ||||
|     <If condition={state.visualFeedback}> | ||||
|       <VisualFeedbackPanel {state} {featuresInViewPort} /> | ||||
|     </If> | ||||
| 
 | ||||
|     <div class="flex flex-col items-end"> | ||||
|       <!-- bottom right elements --> | ||||
|  |  | |||
|  | @ -1,28 +1,21 @@ | |||
| import { Translation } from "../UI/i18n/Translation" | ||||
| import Locale from "../UI/i18n/Locale" | ||||
| import { Store } from "../Logic/UIEventSource" | ||||
| 
 | ||||
| export function ariaLabel(htmlElement: Element, t: Translation) { | ||||
|     ariaLabelStore(htmlElement, t?.current) | ||||
| } | ||||
| 
 | ||||
| export function ariaLabelStore(htmlElement: Element, t: Store<string>) { | ||||
|     if (!t) { | ||||
|         return | ||||
|     } | ||||
|     let destroy: () => void = undefined | ||||
| 
 | ||||
|     Locale.language.map((language) => { | ||||
|         if (!t.translations[language]) { | ||||
|             console.log( | ||||
|                 "No aria label in", | ||||
|                 language, | ||||
|                 "for", | ||||
|                 t.context, | ||||
|                 "; en is", | ||||
|                 t.translations["en"] | ||||
|             ) | ||||
|         } | ||||
|     }) | ||||
| 
 | ||||
|     t.current.map( | ||||
|     t?.mapD( | ||||
|         (label) => { | ||||
|             htmlElement.setAttribute("aria-label", label) | ||||
|             // Set the tooltip, which is the 'title' attribute of an html-element
 | ||||
|             htmlElement.setAttribute("title", label) | ||||
|         }, | ||||
|         [], | ||||
|         (f) => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue