MapComplete/src/Logic/Web/NameSuggestionIndex.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

231 lines
8.4 KiB
TypeScript
Raw Normal View History

2024-05-13 17:21:40 +02:00
import * as nsi from "../../../node_modules/name-suggestion-index/dist/nsi.json"
import * as nsiWD from "../../../node_modules/name-suggestion-index/dist/wikidata.min.json"
2024-05-13 17:21:40 +02:00
import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
import { LocationConflation } from "@rapideditor/location-conflation"
2024-04-30 15:59:07 +02:00
import type { Feature, MultiPolygon } from "geojson"
import { Utils } from "../../Utils"
2024-04-30 15:59:07 +02:00
import * as turf from "@turf/turf"
import { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"
import { Tag } from "../Tags/Tag"
import { TypedTranslation } from "../../UI/i18n/Translation"
import { RegexTag } from "../Tags/RegexTag"
2024-05-13 17:21:40 +02:00
/**
* Main name suggestion index file
*/
interface NSIFile {
_meta: {
version: string
generated: string
url: string
hash: string
}
nsi: {
[path: string]: NSIEntry
}
}
/**
* A collection of brands/operators/flagpoles/... with common properties
* See https://github.com/osmlab/name-suggestion-index/wiki/Category-Files for an introduction and
* https://github.com/osmlab/name-suggestion-index/blob/main/schema/categories.json for a full breakdown
*/
interface NSIEntry {
properties: {
path: string
skipCollection?: boolean
preserveTags?: string[]
exclude: unknown
}
items: NSIItem[]
}
/**
* Represents a single brand/operator/flagpole/...
*/
export interface NSIItem {
displayName: string
id: string
locationSet: {
include: string[],
exclude: string[]
}
tags: Record<string, string>
2024-05-13 17:21:40 +02:00
fromTemplate?: boolean
}
export default class NameSuggestionIndex {
private static readonly nsiFile: Readonly<NSIFile> = <any>nsi
private static loco = new LocationConflation(nsiFeatures) // Some additional boundaries
private static _supportedTypes: string[]
public static supportedTypes(): string[] {
if (this._supportedTypes) {
return this._supportedTypes
}
const keys = Object.keys(NameSuggestionIndex.nsiFile.nsi)
const all = keys.map(k => NameSuggestionIndex.nsiFile.nsi[k].properties.path.split("/")[0])
this._supportedTypes = Utils.Dedup(all)
return this._supportedTypes
}
/**
* Fetches the data files for a single country. Note that it contains _all_ entries having this brand, not for a single type of object
* @param type
* @param countries
* @private
*/
private static async fetchFrequenciesFor(type: string, countries: string[]) {
let stats = await Promise.all(countries.map(c => {
try {
return Utils.downloadJsonCached<Record<string, number>>(`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`, 24 * 60 * 60 * 1000)
} catch (e) {
console.error("Could not fetch " + type + " statistics due to", e)
return undefined
}
}))
stats = Utils.NoNull(stats)
if (stats.length === 1) {
return stats[0]
}
const merged = stats[0]
for (let i = 1; i < stats.length; i++) {
for (const countryCode in stats[i]) {
merged[countryCode] = (merged[countryCode] ?? 0) + stats[i][countryCode]
}
}
return merged
}
public static isSvg(nsiItem: NSIItem, type: string): boolean | undefined {
const logos = nsiWD["wikidata"][nsiItem?.tags?.[type + ":wikidata"]]?.logos
if(nsiItem.id === "axa-2f6feb"){
console.trace(">>> HI")
}
if (!logos) {
return undefined
}
if (logos.facebook) {
return false
2024-05-13 17:21:40 +02:00
}
const url: string = logos.wikidata
if (url.toLowerCase().endsWith(".svg")) {
return true
}
return false
}
public static async generateMappings(type: string, key: string, value: string, country: string[], location?: [number, number]) {
const mappings: Mapping[] = []
const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country)
const actualBrands = NameSuggestionIndex.getSuggestionsFor(type, key, value, country.join(";"), location)
for (const nsiItem of actualBrands) {
const tags = nsiItem.tags
const frequency = frequencies[nsiItem.displayName]
const logos = nsiWD["wikidata"][nsiItem.tags[type + ":wikidata"]]?.logos
let iconUrl = logos?.facebook ?? logos?.wikidata
const hasIcon = iconUrl !== undefined
let icon = undefined
if (hasIcon) {
// Using <img src=...> works fine without an extension for JPG and PNG, but _not_ svg :(
icon = "./assets/data/nsi/logos/" + nsiItem.id
if (NameSuggestionIndex.isSvg(nsiItem, type)) {
console.log("Is svg:", nsiItem.displayName)
icon = icon + ".svg"
}
}
mappings.push({
if: new Tag(type, tags[type]),
addExtraTags: Object.keys(tags).filter(k => k !== type).map(k => new Tag(k, tags[k])),
then: new TypedTranslation<{}>({ "*": nsiItem.displayName }),
hideInAnswer: false,
ifnot: undefined,
alsoShowIf: undefined,
icon,
iconClass: "medium",
priorityIf: frequency > 0 ? new RegexTag("id", /.*/) : undefined,
searchTerms: { "*": [nsiItem.displayName, nsiItem.id] }
})
}
return mappings
2024-05-13 17:21:40 +02:00
}
public static supportedTags(type: "operator" | "brand" | "flag" | "transit" | string): Record<string, string[]> {
const tags: Record<string, string []> = {}
const keys = Object.keys(NameSuggestionIndex.nsiFile.nsi)
for (const key of keys) {
const nsiItem = NameSuggestionIndex.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 static allPossible(type: "brand" | "operator"): NSIItem[] {
const options: NSIItem[] = []
2024-05-13 17:21:40 +02:00
const tags = NameSuggestionIndex.supportedTags(type)
for (const osmKey in tags) {
const values = tags[osmKey]
for (const osmValue of values) {
const suggestions = this.getSuggestionsFor(type, osmKey, osmValue)
options.push(...suggestions)
2024-05-13 17:21:40 +02:00
}
}
return (options)
2024-05-13 17:21:40 +02:00
}
/**
*
* @param path
* @param country: a string containing one or more country codes, separated by ";"
2024-05-13 17:21:40 +02:00
* @param location: center point of the feature, should be [lon, lat]
*/
public static getSuggestionsFor(type: string, key: string, value: string, country: string = undefined, location: [number, number] = undefined): NSIItem[] {
const path = `${type}s/${key}/${value}`
const entry = NameSuggestionIndex.nsiFile.nsi[path]
return entry?.items?.filter(i => {
if (i.locationSet.include.indexOf("001") >= 0) {
return true
}
if (country === undefined ||
// We prefer the countries provided by lonlat2country, they are more precise
// Country might contain multiple countries, separated by ';'
i.locationSet.include.some(c => country.indexOf(c) >= 0)) {
return true
}
if (location === undefined) {
return true
}
const resolvedSet = NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
2024-04-30 15:59:07 +02:00
2024-05-13 17:21:40 +02:00
if (resolvedSet) {
// We actually have a location set, so we can check if the feature is in it, by determining if our point is inside the MultiPolygon using @turf/boolean-point-in-polygon
// This might occur for some extra boundaries, such as counties, ...
const setFeature: Feature<MultiPolygon> = resolvedSet.feature
return turf.booleanPointInPolygon(location, setFeature.geometry)
}
return false
})
}
}