Performance: load less data for branded items, use caching

This commit is contained in:
Pieter Vander Vennet 2025-04-27 02:37:47 +02:00
parent 2a3ee4c4f6
commit 164b02c8ff
2 changed files with 186 additions and 147 deletions

View file

@ -82,6 +82,7 @@ data.mapcomplete.org {
header {
+Permissions-Policy "interest-cohort=()"
+Access-Control-Allow-Origin *
Cache-Control: max-age=86400, public, stale-while-revalidate=86400,stale-if-error=86400
}
}

View file

@ -55,77 +55,22 @@ export interface NSIItem {
ext?: string
}
export default class NameSuggestionIndex {
export class NameSuggestionIndexLight {
public static readonly supportedTypes = ["brand", "flag", "operator", "transit"] as const
private readonly nsiFile: Readonly<NSIFile>
private readonly nsiWdFile: Readonly<
Record<
string,
{
logos: { wikidata?: string; facebook?: string }
}
>
>
protected readonly _serverLocation: string
protected readonly nsiFile: Readonly<NSIFile>
private readonly loco: LocationConflation // Some additional boundaries
private static initedLight: NameSuggestionIndexLight = undefined
private loco: LocationConflation // Some additional boundaries
private _supportedTypes: string[]
private _serverLocation: string
private constructor(
protected constructor(
serverLocation: string,
nsiFile: Readonly<NSIFile>,
nsiWdFile: Readonly<
Record<
string,
{
logos: { wikidata?: string; facebook?: string }
}
>
>,
features: Readonly<FeatureCollection>
) {
this._serverLocation = serverLocation
this.nsiFile = nsiFile
this.nsiWdFile = nsiWdFile
this.loco = new LocationConflation(features)
}
private static inited: NameSuggestionIndex = undefined
public static async getNsiIndex(): Promise<NameSuggestionIndex> {
if (NameSuggestionIndex.inited) {
return NameSuggestionIndex.inited
}
const [nsi, nsiWd, features] = await Promise.all(
[
"./assets/data/nsi/nsi.min.json",
"./assets/data/nsi/wikidata.min.json",
"./assets/data/nsi/featureCollection.min.json",
].map((url) => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30))
)
NameSuggestionIndex.inited = new NameSuggestionIndex(
Constants.nsiLogosEndpoint,
<any>nsi,
<any>nsiWd["wikidata"],
<any>features
)
return NameSuggestionIndex.inited
}
public supportedTypes(): string[] {
if (this._supportedTypes) {
return this._supportedTypes
}
const keys = Object.keys(this.nsiFile.nsi)
const all = keys.map((k) => this.nsiFile.nsi[k].properties.path.split("/")[0])
this._supportedTypes = Utils.Dedup(all).map((s) => {
if (s.endsWith("s")) {
s = s.substring(0, s.length - 1)
}
return s
})
return this._supportedTypes
NameSuggestionIndexLight.initedLight = this
}
/**
@ -165,25 +110,6 @@ export default class NameSuggestionIndex {
return merged
}
public isSvg(nsiItem: NSIItem, type: string): boolean | undefined {
if (this.nsiWdFile === undefined) {
throw (
"nsiWdi file is not loaded, cannot determine if " + nsiItem.id + " has an SVG image"
)
}
const logos = this.nsiWdFile[nsiItem?.tags?.[type + ":wikidata"]]?.logos
if (!logos) {
return undefined
}
if (logos.facebook) {
return false
}
const url: string = logos.wikidata
if (url.toLowerCase().endsWith(".svg")) {
return true
}
return false
}
public async generateMappings(
type: string,
@ -236,7 +162,7 @@ export default class NameSuggestionIndex {
// As such, it should be "true" but this is not supported
priorityIf: frequency > 0 ? new RegexTag("id", /.*/) : undefined,
searchTerms: { "*": [nsiItem.displayName, nsiItem.id] },
frequency: frequency ?? -1,
frequency: frequency ?? -1
})
}
}
@ -247,68 +173,21 @@ export default class NameSuggestionIndex {
return mappings
}
public supportedTags(
type: "operator" | "brand" | "flag" | "transit" | string
): Record<string, string[]> {
const tags: Record<string, string[]> = {}
const keys = Object.keys(this.nsiFile.nsi)
for (const key of keys) {
const nsiItem = this.nsiFile.nsi[key]
const path = nsiItem.properties.path
const [osmType, osmkey, osmvalue] = path.split("/")
if (type !== osmType && type + "s" !== osmType) {
continue
}
if (!tags[osmkey]) {
tags[osmkey] = []
}
tags[osmkey].push(osmvalue)
public getIconUrl(nsiItem: NSIItem): string | undefined {
const baseUrl = this._serverLocation
if (!nsiItem.ext || baseUrl === null) {
// No extension -> there is no logo
return undefined
}
return tags
return baseUrl + "/logos/" + nsiItem.id + "." + nsiItem.ext
}
/**
* Returns a list of all brands/operators
* @param type
*/
public allPossible(type: string): NSIItem[] {
const options: NSIItem[] = []
const tags = this.supportedTags(type)
for (const osmKey in tags) {
const values = tags[osmKey]
for (const osmValue of values) {
const suggestions = this.getSuggestionsForKV(type, osmKey, osmValue)
if (!suggestions) {
console.warn("No suggestions found for", type, osmKey, osmValue)
continue
}
options.push(...suggestions)
}
}
return options
}
/**
*
* @param country: a string containing one or more country codes, separated by ";"
* @param location: center point of the feature, should be [lon, lat]
*/
public getSuggestionsFor(
type: string,
tags: { key: string; value: string }[],
country: string = undefined,
location: [number, number] = undefined
): NSIItem[] {
return tags.flatMap((tag) =>
this.getSuggestionsForKV(type, tag.key, tag.value, country, location)
)
}
/**
* Caching for the resolved sets, as they can take a while
* @private
*/
private static resolvedSets: Record<string, any> = {}
private static resolvedSets: Record<string, { type, location, id, feature }> = {}
/**
* Returns all suggestions for the given type (brand|operator) and main tag.
@ -379,6 +258,173 @@ export default class NameSuggestionIndex {
})
}
public static async singleton(): Promise<NameSuggestionIndexLight> {
if (NameSuggestionIndexLight.initedLight) {
return NameSuggestionIndexLight.initedLight
}
const [nsi, features] = await Promise.all(
[
"./assets/data/nsi/nsi.min.json",
"./assets/data/nsi/featureCollection.min.json"
].map((url) => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30))
)
return new NameSuggestionIndexLight(
Constants.nsiLogosEndpoint,
<any>nsi,
<any>features
)
}
}
export default class NameSuggestionIndex extends NameSuggestionIndexLight {
private readonly nsiWdFile: Readonly<
Record<
string,
{
logos: { wikidata?: string; facebook?: string }
}
>
>
private _supportedTypes: string[]
private static inited: NameSuggestionIndex = undefined
private constructor(
serverLocation: string,
nsiFile: Readonly<NSIFile>,
nsiWdFile: Readonly<
Record<
string,
{
logos: { wikidata?: string; facebook?: string }
}
>
>,
features: Readonly<FeatureCollection>
) {
super(serverLocation, nsiFile, features)
this.nsiWdFile = nsiWdFile
NameSuggestionIndex.inited = this
}
public static async getNsiIndex(): Promise<NameSuggestionIndex> {
if (NameSuggestionIndex.inited) {
return NameSuggestionIndex.inited
}
const [nsi, nsiWd, features] = await Promise.all(
[
"./assets/data/nsi/nsi.min.json",
"./assets/data/nsi/wikidata.min.json",
"./assets/data/nsi/featureCollection.min.json"
].map((url) => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30))
)
return new NameSuggestionIndex(
Constants.nsiLogosEndpoint,
<any>nsi,
<any>nsiWd["wikidata"],
<any>features
)
}
public supportedTypes(): string[] {
if (this._supportedTypes) {
return this._supportedTypes
}
const keys = Object.keys(this.nsiFile.nsi)
const all = keys.map((k) => this.nsiFile.nsi[k].properties.path.split("/")[0])
this._supportedTypes = Utils.Dedup(all).map((s) => {
if (s.endsWith("s")) {
s = s.substring(0, s.length - 1)
}
return s
})
return this._supportedTypes
}
public isSvg(nsiItem: NSIItem, type: string): boolean | undefined {
if (this.nsiWdFile === undefined) {
throw (
"nsiWdi file is not loaded, cannot determine if " + nsiItem.id + " has an SVG image"
)
}
const logos = this.nsiWdFile[nsiItem?.tags?.[type + ":wikidata"]]?.logos
if (!logos) {
return undefined
}
if (logos.facebook) {
return false
}
const url: string = logos.wikidata
if (url.toLowerCase().endsWith(".svg")) {
return true
}
return false
}
public supportedTags(
type: "operator" | "brand" | "flag" | "transit" | string
): Record<string, string[]> {
const tags: Record<string, string[]> = {}
const keys = Object.keys(this.nsiFile.nsi)
for (const key of keys) {
const nsiItem = this.nsiFile.nsi[key]
const path = nsiItem.properties.path
const [osmType, osmkey, osmvalue] = path.split("/")
if (type !== osmType && type + "s" !== osmType) {
continue
}
if (!tags[osmkey]) {
tags[osmkey] = []
}
tags[osmkey].push(osmvalue)
}
return tags
}
/**
* Returns a list of all brands/operators
* @param type
*/
public allPossible(type: string): NSIItem[] {
const options: NSIItem[] = []
const tags = this.supportedTags(type)
for (const osmKey in tags) {
const values = tags[osmKey]
for (const osmValue of values) {
const suggestions = this.getSuggestionsForKV(type, osmKey, osmValue)
if (!suggestions) {
console.warn("No suggestions found for", type, osmKey, osmValue)
continue
}
options.push(...suggestions)
}
}
return options
}
/**
*
* @param country: a string containing one or more country codes, separated by ";"
* @param location: center point of the feature, should be [lon, lat]
*/
public getSuggestionsFor(
type: string,
tags: { key: string; value: string }[],
country: string = undefined,
location: [number, number] = undefined
): NSIItem[] {
return tags.flatMap((tag) =>
this.getSuggestionsForKV(type, tag.key, tag.value, country, location)
)
}
public static async generateMappings(
key: string,
tags: Exclude<Record<string, string>, undefined | null>,
@ -388,7 +434,7 @@ export default class NameSuggestionIndex {
sortByFrequency: boolean
}
): Promise<Mapping[]> {
const nsi = await NameSuggestionIndex.getNsiIndex()
const nsi = await NameSuggestionIndexLight.singleton()
return nsi.generateMappings(key, tags, country, center, options)
}
@ -403,14 +449,6 @@ export default class NameSuggestionIndex {
return logos?.facebook ?? logos?.wikidata
}
public getIconUrl(nsiItem: NSIItem): string | undefined {
const baseUrl = this._serverLocation
if (!nsiItem.ext || baseUrl === null) {
// No extension -> there is no logo
return undefined
}
return baseUrl +"/logos/"+ nsiItem.id + "." + nsiItem.ext
}
private static readonly brandPrefix = ["name", "alt_name", "operator", "brand"] as const
@ -425,8 +463,8 @@ export default class NameSuggestionIndex {
static asFilterTags(
item: NSIItem
): string | { and: TagConfigJson[] } | { or: TagConfigJson[] } {
let brandDetection: string[] = []
let required: string[] = []
const brandDetection: string[] = []
const required: string[] = []
const tags: Record<string, string> = item.tags
for (const k in tags) {
if (NameSuggestionIndex.brandPrefix.some((br) => k === br || k.startsWith(br + ":"))) {