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