This commit is contained in:
Pieter Vander Vennet 2024-08-26 13:09:46 +02:00
parent 3ab1a0a3f2
commit 617b4854fa
48 changed files with 662 additions and 491 deletions

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Store, Stores } from "../UIEventSource"
@ -17,12 +17,17 @@ export default class CombinedSearcher implements GeocodingProvider {
* @param geocoded
* @private
*/
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] {
const results: GeoCodeResult[] = []
private merge(geocoded: SearchResult[][]): SearchResult[] {
const results: SearchResult[] = []
const seenIds = new Set<string>()
for (const geocodedElement of geocoded) {
for (const entry of geocodedElement) {
const id = entry.osm_type + entry.osm_id
if (entry.osm_id === undefined) {
throw "Invalid search result: a search result always must have an osm_id to be able to merge results from different sources"
}
const id = (entry["osm_type"] ?? "") + entry.osm_id
if (seenIds.has(id)) {
continue
}
@ -33,13 +38,14 @@ export default class CombinedSearcher implements GeocodingProvider {
return results
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
return results.flatMap(x => x)
return this.merge(results)
}
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return Stores.concat(
this._providersWithSuggest.map(pr => pr.suggest(query, options)))
.map(gcrss => this.merge(gcrss))
}

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource"
@ -52,9 +52,9 @@ export default class CoordinateSearch implements GeocodingProvider {
* results.length // => 1
* results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905", "category": "coordinate", "source": "coordinate:latlon"}
*/
private directSearch(query: string): GeoCodeResult[] {
private directSearch(query: string): SearchResult[] {
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <SearchResult>{
lat: Number(m[1]),
lon: Number(m[2]),
display_name: "lon: " + m[2] + ", lat: " + m[1],
@ -64,7 +64,7 @@ export default class CoordinateSearch implements GeocodingProvider {
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
.map(m => <GeoCodeResult>{
.map(m => <SearchResult>{
lat: Number(m[2]),
lon: Number(m[1]),
display_name: "lon: " + m[1] + ", lat: " + m[2],
@ -74,11 +74,11 @@ export default class CoordinateSearch implements GeocodingProvider {
return matches.concat(matchesLonLat)
}
suggest(query: string): Store<GeoCodeResult[]> {
suggest(query: string): Store<SearchResult[]> {
return new ImmutableStore(this.directSearch(query))
}
async search (query: string): Promise<GeoCodeResult[]> {
async search (query: string): Promise<SearchResult[]> {
return this.directSearch(query)
}

View file

@ -0,0 +1,60 @@
import { ImmutableStore, Store } from "../UIEventSource"
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
import Locale from "../../UI/i18n/Locale"
export default class FilterSearch implements GeocodingProvider {
private readonly _state: SpecialVisualizationState
constructor(state: SpecialVisualizationState) {
this._state = state
}
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
return []
}
private searchDirectly(query: string): SearchResult[] {
const possibleFilters: SearchResult[] = []
for (const layer of this._state.layout.layers) {
if (!Array.isArray(layer.filters)) {
continue
}
for (const filter of layer.filters ?? []) {
for (let i = 0; i < filter.options.length; i++) {
const option = filter.options[i]
if (option === undefined) {
console.log("No options for", filter)
continue
}
const terms = [option.question.txt,
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])].flatMap(term => term.split(" "))
const levehnsteinD = Math.min(...
terms.map(entry => {
const simplified = Utils.simplifyStringForSearch(entry)
return Utils.levenshteinDistance(query, simplified.slice(0, query.length))
}))
if (levehnsteinD / query.length > 0.25) {
continue
}
possibleFilters.push({
payload: { option, layer, filter, index: i },
category: "filter",
osm_id: layer.id + "/" + filter.id + "/" + option.osmTags?.asHumanString() ?? "none",
})
}
}
}
return possibleFilters
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
query = Utils.simplifyStringForSearch(query)
return new ImmutableStore(this.searchDirectly(query))
}
}

View file

@ -1,4 +1,4 @@
import { GeoCodeResult } from "./GeocodingProvider"
import { SearchResult } from "./GeocodingProvider"
import { Store } from "../UIEventSource"
import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature, Geometry } from "geojson"
@ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson"
export default class GeocodingFeatureSource implements FeatureSource {
public features: Store<Feature<Geometry, Record<string, string>>[]>
constructor(provider: Store<GeoCodeResult[]>) {
constructor(provider: Store<SearchResult[]>) {
this.features = provider.map(geocoded => {
if(geocoded === undefined){
return []

View file

@ -5,9 +5,22 @@ import { Store } from "../UIEventSource"
import * as search from "../../assets/generated/layers/search.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" | "shop"
import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export type GeoCodeResult = {
export type GeocodingCategory =
"coordinate"
| "city"
| "house"
| "street"
| "locality"
| "country"
| "train_station"
| "county"
| "airport"
| "shop"
export type GeocodeResult = {
/**
* The name of the feature being displayed
*/
@ -25,11 +38,16 @@ export type GeoCodeResult = {
*/
boundingbox?: number[]
osm_type?: "node" | "way" | "relation"
osm_id?: string,
osm_id: string,
category?: GeocodingCategory,
payload?: object,
source?: string
}
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
export type SearchResult =
| { category: "filter", osm_id: string, payload: FilterPayload }
| { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
| GeocodeResult
export interface GeocodingOptions {
bbox?: BBox,
@ -40,16 +58,16 @@ export interface GeocodingOptions {
export default interface GeocodingProvider {
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
search(query: string, options?: GeocodingOptions): Promise<SearchResult[]>
/**
* @param query
* @param options
*/
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]>
suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]>
}
export type ReverseGeocodingResult = Feature<Geometry,{
export type ReverseGeocodingResult = Feature<Geometry, {
osm_id: number,
osm_type: "node" | "way" | "relation",
country: string,
@ -57,25 +75,26 @@ export type ReverseGeocodingResult = Feature<Geometry,{
countrycode: string,
type: GeocodingCategory,
street: string
} >
}>
export interface ReverseGeocodingProvider {
reverseSearch(
coordinate: { lon: number; lat: number },
zoom: number,
language?: string
): Promise<ReverseGeocodingResult[]> ;
language?: string,
): Promise<ReverseGeocodingResult[]>;
}
export class GeocodingUtils {
public static searchLayer = GeocodingUtils.initSearchLayer()
private static initSearchLayer():LayerConfig{
if(search["id"] === undefined){
public static searchLayer = GeocodingUtils.initSearchLayer()
private static initSearchLayer(): LayerConfig {
if (search["id"] === undefined) {
// We are resetting the layeroverview; trying to parse is useless
return undefined
}
return new LayerConfig(<LayerConfigJson> search, "search")
return new LayerConfig(<LayerConfigJson>search, "search")
}
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {
@ -88,7 +107,7 @@ export class GeocodingUtils {
street: 15,
train_station: 14,
airport: 13,
shop:16
shop: 16,
}
@ -103,7 +122,7 @@ export class GeocodingUtils {
train_station: "train",
county: "building_office_2",
airport: "airport",
shop: "building_storefront"
shop: "building_storefront",
}

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
import ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
@ -27,7 +27,7 @@ export default class LocalElementSearch implements GeocodingProvider {
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
return this.searchEntries(query, options, false).data
}
@ -41,7 +41,6 @@ export default class LocalElementSearch implements GeocodingProvider {
props["addr:street"] + props["addr:number"] : undefined])
let levehnsteinD: number
console.log("Comparing nearby:", candidateId, props.id)
if (candidateId === props.id) {
levehnsteinD = 0
} else {
@ -54,7 +53,7 @@ export default class LocalElementSearch implements GeocodingProvider {
}))
}
const center = GeoOperations.centerpointCoordinates(feature)
if (levehnsteinD <= 2) {
if ((levehnsteinD / query.length) <= 0.3) {
let description = ""
if (feature.properties["addr:street"]) {
@ -76,7 +75,7 @@ export default class LocalElementSearch implements GeocodingProvider {
return results
}
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> {
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> {
if (query.length < 3) {
return new ImmutableStore([])
}
@ -101,7 +100,7 @@ export default class LocalElementSearch implements GeocodingProvider {
}
return results.map(entry => {
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
return <GeoCodeResult>{
return <SearchResult>{
lon: entry.center[0],
lat: entry.center[1],
osm_type,
@ -118,7 +117,7 @@ export default class LocalElementSearch implements GeocodingProvider {
}
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return this.searchEntries(query, options, true)
}

View file

@ -3,7 +3,7 @@ import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { FeatureCollection } from "geojson"
import Locale from "../../UI/i18n/Locale"
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult } from "./GeocodingProvider"
export class NominatimGeocoding implements GeocodingProvider {
@ -13,7 +13,7 @@ export class NominatimGeocoding implements GeocodingProvider {
this._host = host
}
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<SearchResult[]> {
const b = options?.bbox ?? BBox.global
const url = `${
this._host

View file

@ -1,5 +1,5 @@
import { Store, UIEventSource } from "../UIEventSource"
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
import { OsmId } from "../../Models/OsmFeature"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
@ -30,7 +30,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
return undefined
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
const id = OpenStreetMapIdSearch.extractId(query)
if (!id) {
return []
@ -59,7 +59,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
}]
}
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return UIEventSource.FromPromise(this.search(query, options))
}

View file

@ -1,10 +1,10 @@
import Constants from "../../Models/Constants"
import GeocodingProvider, {
GeoCodeResult,
GeocodeResult,
GeocodingCategory,
GeocodingOptions,
ReverseGeocodingProvider,
ReverseGeocodingResult
ReverseGeocodingResult,
} from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson"
@ -18,7 +18,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
private static readonly types = {
"R": "relation",
"W": "way",
"N": "node"
"N": "node",
}
@ -54,7 +54,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
}
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
return Stores.FromPromise(this.search(query, options))
}
@ -95,7 +95,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
private getCategory(entry: Feature) {
const p = entry.properties
if(p.osm_key === "shop"){
if (p.osm_key === "shop") {
return "shop"
}
if (p.osm_value === "train_station" || p.osm_key === "railway") {
@ -107,7 +107,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
return p.type
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
if (query.length < 3) {
return []
}
@ -126,7 +126,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
const [lon0, lat0, lon1, lat1] = f.properties.extent
boundingbox = [lat0, lat1, lon0, lon1]
}
return <GeoCodeResult>{
return <GeocodeResult>{
feature: f,
osm_id: f.properties.osm_id,
display_name: f.properties.name,
@ -135,7 +135,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
category: this.getCategory(f),
boundingbox,
lon, lat,
source: this._endpoint
source: this._endpoint,
}
})
}

View file

@ -1,36 +1,41 @@
import { Store, UIEventSource } from "../UIEventSource"
import { Feature } from "geojson"
import { OsmConnection } from "../Osm/OsmConnection"
import { GeoCodeResult } from "./GeocodingProvider"
import { GeocodeResult } from "./GeocodingProvider"
import { GeoOperations } from "../GeoOperations"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export class RecentSearch {
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]>
public readonly seenThisSession: Store<GeoCodeResult[]>
private readonly _seenThisSession: UIEventSource<GeocodeResult[]>
public readonly seenThisSession: Store<GeocodeResult[]>
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
prefs.addCallbackAndRunD(prev => console.trace("Previous searches are:", prev))
prefs.set(null)
this._seenThisSession = new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
this.seenThisSession = this._seenThisSession
prefs.addCallbackAndRunD(prefs => {
if(prefs === ""){
prefs.addCallbackAndRunD(pref => {
if (pref === "") {
return
}
const simpleArr = <GeoCodeResult[]> JSON.parse(prefs)
if(simpleArr.length > 0){
this._seenThisSession.set(simpleArr)
return true
try {
const simpleArr = <GeocodeResult[]>JSON.parse(pref)
if (simpleArr.length > 0) {
this._seenThisSession.set(simpleArr)
return true
}
} catch (e) {
console.error(e, pref)
prefs.setData("")
}
})
this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => {
const results= []
const results = []
for (let i = 0; i < Math.min(3, seen.length); i++) {
const gc = seen[i]
const simple = {
@ -39,7 +44,7 @@ export class RecentSearch {
display_name: gc.display_name,
lat: gc.lat, lon: gc.lon,
osm_id: gc.osm_id,
osm_type: gc.osm_type
osm_type: gc.osm_type,
}
results.push(simple)
}
@ -51,25 +56,25 @@ export class RecentSearch {
state.selectedElement.addCallbackAndRunD(selected => {
const [osm_type, osm_id] = selected.properties.id.split("/")
if(!osm_id){
if (!osm_id) {
return
}
console.log("Selected element is", selected)
if(["node","way","relation"].indexOf(osm_type) < 0){
if (["node", "way", "relation"].indexOf(osm_type) < 0) {
return
}
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
const entry = <GeoCodeResult>{
const entry = <GeocodeResult>{
feature: selected,
osm_id, osm_type,
lon, lat
lon, lat,
}
this.addSelected(entry)
})
}
addSelected(entry: GeoCodeResult) {
addSelected(entry: GeocodeResult) {
const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry]
const seenIds = new Set<string>()
@ -81,7 +86,7 @@ export class RecentSearch {
seenIds.add(id)
}
}
console.log(">>>",arr)
console.log(">>>", arr)
this._seenThisSession.set(arr)
}
}

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
@ -17,15 +17,15 @@ export default class ThemeSearch implements GeocodingProvider {
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
}
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
return this.searchDirect(query, options)
}
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return new ImmutableStore(this.searchDirect(query, options))
}
private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] {
private searchDirect(query: string, options?: GeocodingOptions): SearchResult[] {
if(query.length < 1){
return []
}
@ -37,8 +37,9 @@ 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 => <SearchResult> {
payload: match,
category: "theme",
osm_id: match.id
})
}

View file

@ -7,7 +7,13 @@ import { Tag } from "../Tags/Tag"
import Translations from "../../UI/i18n/Translations"
import { RegexTag } from "../Tags/RegexTag"
import { Or } from "../Tags/Or"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
export type ActiveFilter = {
layer: LayerConfig,
filter: FilterConfig,
control: UIEventSource<string | number | undefined>
}
/**
* The layer state keeps track of:
* - Which layers are enabled
@ -26,6 +32,9 @@ export default class LayerState {
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public readonly filteredLayers: ReadonlyMap<string, FilteredLayer>
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
private readonly osmConnection: OsmConnection
/**
@ -56,6 +65,41 @@ export default class LayerState {
}
this.filteredLayers = filteredLayers
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
this.filteredLayers.forEach(fl => {
fl.isDisplayed.addCallback(() => this.updateActiveFilters())
for (const [_, appliedFilter] of fl.appliedFilters) {
appliedFilter.addCallback(() => this.updateActiveFilters())
}
})
this.updateActiveFilters()
}
private updateActiveFilters(){
const filters: ActiveFilter[] = []
this.filteredLayers.forEach(fl => {
if(!fl.isDisplayed.data){
return
}
for (const [filtername, appliedFilter] of fl.appliedFilters) {
if (appliedFilter.data === undefined) {
continue
}
const filter = fl.layerDef.filters.find(f => f.id === filtername)
if(typeof appliedFilter.data === "number"){
if(filter.options[appliedFilter.data].osmTags === undefined){
// This is probably the first, generic option which doesn't _actually_ filter
continue
}
}
filters.push({
layer: fl.layerDef,
control: appliedFilter,
filter,
})
}
})
this._activeFilters.set(filters)
}
/**

View file

@ -207,7 +207,6 @@ export default class UserRelatedState {
isOfficial: boolean
}
| undefined {
console.log("GETTING UNOFFICIAL THEME")
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
const str = pref.data
@ -351,10 +350,8 @@ export default class UserRelatedState {
const key = k.substring(0, k.length - "length".length)
let combined = ""
for (let i = 0; i < l; i++) {
console.log("Building preference:",key,i,">>>", newPrefs[key + i], "<<<", newPrefs, )
combined += (newPrefs[key + i])
}
console.log("Combined",key,">>>",combined)
amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined
} else {
amendedPrefs.data[k] = newPrefs[k]