forked from MapComplete/MapComplete
More search functionality
This commit is contained in:
parent
5d0de8520b
commit
1c46a65c84
25 changed files with 962 additions and 846 deletions
|
@ -909,6 +909,7 @@ export class GeoOperations {
|
|||
|
||||
/**
|
||||
* GeoOperations.distanceToHuman(52.3) // => "50m"
|
||||
* GeoOperations.distanceToHuman(999) // => "1.0km"
|
||||
* GeoOperations.distanceToHuman(2800) // => "2.8km"
|
||||
* GeoOperations.distanceToHuman(12800) // => "13km"
|
||||
* GeoOperations.distanceToHuman(128000) // => "130km"
|
||||
|
@ -920,7 +921,7 @@ export class GeoOperations {
|
|||
if (meters === undefined) {
|
||||
return ""
|
||||
}
|
||||
meters = Math.round(meters)
|
||||
meters = Utils.roundHuman( Math.round(meters))
|
||||
if (meters < 1000) {
|
||||
return Utils.roundHuman(meters) + "m"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
|
||||
export default class CombinedSearcher implements GeocodingProvider {
|
||||
private _providers: ReadonlyArray<GeocodingProvider>
|
||||
|
@ -16,13 +17,13 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
* @param geocoded
|
||||
* @private
|
||||
*/
|
||||
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[]{
|
||||
const results : GeoCodeResult[] = []
|
||||
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] {
|
||||
const results: GeoCodeResult[] = []
|
||||
const seenIds = new Set<string>()
|
||||
for (const geocodedElement of geocoded) {
|
||||
for (const entry of geocodedElement) {
|
||||
const id = entry.osm_type+ entry.osm_id
|
||||
if(seenIds.has(id)){
|
||||
const id = entry.osm_type + entry.osm_id
|
||||
if (seenIds.has(id)) {
|
||||
continue
|
||||
}
|
||||
seenIds.add(id)
|
||||
|
@ -33,12 +34,12 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
const results = await Promise.all(this._providers.map(pr => pr.search(query, options)))
|
||||
return this.merge(results)
|
||||
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
|
||||
return results.flatMap(x => x)
|
||||
}
|
||||
|
||||
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||
return this.merge(results)
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
|
||||
/**
|
||||
* A simple search-class which interprets possible locations
|
||||
|
@ -17,28 +18,25 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
]
|
||||
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @param options
|
||||
*
|
||||
* const ls = new CoordinateSearch()
|
||||
* const results = await ls.search("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
|
||||
* const results = ls.directSearch("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
|
||||
* results.length // => 1
|
||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
||||
*
|
||||
* const ls = new CoordinateSearch()
|
||||
* const results = await ls.search("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
|
||||
* const results = ls.directSearch("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
|
||||
* results.length // => 1
|
||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
||||
*
|
||||
* const ls = new CoordinateSearch()
|
||||
* const results = await ls.search("51.2611 3.2217")
|
||||
* const results = ls.directSearch("51.2611 3.2217")
|
||||
* results.length // => 2
|
||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinateSearch"}
|
||||
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinateSearch"}
|
||||
*
|
||||
*/
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
private directSearch(query: string): GeoCodeResult[] {
|
||||
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
|
||||
lat: Number(m[1]),
|
||||
|
@ -49,8 +47,7 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
})
|
||||
|
||||
|
||||
|
||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||
.map(m => <GeoCodeResult>{
|
||||
lat: Number(m[2]),
|
||||
lon: Number(m[1]),
|
||||
|
@ -58,12 +55,15 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
source: "coordinateSearch",
|
||||
category: "coordinate"
|
||||
})
|
||||
|
||||
return matches.concat(matchesLonLat)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.search(query, options)
|
||||
suggest(query: string): Store<GeoCodeResult[]> {
|
||||
return new ImmutableStore(this.directSearch(query))
|
||||
}
|
||||
|
||||
async search (query: string): Promise<GeoCodeResult[]> {
|
||||
return this.directSearch(query)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { BBox } from "../BBox"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
import { DefaultPinIcon } from "../../Models/Constants"
|
||||
import { Store } from "../UIEventSource"
|
||||
|
||||
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport"
|
||||
|
||||
|
@ -42,7 +43,7 @@ export default interface GeocodingProvider {
|
|||
* @param query
|
||||
* @param options
|
||||
*/
|
||||
suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
|
||||
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]>
|
||||
}
|
||||
|
||||
export type ReverseGeocodingResult = Feature<Geometry,{
|
||||
|
|
|
@ -3,7 +3,19 @@ import ThemeViewState from "../../Models/ThemeViewState"
|
|||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
||||
|
||||
type IntermediateResult = {
|
||||
feature: Feature,
|
||||
/**
|
||||
* Lon, lat
|
||||
*/
|
||||
center: [number, number],
|
||||
levehnsteinD: number,
|
||||
physicalDistance: number,
|
||||
searchTerms: string[],
|
||||
description: string
|
||||
}
|
||||
export default class LocalElementSearch implements GeocodingProvider {
|
||||
private readonly _state: ThemeViewState
|
||||
private readonly _limit: number
|
||||
|
@ -15,86 +27,91 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.searchEntries(query, options, false)
|
||||
return this.searchEntries(query, options, false).data
|
||||
}
|
||||
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] {
|
||||
private getPartialResult(query: string, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
|
||||
const results: IntermediateResult [] = []
|
||||
|
||||
for (const feature of features) {
|
||||
const props = feature.properties
|
||||
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
|
||||
(props["addr:street"] && props["addr:number"]) ?
|
||||
props["addr:street"] + props["addr:number"] : undefined])
|
||||
|
||||
|
||||
const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
|
||||
let simplified = Utils.simplifyStringForSearch(entry)
|
||||
if (matchStart) {
|
||||
simplified = simplified.slice(0, query.length)
|
||||
}
|
||||
return Utils.levenshteinDistance(query, simplified)
|
||||
}))
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
if (levehnsteinD <= 2) {
|
||||
|
||||
let description = ""
|
||||
if (feature.properties["addr:street"]) {
|
||||
description += "" + feature.properties["addr:street"]
|
||||
}
|
||||
if (feature.properties["addr:housenumber"]) {
|
||||
description += " " + feature.properties["addr:housenumber"]
|
||||
}
|
||||
results.push({
|
||||
feature,
|
||||
center,
|
||||
physicalDistance: GeoOperations.distanceBetween(centerpoint, center),
|
||||
levehnsteinD,
|
||||
searchTerms,
|
||||
description: description !== "" ? description : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> {
|
||||
if (query.length < 3) {
|
||||
return []
|
||||
return new ImmutableStore([])
|
||||
}
|
||||
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
|
||||
const centerPoint: [number, number] = [center.lon, center.lat]
|
||||
let results: {
|
||||
feature: Feature,
|
||||
/**
|
||||
* Lon, lat
|
||||
*/
|
||||
center: [number, number],
|
||||
levehnsteinD: number,
|
||||
physicalDistance: number,
|
||||
searchTerms: string[],
|
||||
description: string
|
||||
}[] = []
|
||||
const properties = this._state.perLayer
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
|
||||
const partials: Store<IntermediateResult[]>[] = []
|
||||
|
||||
for (const [_, geoIndexedStore] of properties) {
|
||||
for (const feature of geoIndexedStore.features.data) {
|
||||
const props = feature.properties
|
||||
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
|
||||
(props["addr:street"] && props["addr:number"]) ?
|
||||
props["addr:street"] + props["addr:number"] : undefined])
|
||||
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, matchStart, centerPoint, features))
|
||||
partials.push(partialResult)
|
||||
}
|
||||
|
||||
|
||||
const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
|
||||
let simplified = Utils.simplifyStringForSearch(entry)
|
||||
if (matchStart) {
|
||||
simplified = simplified.slice(0, query.length)
|
||||
}
|
||||
return Utils.levenshteinDistance(query, simplified)
|
||||
}))
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
if (levehnsteinD <= 2) {
|
||||
|
||||
let description = ""
|
||||
function ifDef(prefix: string, key: string){
|
||||
if(feature.properties[key]){
|
||||
description += prefix+ feature.properties[key]
|
||||
}
|
||||
}
|
||||
ifDef("", "addr:street")
|
||||
ifDef(" ", "addr:housenumber")
|
||||
results.push({
|
||||
feature,
|
||||
center,
|
||||
physicalDistance: GeoOperations.distanceBetween(centerPoint, center),
|
||||
levehnsteinD,
|
||||
searchTerms,
|
||||
description: description !== "" ? description : undefined
|
||||
})
|
||||
const listed: Store<IntermediateResult[]> = Stores.concat(partials)
|
||||
return listed.mapD(results => {
|
||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||
if (this._limit || options?.limit) {
|
||||
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
||||
}
|
||||
return results.map(entry => {
|
||||
const id = entry.feature.properties.id.split("/")
|
||||
return <GeoCodeResult>{
|
||||
lon: entry.center[0],
|
||||
lat: entry.center[1],
|
||||
osm_type: id[0],
|
||||
osm_id: id[1],
|
||||
display_name: entry.searchTerms[0],
|
||||
source: "localElementSearch",
|
||||
feature: entry.feature,
|
||||
importance: 1,
|
||||
description: entry.description
|
||||
}
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||
if (this._limit || options?.limit) {
|
||||
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
||||
}
|
||||
return results.map(entry => {
|
||||
const id = entry.feature.properties.id.split("/")
|
||||
return <GeoCodeResult>{
|
||||
lon: entry.center[0],
|
||||
lat: entry.center[1],
|
||||
osm_type: id[0],
|
||||
osm_id: id[1],
|
||||
display_name: entry.searchTerms[0],
|
||||
source: "localElementSearch",
|
||||
feature: entry.feature,
|
||||
importance: 1,
|
||||
description: entry.description
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
return this.searchEntries(query, options, true)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ import { BBox } from "../BBox"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import GeocodingProvider, { GeoCodeResult, ReverseGeocodingProvider } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
|
||||
export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider {
|
||||
export class NominatimGeocoding implements GeocodingProvider {
|
||||
|
||||
private readonly _host ;
|
||||
|
||||
|
@ -13,14 +14,14 @@ export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingPr
|
|||
this._host = host
|
||||
}
|
||||
|
||||
public async search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
|
||||
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
|
||||
const b = options?.bbox ?? BBox.global
|
||||
const url = `${
|
||||
this._host
|
||||
}search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
||||
Locale.language.data
|
||||
}&q=${query}`
|
||||
return await Utils.downloadJson(url)
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Constants from "../../Models/Constants"
|
||||
import GeocodingProvider, {
|
||||
GeoCodeResult, GeocodingCategory,
|
||||
GeoCodeResult,
|
||||
GeocodingCategory,
|
||||
GeocodingOptions,
|
||||
ReverseGeocodingProvider,
|
||||
ReverseGeocodingResult
|
||||
|
@ -9,6 +10,7 @@ import { Utils } from "../../Utils"
|
|||
import { Feature, FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
|
||||
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
|
||||
private _endpoint: string
|
||||
|
@ -52,8 +54,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
|
||||
}
|
||||
|
||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.suggest(query, options)
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
return Stores.FromPromise(this.search(query, options))
|
||||
}
|
||||
|
||||
private buildDescription(entry: Feature) {
|
||||
|
@ -71,7 +73,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
case "house": {
|
||||
|
||||
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
|
||||
if(!addr){
|
||||
if (!addr) {
|
||||
return p.city
|
||||
}
|
||||
return addr + ifdef(", ", p.city)
|
||||
|
@ -81,8 +83,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
return p.city ?? p.country
|
||||
case "city":
|
||||
case "locality":
|
||||
if(p.state){
|
||||
return p.state + ifdef(", ", p.country)
|
||||
if (p.state) {
|
||||
return p.state + ifdef(", ", p.country)
|
||||
}
|
||||
return p.country
|
||||
case "country":
|
||||
|
@ -91,18 +93,18 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
|
||||
}
|
||||
|
||||
private getCategory(entry: Feature){
|
||||
private getCategory(entry: Feature) {
|
||||
const p = entry.properties
|
||||
if(p.osm_value === "train_station" || p.osm_key === "railway"){
|
||||
if (p.osm_value === "train_station" || p.osm_key === "railway") {
|
||||
return "train_station"
|
||||
}
|
||||
if(p.osm_value === "aerodrome" || p.osm_key === "aeroway"){
|
||||
if (p.osm_value === "aerodrome" || p.osm_key === "aeroway") {
|
||||
return "airport"
|
||||
}
|
||||
return p.type
|
||||
}
|
||||
|
||||
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
if (query.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@ export class RecentSearch {
|
|||
|
||||
|
||||
state.selectedElement.addCallbackAndRunD(selected => {
|
||||
|
||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||
if(!osm_id){
|
||||
return
|
||||
}
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||
const entry = <GeoCodeResult>{
|
||||
feature: selected,
|
||||
osm_id, osm_type,
|
||||
description: "Viewed recently",
|
||||
lon, lat
|
||||
}
|
||||
this.addSelected(entry)
|
||||
|
|
|
@ -4,7 +4,7 @@ import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
|||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import { Utils } from "../../Utils"
|
||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
||||
import { Store } from "../UIEventSource"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
|
||||
export default class ThemeSearch implements GeocodingProvider {
|
||||
|
||||
|
@ -17,11 +17,15 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
||||
}
|
||||
|
||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.suggest(query, options)
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
return this.searchDirect(query, options)
|
||||
}
|
||||
|
||||
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
return new ImmutableStore(this.searchDirect(query, options))
|
||||
}
|
||||
|
||||
private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] {
|
||||
if(query.length < 1){
|
||||
return []
|
||||
}
|
||||
|
@ -33,10 +37,10 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||
.slice(0, limit + 1)
|
||||
|
||||
return withMatch.map(match => (<GeoCodeResult> {
|
||||
return withMatch.map(match => <GeoCodeResult> {
|
||||
payload: match,
|
||||
osm_id: match.id
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -41,8 +41,26 @@ export class Stores {
|
|||
return src
|
||||
}
|
||||
|
||||
public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> {
|
||||
return UIEventSource.flatten(source, possibleSources)
|
||||
public static concat<T>(stores: Store<T[]>[]): Store<T[]> {
|
||||
const newStore = new UIEventSource<T[]>([])
|
||||
function update(){
|
||||
if(newStore._callbacks.isDestroyed){
|
||||
return true // unregister
|
||||
}
|
||||
const results: T[] = []
|
||||
for (const store of stores) {
|
||||
if(store.data){
|
||||
results.push(...store.data)
|
||||
}
|
||||
}
|
||||
newStore.setData(results)
|
||||
}
|
||||
|
||||
for (const store of stores) {
|
||||
store.addCallback(() => update())
|
||||
}
|
||||
update()
|
||||
return newStore
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,8 +123,6 @@ export abstract class Store<T> implements Readable<T> {
|
|||
callbackDestroyFunction: (f: () => void) => void
|
||||
): Store<J>
|
||||
|
||||
M
|
||||
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraStoresToWatch?: Store<any>[],
|
||||
|
@ -120,7 +136,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
return null
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
}, extraStoresToWatch)
|
||||
}, extraStoresToWatch, callbackDestroyFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,6 +247,9 @@ export abstract class Store<T> implements Readable<T> {
|
|||
if (mapped.data === newEventSource) {
|
||||
sink.setData(resultData)
|
||||
}
|
||||
if(sink._callbacks.isDestroyed){
|
||||
return true // unregister
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -308,6 +327,8 @@ export abstract class Store<T> implements Readable<T> {
|
|||
run(v)
|
||||
})
|
||||
}
|
||||
|
||||
public abstract destroy()
|
||||
}
|
||||
|
||||
export class ImmutableStore<T> extends Store<T> {
|
||||
|
@ -361,6 +382,10 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
bind<X>(f: (t: T) => Store<X>): Store<X> {
|
||||
return f(this.data)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -369,7 +394,7 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
class ListenerTracker<T> {
|
||||
public pingCount = 0
|
||||
private readonly _callbacks: ((t: T) => boolean | void | any)[] = []
|
||||
|
||||
public isDestroyed = false
|
||||
/**
|
||||
* Adds a callback which can be called; a function to unregister is returned
|
||||
*/
|
||||
|
@ -429,6 +454,11 @@ class ListenerTracker<T> {
|
|||
length() {
|
||||
return this._callbacks.length
|
||||
}
|
||||
|
||||
public destroy(){
|
||||
this.isDestroyed= true
|
||||
this._callbacks.splice(0, this._callbacks.length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -584,10 +614,14 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
this._data = newData
|
||||
this._callbacks.ping(this._data)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.unregisterFromUpstream()
|
||||
}
|
||||
}
|
||||
|
||||
export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||
private static readonly pass: () => {}
|
||||
private static readonly pass: (() => void) = () => {};
|
||||
public data: T
|
||||
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
|
||||
|
||||
|
@ -596,9 +630,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
this.data = data
|
||||
}
|
||||
|
||||
public destroy(){
|
||||
this._callbacks.destroy()
|
||||
}
|
||||
|
||||
public static flatten<X>(
|
||||
source: Store<Store<X>>,
|
||||
possibleSources?: Store<any>[]
|
||||
possibleSources?: Store<object>[]
|
||||
): UIEventSource<X> {
|
||||
const sink = new UIEventSource<X>(source.data?.data)
|
||||
|
||||
|
@ -627,7 +665,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
*/
|
||||
public static FromPromise<T>(
|
||||
promise: Promise<T>,
|
||||
onError: (e: any) => void = undefined
|
||||
onError: (e) => void = undefined
|
||||
): UIEventSource<T> {
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise?.then((d) => src.setData(d))
|
||||
|
@ -671,7 +709,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
public static asInt(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.sync(
|
||||
(str) => {
|
||||
let parsed = parseInt(str)
|
||||
const parsed = parseInt(str)
|
||||
return isNaN(parsed) ? undefined : parsed
|
||||
},
|
||||
[],
|
||||
|
@ -702,7 +740,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.sync(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str)
|
||||
const parsed = parseFloat(str)
|
||||
return isNaN(parsed) ? undefined : parsed
|
||||
},
|
||||
[],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue