Performance: load NSI when needed, should decrease bundle size

This commit is contained in:
Pieter Vander Vennet 2025-01-02 03:38:15 +01:00
parent 4fcdd8ba5a
commit a79557c87c
7 changed files with 122 additions and 76 deletions

View file

@ -24,6 +24,8 @@ else
exit 1 exit 1
fi fi
cp node_modules/name-suggestion-index/dist/nsi.json public/assets/data/nsi
cp node_modules/name-suggestion-index/dist/wikidata.min.json public/assets/data/nsi
export NODE_OPTIONS=--max-old-space-size=16000 export NODE_OPTIONS=--max-old-space-size=16000
which vite which vite

View file

@ -1,6 +1,3 @@
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"
import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json" import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
import { LocationConflation } from "@rapideditor/location-conflation" import { LocationConflation } from "@rapideditor/location-conflation"
import type { Feature, MultiPolygon } from "geojson" import type { Feature, MultiPolygon } from "geojson"
@ -56,27 +53,53 @@ export interface NSIItem {
} }
export default class NameSuggestionIndex { export default class NameSuggestionIndex {
private static readonly nsiFile: Readonly<NSIFile> = <any>nsi public static readonly supportedTypes = ["brand",
private static readonly nsiWdFile: Readonly< "flag",
"operator",
"transit"] as const
private readonly nsiFile: Readonly<NSIFile>
private readonly nsiWdFile: Readonly<
Record< Record<
string, string,
{ {
logos: { wikidata?: string; facebook?: string } logos: { wikidata?: string; facebook?: string }
} }
> >
> = <any>nsiWD["wikidata"] >
private static loco = new LocationConflation(nsiFeatures) // Some additional boundaries private static loco = new LocationConflation(nsiFeatures) // Some additional boundaries
private static _supportedTypes: string[] private _supportedTypes: string[]
public static supportedTypes(): string[] { constructor(nsiFile: Readonly<NSIFile>, nsiWdFile: Readonly<
Record<
string,
{
logos: { wikidata?: string; facebook?: string }
}
>>) {
this.nsiFile = nsiFile
this.nsiWdFile = nsiWdFile
}
private static inited: NameSuggestionIndex = undefined
public static async getNsiIndex(): Promise<NameSuggestionIndex> {
if (NameSuggestionIndex.inited) {
return NameSuggestionIndex.inited
}
const [nsi, nsiWd] = await Promise.all(["assets/data/nsi/nsi.json", "assets/data/nsi/wikidata.min.json"].map(url => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30)))
NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"])
return NameSuggestionIndex.inited
}
public supportedTypes(): string[] {
if (this._supportedTypes) { if (this._supportedTypes) {
return this._supportedTypes return this._supportedTypes
} }
const keys = Object.keys(NameSuggestionIndex.nsiFile.nsi) const keys = Object.keys(this.nsiFile.nsi)
const all = keys.map( const all = keys.map(
(k) => NameSuggestionIndex.nsiFile.nsi[k].properties.path.split("/")[0] (k) => this.nsiFile.nsi[k].properties.path.split("/")[0],
) )
this._supportedTypes = Utils.Dedup(all).map((s) => { this._supportedTypes = Utils.Dedup(all).map((s) => {
if (s.endsWith("s")) { if (s.endsWith("s")) {
@ -99,13 +122,13 @@ export default class NameSuggestionIndex {
try { try {
return Utils.downloadJsonCached<Record<string, number>>( return Utils.downloadJsonCached<Record<string, number>>(
`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`, `./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
24 * 60 * 60 * 1000 24 * 60 * 60 * 1000,
) )
} catch (e) { } catch (e) {
console.error("Could not fetch " + type + " statistics due to", e) console.error("Could not fetch " + type + " statistics due to", e)
return undefined return undefined
} }
}) }),
) )
stats = Utils.NoNull(stats) stats = Utils.NoNull(stats)
if (stats.length === 1) { if (stats.length === 1) {
@ -123,7 +146,10 @@ export default class NameSuggestionIndex {
return merged return merged
} }
public static isSvg(nsiItem: NSIItem, type: string): boolean | undefined { 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 const logos = this.nsiWdFile[nsiItem?.tags?.[type + ":wikidata"]]?.logos
if (!logos) { if (!logos) {
return undefined return undefined
@ -138,7 +164,7 @@ export default class NameSuggestionIndex {
return false return false
} }
public static async generateMappings( public async generateMappings(
type: string, type: string,
tags: Record<string, string>, tags: Record<string, string>,
country: string[], country: string[],
@ -148,7 +174,7 @@ export default class NameSuggestionIndex {
* If set, sort by frequency instead of alphabetically * If set, sort by frequency instead of alphabetically
*/ */
sortByFrequency: boolean sortByFrequency: boolean
} },
): Promise<Mapping[]> { ): Promise<Mapping[]> {
const mappings: (Mapping & { frequency: number })[] = [] const mappings: (Mapping & { frequency: number })[] = []
const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country) const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country)
@ -157,12 +183,12 @@ export default class NameSuggestionIndex {
continue continue
} }
const value = tags[key] const value = tags[key]
const actualBrands = NameSuggestionIndex.getSuggestionsForKV( const actualBrands = this.getSuggestionsForKV(
type, type,
key, key,
value, value,
country.join(";"), country.join(";"),
location location,
) )
if (!actualBrands) { if (!actualBrands) {
continue continue
@ -177,7 +203,7 @@ export default class NameSuggestionIndex {
if (hasIcon) { if (hasIcon) {
// Using <img src=...> works fine without an extension for JPG and PNG, but _not_ svg :( // Using <img src=...> works fine without an extension for JPG and PNG, but _not_ svg :(
icon = "./assets/data/nsi/logos/" + nsiItem.id icon = "./assets/data/nsi/logos/" + nsiItem.id
if (NameSuggestionIndex.isSvg(nsiItem, type)) { if (this.isSvg(nsiItem, type)) {
icon = icon + ".svg" icon = icon + ".svg"
} }
} }
@ -207,13 +233,13 @@ export default class NameSuggestionIndex {
return mappings return mappings
} }
public static supportedTags( public supportedTags(
type: "operator" | "brand" | "flag" | "transit" | string type: "operator" | "brand" | "flag" | "transit" | string,
): Record<string, string[]> { ): Record<string, string[]> {
const tags: Record<string, string[]> = {} const tags: Record<string, string[]> = {}
const keys = Object.keys(NameSuggestionIndex.nsiFile.nsi) const keys = Object.keys(this.nsiFile.nsi)
for (const key of keys) { for (const key of keys) {
const nsiItem = NameSuggestionIndex.nsiFile.nsi[key] const nsiItem = this.nsiFile.nsi[key]
const path = nsiItem.properties.path const path = nsiItem.properties.path
const [osmType, osmkey, osmvalue] = path.split("/") const [osmType, osmkey, osmvalue] = path.split("/")
if (type !== osmType && type + "s" !== osmType) { if (type !== osmType && type + "s" !== osmType) {
@ -231,9 +257,9 @@ export default class NameSuggestionIndex {
* Returns a list of all brands/operators * Returns a list of all brands/operators
* @param type * @param type
*/ */
public static allPossible(type: "brand" | "operator"): NSIItem[] { public allPossible(type: "brand" | "operator"): NSIItem[] {
const options: NSIItem[] = [] const options: NSIItem[] = []
const tags = NameSuggestionIndex.supportedTags(type) const tags = this.supportedTags(type)
for (const osmKey in tags) { for (const osmKey in tags) {
const values = tags[osmKey] const values = tags[osmKey]
for (const osmValue of values) { for (const osmValue of values) {
@ -249,14 +275,14 @@ export default class NameSuggestionIndex {
* @param country: a string containing one or more country codes, separated by ";" * @param country: a string containing one or more country codes, separated by ";"
* @param location: center point of the feature, should be [lon, lat] * @param location: center point of the feature, should be [lon, lat]
*/ */
public static getSuggestionsFor( public getSuggestionsFor(
type: string, type: string,
tags: { key: string; value: string }[], tags: { key: string; value: string }[],
country: string = undefined, country: string = undefined,
location: [number, number] = undefined location: [number, number] = undefined,
): NSIItem[] { ): NSIItem[] {
return tags.flatMap((tag) => return tags.flatMap((tag) =>
this.getSuggestionsForKV(type, tag.key, tag.value, country, location) this.getSuggestionsForKV(type, tag.key, tag.value, country, location),
) )
} }
@ -274,15 +300,15 @@ export default class NameSuggestionIndex {
* @param country: a string containing one or more country codes, separated by ";" * @param country: a string containing one or more country codes, separated by ";"
* @param location: center point of the feature, should be [lon, lat] * @param location: center point of the feature, should be [lon, lat]
*/ */
public static getSuggestionsForKV( public getSuggestionsForKV(
type: string, type: string,
key: string, key: string,
value: string, value: string,
country: string = undefined, country: string = undefined,
location: [number, number] = undefined location: [number, number] = undefined,
): NSIItem[] { ): NSIItem[] {
const path = `${type}s/${key}/${value}` const path = `${type}s/${key}/${value}`
const entry = NameSuggestionIndex.nsiFile.nsi[path] const entry = this.nsiFile.nsi[path]
const countries = country?.split(";") ?? [] const countries = country?.split(";") ?? []
return entry?.items?.filter((i) => { return entry?.items?.filter((i) => {
if (i.locationSet.include.indexOf("001") >= 0) { if (i.locationSet.include.indexOf("001") >= 0) {
@ -335,4 +361,11 @@ export default class NameSuggestionIndex {
return false return false
}) })
} }
public static async generateMappings(key: string, tags: Exclude<Record<string, string>, undefined | null>, country: string[], center: [number, number], options: {
sortByFrequency: boolean
}): Promise<Mapping[]> {
const nsi = await NameSuggestionIndex.getNsiIndex()
return nsi.generateMappings(key, tags, country, center, options)
}
} }

View file

@ -1,17 +1,13 @@
import { DesugaringStep } from "./Conversion" import { DesugaringStep } from "./Conversion"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { Translation } from "../../../UI/i18n/Translation" import { Translation } from "../../../UI/i18n/Translation"
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
import { TagUtils } from "../../../Logic/Tags/TagUtils" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { Tag } from "../../../Logic/Tags/Tag"
import Validators from "../../../UI/InputElement/Validators" import Validators from "../../../UI/InputElement/Validators"
import { CheckTranslation } from "./Validation" import { CheckTranslation } from "./Validation"
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> { export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
private readonly _layerConfig: LayerConfigJson private readonly _layerConfig: LayerConfigJson
@ -197,11 +193,11 @@ export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJso
} }
} }
if ( if (
this._layerConfig?.source?.osmTags && this._layerConfig?.source?.["osmTags"] &&
NameSuggestionIndex.supportedTypes().indexOf(json.freeform.key) >= 0 NameSuggestionIndex.supportedTypes.indexOf(<any> json.freeform.key) >= 0
) { ) {
const tags = TagUtils.TagD(this._layerConfig?.source?.osmTags)?.usedTags() const tags = TagUtils.TagD(this._layerConfig?.source?.["osmTags"])?.usedTags()
const suggestions = NameSuggestionIndex.getSuggestionsFor(json.freeform.key, tags) /* const suggestions = nameSuggestionIndexBundled.getSuggestionsFor(json.freeform.key, tags)
if (suggestions === undefined) { if (suggestions === undefined) {
context context
.enters("freeform", "type") .enters("freeform", "type")
@ -209,8 +205,8 @@ export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJso
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " + "No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ") tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ")
) )
} }*/
} else if (json.freeform.type === "nsi") { } else if (json.freeform["type"] === "nsi") {
context context
.enters("freeform", "type") .enters("freeform", "type")
.warn( .warn(

View file

@ -999,7 +999,7 @@ export class TagRenderingConfigUtils {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
feature?: Feature feature?: Feature
): Store<TagRenderingConfig> { ): Store<TagRenderingConfig> {
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0 const isNSI = NameSuggestionIndex.supportedTypes.indexOf(<any> config.freeform?.key) >= 0
if (!isNSI) { if (!isNSI) {
return new ImmutableStore(config) return new ImmutableStore(config)
} }
@ -1019,8 +1019,8 @@ export class TagRenderingConfigUtils {
) )
) )
}) })
return extraMappings.map((extraMappings) => { return extraMappings.mapD((extraMappings) => {
if (!extraMappings || extraMappings.length == 0) { if (extraMappings.length == 0) {
return config return config
} }
const clone: TagRenderingConfig = Object.create(config) const clone: TagRenderingConfig = Object.create(config)

View file

@ -7,6 +7,7 @@
import { UIEventSource } from "../../../Logic/UIEventSource" import { UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte" import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import Loading from "../../Base/Loading.svelte"
export let tags: UIEventSource<Record<string, string> | undefined> export let tags: UIEventSource<Record<string, string> | undefined>
@ -20,12 +21,16 @@
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement) let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script> </script>
<TagRenderingAnswer {#if $dynamicConfig === undefined}
{selectedElement} <Loading />
{layer} {:else}
config={$dynamicConfig} <TagRenderingAnswer
{extraClasses} {selectedElement}
{id} {layer}
{tags} config={$dynamicConfig}
{state} {extraClasses}
/> {id}
{tags}
{state}
/>
{/if}

View file

@ -7,6 +7,7 @@
import type { SpecialVisualizationState } from "../../SpecialVisualization" import type { SpecialVisualizationState } from "../../SpecialVisualization"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import TagRenderingEditable from "./TagRenderingEditable.svelte" import TagRenderingEditable from "./TagRenderingEditable.svelte"
import Loading from "../../Base/Loading.svelte"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -23,14 +24,18 @@
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement) let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script> </script>
<TagRenderingEditable {#if $dynamicConfig}
config={$dynamicConfig} <TagRenderingEditable
{editMode} config={$dynamicConfig}
{clss} {editMode}
{highlightedRendering} {clss}
{editingEnabled} {highlightedRendering}
{layer} {editingEnabled}
{state} {layer}
{selectedElement} {state}
{tags} {selectedElement}
/> {tags}
/>
{:else}
<Loading />
{/if}

View file

@ -18,6 +18,7 @@
import { twJoin } from "tailwind-merge" import { twJoin } from "tailwind-merge"
import Tr from "../../Base/Tr.svelte" import Tr from "../../Base/Tr.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid" import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import Loading from "../../Base/Loading.svelte"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -31,14 +32,18 @@
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement) let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script> </script>
<TagRenderingQuestion {#if $dynamicConfig }
{tags} <TagRenderingQuestion
config={$dynamicConfig} {tags}
{state} config={$dynamicConfig}
{selectedElement} {state}
{layer} {selectedElement}
{selectedTags} {layer}
{extraTags} {selectedTags}
> {extraTags}
<slot name="cancel" slot="cancel" /> >
</TagRenderingQuestion> <slot name="cancel" slot="cancel" />
</TagRenderingQuestion>
{:else}
<Loading />
{/if}