MapComplete/src/UI/InputElement/Helpers/NameSuggestionIndexInput.svelte

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

166 lines
5.2 KiB
Svelte
Raw Normal View History

2024-04-25 00:56:36 +02:00
<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"
2024-04-25 21:52:48 +02:00
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twMerge } from "tailwind-merge"
2024-04-25 00:56:36 +02:00
const nsiFile: NSIFile = nsi
const loco = new LocationConflation(nsiFeatures)
/**
* All props for this input helper
*/
export let value: UIEventSource<string> = new UIEventSource<string>(undefined)
2024-04-25 21:00:14 +02:00
// Since we're getting extra tags aside from the standard we need to export them
export let extraTags: UIEventSource<Record<string, string>>
2024-04-25 00:56:36 +02:00
export let feature: Feature
2024-04-25 10:52:10 +02:00
export let helperArgs: (string | number | boolean)[]
export let key: string
2024-04-25 00:56:36 +02:00
2024-04-25 10:52:10 +02:00
let maintag = helperArgs[0].toString()
let tag = key
2024-04-25 00:56:36 +02:00
const path = `${tag}s/${maintag.split("=")[0]}/${maintag.split("=")[1]}`
2024-04-25 10:52:10 +02:00
// 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`)
}
2024-04-25 00:56:36 +02:00
let items = nsiFile.nsi[path].items
2024-04-25 21:52:48 +02:00
let selectedItem: NSIItem = items.find((item) => item.tags[tag] === value.data)
2024-04-25 00:56:36 +02:00
// 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
}
2024-04-25 10:52:10 +02:00
/**
* Filter the items, first by the display name, then by the location set
*/
2024-04-25 00:56:36 +02:00
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
*/
2024-04-25 21:00:14 +02:00
/**
* Main name suggestion index file
*/
2024-04-25 00:56:36 +02:00
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
}
2024-04-25 21:00:14 +02:00
/**
* 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>)
2024-04-29 00:54:32 +02:00
// 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
2024-04-25 21:00:14 +02:00
extraTags.setData(tags)
}
2024-04-25 00:56:36 +02:00
</script>
<div>
2024-04-25 21:52:48 +02:00
<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">
2024-04-25 00:56:36 +02:00
{#each filteredItems as item}
<div
2024-04-25 21:52:48 +02:00
class={twMerge(
"m-1 h-fit rounded-full border-2 border-black p-4 text-center",
selectedItem === item ? "interactive" : "bg-white"
)}
2024-04-25 00:56:36 +02:00
on:click={() => {
2024-04-25 21:00:14 +02:00
select(item)
}}
on:keydown={(e) => {
if (e.key === "Enter") {
select(item)
}
2024-04-25 00:56:36 +02:00
}}
>
{item.displayName}
</div>
{/each}
</div>
</div>