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
				
			
		|  | @ -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, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue