2024-05-13 17:21:40 +02:00
import * as nsi from "../../../node_modules/name-suggestion-index/dist/nsi.json"
2024-05-16 00:12:50 +02:00
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"
2024-05-16 00:12:50 +02:00
import { Utils } from "../../Utils"
2024-04-30 15:59:07 +02:00
import * as turf from "@turf/turf"
2024-05-16 00:12:50 +02:00
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 : {
2024-06-16 16:06:26 +02:00
include : string [ ]
2024-05-13 17:21:40 +02:00
exclude : string [ ]
}
2024-05-16 00:12:50 +02:00
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
2024-06-16 16:06:26 +02:00
private static readonly nsiWdFile : Readonly <
Record <
string ,
{
logos : { wikidata? : string ; facebook? : string }
}
>
> = < any > nsiWD [ "wikidata" ]
2024-05-23 04:42:26 +02:00
2024-05-13 17:21:40 +02:00
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 )
2024-06-16 16:06:26 +02:00
const all = keys . map (
( k ) = > NameSuggestionIndex . nsiFile . nsi [ k ] . properties . path . split ( "/" ) [ 0 ]
)
this . _supportedTypes = Utils . Dedup ( all ) . map ( ( s ) = > {
if ( s . endsWith ( "s" ) ) {
2024-05-23 04:42:26 +02:00
s = s . substring ( 0 , s . length - 1 )
}
return s
} )
2024-05-13 17:21:40 +02:00
return this . _supportedTypes
}
2024-05-16 00:12:50 +02:00
/ * *
* 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 [ ] ) {
2024-06-16 16:06:26 +02:00
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
}
} )
)
2024-05-16 00:12:50 +02:00
stats = Utils . NoNull ( stats )
if ( stats . length === 1 ) {
return stats [ 0 ]
}
2024-06-16 16:06:26 +02:00
if ( stats . length === 0 ) {
2024-05-23 04:42:26 +02:00
return { }
}
2024-05-16 00:12:50 +02:00
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 {
2024-05-23 04:42:26 +02:00
const logos = this . nsiWdFile [ nsiItem ? . tags ? . [ type + ":wikidata" ] ] ? . logos
2024-05-16 00:12:50 +02:00
if ( ! logos ) {
return undefined
}
if ( logos . facebook ) {
return false
2024-05-13 17:21:40 +02:00
}
2024-05-16 00:12:50 +02:00
const url : string = logos . wikidata
if ( url . toLowerCase ( ) . endsWith ( ".svg" ) ) {
return true
}
return false
}
2024-06-16 16:06:26 +02:00
public static async generateMappings (
type : string ,
tags : Record < string , string > ,
country : string [ ] ,
2024-08-09 14:43:30 +02:00
location ? : [ number , number ] ,
2024-08-14 13:53:56 +02:00
options ? : {
2024-08-09 14:43:30 +02:00
/ * *
* If set , sort by frequency instead of alphabetically
* /
sortByFrequency : boolean
}
2024-06-16 16:06:26 +02:00
) : Promise < Mapping [ ] > {
2024-08-14 13:53:56 +02:00
const mappings : ( Mapping & { frequency : number } ) [ ] = [ ]
2024-05-16 00:12:50 +02:00
const frequencies = await NameSuggestionIndex . fetchFrequenciesFor ( type , country )
2024-05-23 04:42:26 +02:00
for ( const key in tags ) {
if ( key . startsWith ( "_" ) ) {
continue
}
const value = tags [ key ]
2024-06-16 16:06:26 +02:00
const actualBrands = NameSuggestionIndex . getSuggestionsForKV (
type ,
key ,
value ,
country . join ( ";" ) ,
location
)
if ( ! actualBrands ) {
2024-05-23 04:42:26 +02:00
continue
}
for ( const nsiItem of actualBrands ) {
const tags = nsiItem . tags
const frequency = frequencies [ nsiItem . displayName ]
const logos = this . nsiWdFile [ nsiItem . tags [ type + ":wikidata" ] ] ? . logos
const 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 ) ) {
icon = icon + ".svg"
}
2024-05-16 00:12:50 +02:00
}
2024-05-23 04:42:26 +02:00
mappings . push ( {
if : new Tag ( type , tags [ type ] ) ,
2024-06-16 16:06:26 +02:00
addExtraTags : Object.keys ( tags )
. filter ( ( k ) = > k !== type )
. map ( ( k ) = > new Tag ( k , tags [ k ] ) ) ,
2024-08-09 14:48:29 +02:00
then : new TypedTranslation < Record < string , never > > ( { "*" : nsiItem . displayName } ) ,
2024-05-23 04:42:26 +02:00
hideInAnswer : false ,
ifnot : undefined ,
alsoShowIf : undefined ,
icon ,
iconClass : "medium" ,
2024-07-09 10:54:41 +02:00
// The 'frequency' is already for the country of the object we are working with
// As such, it should be "true" but this is not supported
2024-05-23 04:42:26 +02:00
priorityIf : frequency > 0 ? new RegexTag ( "id" , /.*/ ) : undefined ,
2024-06-16 16:06:26 +02:00
searchTerms : { "*" : [ nsiItem . displayName , nsiItem . id ] } ,
2024-08-14 13:53:56 +02:00
frequency : frequency ? ? - 1 ,
2024-05-23 04:42:26 +02:00
} )
2024-05-16 00:12:50 +02:00
}
}
2024-08-14 13:53:56 +02:00
if ( options ? . sortByFrequency ) {
2024-08-09 14:43:30 +02:00
mappings . sort ( ( a , b ) = > b . frequency - a . frequency )
}
2024-05-16 00:12:50 +02:00
return mappings
2024-05-13 17:21:40 +02:00
}
2024-06-16 16:06:26 +02:00
public static supportedTags (
type : "operator" | "brand" | "flag" | "transit" | string
) : Record < string , string [ ] > {
const tags : Record < string , string [ ] > = { }
2024-05-13 17:21:40 +02:00
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 ( "/" )
2024-06-16 16:06:26 +02:00
if ( type !== osmType && type + "s" !== osmType ) {
2024-05-13 17:21:40 +02:00
continue
}
if ( ! tags [ osmkey ] ) {
tags [ osmkey ] = [ ]
}
tags [ osmkey ] . push ( osmvalue )
}
return tags
}
2024-05-16 00:12:50 +02:00
/ * *
* 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 ) {
2024-05-23 04:42:26 +02:00
const suggestions = this . getSuggestionsForKV ( type , osmKey , osmValue )
2024-05-16 00:12:50 +02:00
options . push ( . . . suggestions )
2024-05-13 17:21:40 +02:00
}
}
2024-06-16 16:06:26 +02:00
return options
2024-05-13 17:21:40 +02:00
}
/ * *
*
2024-05-16 00:12:50 +02:00
* @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 ]
* /
2024-06-16 16:06:26 +02:00
public static 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 )
)
2024-05-23 04:42:26 +02:00
}
/ * *
2024-09-18 16:52:02 +02:00
* Caching for the resolved sets , as they can take a while
* @private
* /
private static resolvedSets : Record < string , any > = { }
/ * *
* Returns all suggestions for the given type ( brand | operator ) and main tag .
* Can optionally be filtered by countries and location set
*
2024-05-23 04:42:26 +02:00
*
* @param country : a string containing one or more country codes , separated by ";"
* @param location : center point of the feature , should be [ lon , lat ]
* /
2024-06-16 16:06:26 +02:00
public static getSuggestionsForKV (
type : string ,
key : string ,
value : string ,
country : string = undefined ,
location : [ number , number ] = undefined
) : NSIItem [ ] {
2024-05-13 17:21:40 +02:00
const path = ` ${ type } s/ ${ key } / ${ value } `
const entry = NameSuggestionIndex . nsiFile . nsi [ path ]
2024-09-18 16:52:02 +02:00
const countries = country ? . split ( ";" ) ? ? [ ]
2024-06-16 16:06:26 +02:00
return entry ? . items ? . filter ( ( i ) = > {
2024-05-13 17:21:40 +02:00
if ( i . locationSet . include . indexOf ( "001" ) >= 0 ) {
2024-09-18 16:52:02 +02:00
// this brand is spread globally
return true
}
if ( country === undefined ) {
// IF the country is not set, we are probably in some international area, it isn't loaded yet,
// or we are in a code path where we need everything (e.g. a script)
// We just allow everything
2024-05-13 17:21:40 +02:00
return true
}
2024-06-16 16:06:26 +02:00
if (
2024-09-18 16:52:02 +02:00
i . locationSet . include . some ( ( c ) = > countries . indexOf ( c ) >= 0 )
2024-06-16 16:06:26 +02:00
) {
2024-09-18 16:52:02 +02:00
// We prefer the countries provided by lonlat2country, they are more precise and are loaded already anyway (cheap)
// Country might contain multiple countries, separated by ';'
2024-05-13 17:21:40 +02:00
return true
}
2024-09-18 16:52:02 +02:00
if ( i . locationSet . exclude ? . some ( c = > countries . indexOf ( c ) >= 0 ) ) {
return false
}
2024-05-13 17:21:40 +02:00
if ( location === undefined ) {
return true
}
2024-09-18 16:52:02 +02:00
const hasSpecial = i . locationSet . include ? . some ( i = > i . endsWith ( ".geojson" ) || Array . isArray ( i ) ) || i . locationSet . exclude ? . some ( i = > i . endsWith ( ".geojson" ) || Array . isArray ( i ) )
if ( ! hasSpecial ) {
return false
}
const key = i . locationSet . include ? . join ( ";" ) + "-" + i . locationSet . exclude ? . join ( ";" )
const fromCache = NameSuggestionIndex . resolvedSets [ key ]
const resolvedSet = fromCache ? ? NameSuggestionIndex . loco . resolveLocationSet ( i . locationSet )
if ( ! fromCache ) {
NameSuggestionIndex . resolvedSets [ key ] = resolvedSet
}
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
} )
}
}