MapComplete/src/UI/InputElement/Helpers/NameSuggestionIndexInput.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>