forked from MapComplete/MapComplete
Some work on NSI
This commit is contained in:
parent
1dc7c2a45b
commit
d719d0e1be
4 changed files with 112 additions and 77 deletions
80
src/Logic/Web/NameSuggestionIndex.ts
Normal file
80
src/Logic/Web/NameSuggestionIndex.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import * as nsi from "../../../node_modules/name-suggestion-index/dist/nsi.json"
|
||||||
|
import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
|
||||||
|
import { LocationConflation } from "@rapideditor/location-conflation"
|
||||||
|
import type { Feature, MultiPolygon } from "geojson"
|
||||||
|
import * as turf from "@turf/turf"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
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 singleton: NameSuggestionIndex
|
||||||
|
|
||||||
|
public static getSuggestionsFor(path: string, country: Set<string>): NSIItem[] {
|
||||||
|
const entry = NameSuggestionIndex.nsiFile.nsi[path]
|
||||||
|
return entry.items.filter(i => {
|
||||||
|
if(i.locationSet.include.indexOf("001") >= 0){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(i.locationSet.include.some(c => country.indexOf(c) >= 0)){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedSet = NameSuggestionIndex.loco.resolveLocationSet(item.locationSet)
|
||||||
|
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([lon, lat], setFeature.geometry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1091,6 +1091,9 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(json.freeform.type === "nsi"){
|
||||||
|
throw "Should validate NSI: path should exist"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (json.render && json["question"] && json.freeform === undefined) {
|
if (json.render && json["question"] && json.freeform === undefined) {
|
||||||
context.err(
|
context.err(
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Feature, MultiPolygon } from "geojson"
|
import type { Feature, MultiPolygon } from "geojson"
|
||||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import * as nsi from "../../../../node_modules/name-suggestion-index/dist/nsi.json"
|
|
||||||
import * as nsiFeatures from "../../../../node_modules/name-suggestion-index/dist/featureCollection.json"
|
|
||||||
import { LocationConflation } from "@rapideditor/location-conflation"
|
|
||||||
import * as turf from "@turf/turf"
|
|
||||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||||
|
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
|
||||||
|
import type { NSIItem } from "../../../Logic/Web/NameSuggestionIndex"
|
||||||
|
|
||||||
const nsiFile: NSIFile = nsi
|
|
||||||
const loco = new LocationConflation(nsiFeatures)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All props for this input helper
|
|
||||||
*/
|
|
||||||
export let value: UIEventSource<string> = new UIEventSource<string>(undefined)
|
export let value: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||||
// Since we're getting extra tags aside from the standard we need to export them
|
// Since we're getting extra tags aside from the standard we need to export them
|
||||||
export let extraTags: UIEventSource<Record<string, string>>
|
export let extraTags: UIEventSource<Record<string, string>>
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
export let helperArgs: (string | number | boolean)[]
|
export let helperArgs: (string | number | boolean)[]
|
||||||
|
/**
|
||||||
|
* An inputhelper is always used with a freeform.
|
||||||
|
* The 'key' is what the value will be written into.
|
||||||
|
* This will probably be `brand` or `operator`
|
||||||
|
*/
|
||||||
export let key: string
|
export let key: string
|
||||||
|
|
||||||
let maintag = helperArgs[0].toString()
|
let maintag = helperArgs[0].toString()
|
||||||
|
@ -28,7 +27,7 @@
|
||||||
addExtraTags = helperArgs[1].split(";")
|
addExtraTags = helperArgs[1].split(";")
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = `${tag}s/${maintag.split("=")[0]}/${maintag.split("=")[1]}`
|
const path = `${key}s/${maintag.split("=")[0]}/${maintag.split("=")[1]}`
|
||||||
|
|
||||||
// Check if the path exists in the NSI file
|
// Check if the path exists in the NSI file
|
||||||
if (!nsiFile.nsi[path]) {
|
if (!nsiFile.nsi[path]) {
|
||||||
|
@ -36,21 +35,12 @@
|
||||||
throw new Error(`Path ${path} does not exist in the NSI file`)
|
throw new Error(`Path ${path} does not exist in the NSI file`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = nsiFile.nsi[path].items
|
let items: NSIItem[] = NameSuggestionIndex.getSuggestionsFor(path, feature.properties["_country"])
|
||||||
|
|
||||||
let selectedItem: NSIItem = items.find((item) => item.tags[tag] === value.data)
|
let selectedItem: NSIItem = items.find((item) => item.tags[key] === value.data)
|
||||||
|
|
||||||
// Get the coordinates if the feature is a point, otherwise use the center
|
// Get the coordinates if the feature is a point, otherwise use the center
|
||||||
let lon: number
|
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||||
let lat: number
|
|
||||||
if (feature.geometry.type === "Point") {
|
|
||||||
const coordinates = feature.geometry.coordinates
|
|
||||||
lon = coordinates[0]
|
|
||||||
lat = coordinates[1]
|
|
||||||
} else {
|
|
||||||
lon = feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2
|
|
||||||
lat = feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the items, first by the display name, then by the location set
|
* Filter the items, first by the display name, then by the location set
|
||||||
|
@ -60,78 +50,36 @@
|
||||||
.filter((item) => item.displayName.toLowerCase().includes(filter.toLowerCase()))
|
.filter((item) => item.displayName.toLowerCase().includes(filter.toLowerCase()))
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
// Check if the feature is in the location set using the location-conflation library
|
// Check if the feature is in the location set using the location-conflation library
|
||||||
const resolvedSet = loco.resolveLocationSet(item.locationSet)
|
|
||||||
if (resolvedSet) {
|
|
||||||
const setFeature: Feature<MultiPolygon> = resolvedSet.feature
|
|
||||||
// We actually have a location set, so we can check if the feature is in it, by determining if our point is inside of the MultiPolygon using @turf/boolean-point-in-polygon
|
|
||||||
return turf.booleanPointInPolygon([lon, lat], setFeature.geometry)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Some interfaces for the NSI files
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main name suggestion index file
|
|
||||||
*/
|
|
||||||
interface NSIFile {
|
|
||||||
_meta: {
|
|
||||||
version: string
|
|
||||||
generated: string
|
|
||||||
url: string
|
|
||||||
hash: string
|
|
||||||
}
|
|
||||||
nsi: {
|
|
||||||
[path: string]: NSIEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NSIEntry {
|
|
||||||
properties: {
|
|
||||||
path: string
|
|
||||||
skipCollection?: boolean
|
|
||||||
preserveTags?: string[]
|
|
||||||
exclude: unknown
|
|
||||||
}
|
|
||||||
items: NSIItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NSIItem {
|
|
||||||
displayName: string
|
|
||||||
id: string
|
|
||||||
locationSet: unknown
|
|
||||||
tags: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
fromTemplate?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called when an item is selected
|
* Function called when an item is selected
|
||||||
*/
|
*/
|
||||||
function select(item: NSIItem) {
|
function select(item: NSIItem) {
|
||||||
value.setData(item.tags[tag])
|
value.setData(item.tags[key])
|
||||||
selectedItem = item
|
selectedItem = item
|
||||||
// Tranform the object into record<string, string> and remove the tag we're using, as well as the maintag
|
// Transform the object into record<string, string> and remove the tag we're using, as well as the maintag
|
||||||
const tags = Object.entries(item.tags).reduce((acc, [key, value]) => {
|
const tags = Object.entries(item.tags).reduce((acc, [k, value]) => {
|
||||||
if (key !== tag && key !== maintag.split("=")[0]) {
|
if (k !== key && key !== maintag.split("=")[0]) {
|
||||||
acc[key] = value
|
acc[k] = value
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, string>)
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
// Also check if the object currently matches a different item, based on the key
|
// Also check if the object currently matches a different item, based on the key
|
||||||
const otherItem = items.find((item) => item.tags[tag] === feature.properties[key])
|
const otherItem = items.find((item) => item.tags[key] === feature.properties[key])
|
||||||
|
|
||||||
// If the other item is not the same as the selected item, we need to make sure we clear any old keys we don't need anymore by setting them to an empty string
|
// If the other item is not the same as the selected item, we need to make sure we clear any old keys we don't need anymore by setting them to an empty string
|
||||||
if (otherItem && otherItem !== item) {
|
if (otherItem && otherItem !== item) {
|
||||||
Object.keys(otherItem.tags).forEach((key) => {
|
Object.keys(otherItem.tags).forEach((k) => {
|
||||||
// If we have a different value for the key, we don't need to clear it
|
// If we have a different value for the key, we don't need to clear it
|
||||||
if (!tags[key] && key !== tag && key !== maintag.split("=")[0]) {
|
if (!tags[k] && key !== k && key !== maintag.split("=")[0]) {
|
||||||
console.log(`Clearing key ${key}`)
|
console.log(`Clearing key ${key}`)
|
||||||
tags[key] = ""
|
tags[k] = ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -154,14 +102,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the value is not selected, we check if there is an item with the same value and select it
|
// If the value is not selected, we check if there is an item with the same value and select it
|
||||||
const item = items.find((item) => item.tags[tag] === value)
|
const item = items.find((item) => item.tags[key] === value)
|
||||||
if (item) {
|
if (item) {
|
||||||
select(item)
|
select(item)
|
||||||
} else {
|
} else {
|
||||||
// If there is no item with the value, we need to clear the extra tags based on the last selected item by looping over the tags from the last selected item
|
// If there is no item with the value, we need to clear the extra tags based on the last selected item by looping over the tags from the last selected item
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
const tags = Object.entries(selectedItem.tags).reduce((acc, [key, value]) => {
|
const tags = Object.entries(selectedItem.tags).reduce((acc, [k, value]) => {
|
||||||
if (key !== tag && key !== maintag.split("=")[0]) {
|
if (key !== k && key !== maintag.split("=")[0]) {
|
||||||
acc[key] = ""
|
acc[key] = ""
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
import Test from "./UI/Test.svelte"
|
import Test from "./UI/Test.svelte"
|
||||||
|
import NameSuggestionIndex from "./Logic/Web/NameSuggestionIndex"
|
||||||
|
|
||||||
|
const nsi = NameSuggestionIndex.get()
|
||||||
|
const secondhandshops = nsi.getSuggestionsFor("brands/shop/second_hand", ["be"])
|
||||||
|
console.log(secondhandshops)
|
||||||
new SvelteUIElement(Test).AttachTo("maindiv")
|
new SvelteUIElement(Test).AttachTo("maindiv")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue