More work on A11y

This commit is contained in:
Pieter Vander Vennet 2023-12-21 17:36:43 +01:00
parent 87aee9e2b7
commit 6da72b80ef
28 changed files with 398 additions and 209 deletions

View file

@ -288,4 +288,8 @@ export class BBox {
throw "BBOX has NAN"
}
}
public overlapsWithFeature(f: Feature) {
return GeoOperations.calculateOverlap(this.asGeoJson({}), [f]).length > 0
}
}

View file

@ -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]
)
}
}

View file

@ -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]
}
}

View file

@ -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(/&lt;/g, "<")?.replace(/&gt;/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(/&lt;/g,'<')?.replace(/&gt;/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'
}
}

View file

@ -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,