forked from MapComplete/MapComplete
165 lines
5.2 KiB
Svelte
165 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import type { Feature, MultiPolygon } from "geojson"
|
|
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 { twMerge } from "tailwind-merge"
|
|
|
|
const nsiFile: NSIFile = nsi
|
|
const loco = new LocationConflation(nsiFeatures)
|
|
|
|
/**
|
|
* All props for this input helper
|
|
*/
|
|
export let value: UIEventSource<string> = new UIEventSource<string>(undefined)
|
|
// Since we're getting extra tags aside from the standard we need to export them
|
|
export let extraTags: UIEventSource<Record<string, string>>
|
|
export let feature: Feature
|
|
export let helperArgs: (string | number | boolean)[]
|
|
export let key: string
|
|
|
|
let maintag = helperArgs[0].toString()
|
|
let tag = key
|
|
|
|
const path = `${tag}s/${maintag.split("=")[0]}/${maintag.split("=")[1]}`
|
|
|
|
// Check if the path exists in the NSI file
|
|
if (!nsiFile.nsi[path]) {
|
|
console.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 selectedItem: NSIItem = items.find((item) => item.tags[tag] === value.data)
|
|
|
|
// Get the coordinates if the feature is a point, otherwise use the center
|
|
let lon: number
|
|
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
|
|
*/
|
|
let filter = ""
|
|
$: filteredItems = items
|
|
.filter((item) => item.displayName.toLowerCase().includes(filter.toLowerCase()))
|
|
.filter((item) => {
|
|
// 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
|
|
})
|
|
|
|
/**
|
|
* 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 select(item: NSIItem) {
|
|
value.setData(item.tags[tag])
|
|
selectedItem = item
|
|
// Tranform 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]) => {
|
|
if (key !== tag && key !== maintag.split("=")[0]) {
|
|
acc[key] = value
|
|
}
|
|
return acc
|
|
}, {} as Record<string, string>)
|
|
|
|
// 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])
|
|
|
|
// 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) {
|
|
Object.keys(otherItem.tags).forEach((key) => {
|
|
// 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]) {
|
|
console.log(`Clearing key ${key}`)
|
|
tags[key] = ""
|
|
}
|
|
})
|
|
}
|
|
|
|
// Finally, set the extra tags
|
|
extraTags.setData(tags)
|
|
}
|
|
</script>
|
|
|
|
<div>
|
|
<div class="normal-background my-2 flex w-5/6 justify-between rounded-full pl-2">
|
|
<input type="text" placeholder="Filter entries" bind:value={filter} class="outline-none" />
|
|
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" />
|
|
</div>
|
|
<div class="flex h-36 w-full flex-wrap overflow-scroll">
|
|
{#each filteredItems as item}
|
|
<div
|
|
class={twMerge(
|
|
"m-1 h-fit rounded-full border-2 border-black p-4 text-center",
|
|
selectedItem === item ? "interactive" : "bg-white"
|
|
)}
|
|
on:click={() => {
|
|
select(item)
|
|
}}
|
|
on:keydown={(e) => {
|
|
if (e.key === "Enter") {
|
|
select(item)
|
|
}
|
|
}}
|
|
>
|
|
{item.displayName}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|