Merge master

This commit is contained in:
Pieter Vander Vennet 2025-04-27 00:34:30 +02:00
commit b61f930fea
39 changed files with 525 additions and 22753 deletions

View file

@ -13,7 +13,6 @@ import StaticFeatureSource, {
} from "../FeatureSource/Sources/StaticFeatureSource"
import { MapProperties } from "../../Models/MapProperties"
import { Orientation } from "../../Sensors/Orientation"
;("use strict")
/**
* The geolocation-handler takes a map-location and a geolocation state.

View file

@ -330,6 +330,9 @@ ${nds}${tags} </way>
}
private isPolygon(): boolean {
if (this.coordinates.length === 0) {
return false
}
// Compare lat and lon seperately, as the coordinate array might not be a reference to the same object
if (
this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] ||

View file

@ -5,7 +5,6 @@ import { BBox } from "../BBox"
import osmtogeojson from "osmtogeojson"
import { FeatureCollection, Geometry } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
;("use strict")
/**
* Interfaces overpass to get all the latest data

View file

@ -8,6 +8,7 @@ import { TypedTranslation } from "../../UI/i18n/Translation"
import { RegexTag } from "../Tags/RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { TagUtils } from "../Tags/TagUtils"
import Constants from "../../Models/Constants"
/**
* Main name suggestion index file
@ -69,8 +70,10 @@ export default class NameSuggestionIndex {
private loco: LocationConflation // Some additional boundaries
private _supportedTypes: string[]
private _serverLocation: string
constructor(
private constructor(
serverLocation: string,
nsiFile: Readonly<NSIFile>,
nsiWdFile: Readonly<
Record<
@ -82,6 +85,7 @@ export default class NameSuggestionIndex {
>,
features: Readonly<FeatureCollection>
) {
this._serverLocation = serverLocation
this.nsiFile = nsiFile
this.nsiWdFile = nsiWdFile
this.loco = new LocationConflation(features)
@ -101,6 +105,7 @@ export default class NameSuggestionIndex {
].map((url) => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30))
)
NameSuggestionIndex.inited = new NameSuggestionIndex(
Constants.nsiLogosEndpoint,
<any>nsi,
<any>nsiWd["wikidata"],
<any>features
@ -129,12 +134,13 @@ export default class NameSuggestionIndex {
* @param countries
* @private
*/
private static async fetchFrequenciesFor(type: string, countries: string[]) {
private async fetchFrequenciesFor(type: string, countries: string[]) {
const server = this._serverLocation
let stats = await Promise.all(
countries.map((c) => {
try {
return Utils.downloadJsonCached<Record<string, number>>(
`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
`${server}/stats/${type}.${c.toUpperCase()}.json`,
24 * 60 * 60 * 1000
)
} catch (e) {
@ -194,7 +200,7 @@ export default class NameSuggestionIndex {
const mappings: (Mapping & { frequency: number })[] = []
const frequencies =
country !== undefined
? await NameSuggestionIndex.fetchFrequenciesFor(type, country)
? await this.fetchFrequenciesFor(type, country)
: {}
for (const key in tags) {
if (key.startsWith("_")) {
@ -398,11 +404,12 @@ export default class NameSuggestionIndex {
}
public getIconUrl(nsiItem: NSIItem): string | undefined {
if (!nsiItem.ext) {
const baseUrl = this._serverLocation
if (!nsiItem.ext || baseUrl === null) {
// No extension -> there is no logo
return undefined
}
return "./assets/data/nsi/logos/" + nsiItem.id + "." + nsiItem.ext
return baseUrl +"/logos/"+ nsiItem.id + "." + nsiItem.ext
}
private static readonly brandPrefix = ["name", "alt_name", "operator", "brand"] as const

View file

@ -139,6 +139,7 @@ export default class Constants {
public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials
public static nominatimEndpoint: string = Constants.config.nominatimEndpoint
public static photonEndpoint: string = Constants.config.photonEndpoint
public static nsiLogosEndpoint: string = Constants.config.nsi_logos_server ?? null
public static weblate: string = "https://translate.mapcomplete.org/"
public static linkedDataProxy: string = Constants.config["jsonld-proxy"]

View file

@ -1,48 +0,0 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Title from "../Base/Title"
import TagRenderingChart from "./TagRenderingChart"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
import BaseUIElement from "../BaseUIElement"
export default class StatisticsForLayerPanel extends VariableUiElement {
constructor(elementsInview: FeatureSourceForLayer) {
const layer = elementsInview.layer.layerDef
super(
elementsInview.features.stabilized(1000).map(
(features) => {
const els: BaseUIElement[] = []
const featuresForLayer = features
if (featuresForLayer.length === 0) {
return
}
els.push(new Title(layer.name, 1).SetClass("mt-8"))
const layerStats = []
for (const tagRendering of layer?.tagRenderings ?? []) {
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
chartclasses: "w-full",
chartstyle: "height: 60rem",
includeTitle: false,
})
const title = new Title(
tagRendering.question?.Clone() ?? tagRendering.id,
4
).SetClass("mt-8")
if (!chart.HasClass("hidden")) {
layerStats.push(
new Combine([title, chart]).SetClass(
"flex flex-col w-full lg:w-1/3"
)
)
}
}
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
return new Combine(els)
},
[Locale.language]
)
)
}
}

View file

@ -247,7 +247,7 @@ export default class TagRenderingChart extends Combine {
}
const config = <ChartConfiguration>{
type: options.chartType ?? (barchartMode ? "bar" : "doughnut"),
type: options.chartType ?? (barchartMode ? "bar" : "pie"),
data: {
labels,
datasets: [

View file

@ -10,6 +10,9 @@
import Tr from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Translations from "../i18n/Translations"
import TagRenderingChart from "../BigComponents/TagRenderingChart"
import ToSvelte from "../Base/ToSvelte.svelte"
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
export let onlyShowUsername: string[]
export let features: Feature[]
@ -25,10 +28,13 @@
key: string
value?: string
oldValue?: string
step: OsmObject
}[]
> = allHistories.mapD((histories) => HistoryUtils.fullHistoryDiff(histories, usernames))
const trs = shared_questions.tagRenderings.map((tr) => new TagRenderingConfig(tr))
const trs = shared_questions.tagRenderings.map(
(tr) => new TagRenderingConfig(<TagRenderingConfigJson>tr)
)
function detectQuestion(key: string): TagRenderingConfig {
return trs.find((tr) => tr.freeform?.key === key)
@ -40,17 +46,19 @@
tr: TagRenderingConfig
count: number
values: { value: string; count: number }[]
features: Feature[]
}[]
> = allDiffs.mapD((allDiffs) => {
const keyCounts = new Map<string, Map<string, number>>()
const keyCounts = new Map<string, Map<string, OsmObject[]>>()
for (const diff of allDiffs) {
const k = diff.key
if (!keyCounts.has(k)) {
keyCounts.set(k, new Map<string, number>())
keyCounts.set(k, new Map<string, OsmObject[]>())
}
const valueCounts = keyCounts.get(k)
const v = diff.value ?? ""
valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0))
const oldFeaturesList = valueCounts.get(v) ?? []
valueCounts.set(v, [...oldFeaturesList, diff.step])
}
const perKey: {
@ -58,19 +66,29 @@
tr: TagRenderingConfig
count: number
values: { value: string; count: number }[]
features: Feature[]
}[] = []
keyCounts.forEach((values, key) => {
const keyTotal: { value: string; count: number }[] = []
keyCounts.forEach((values: Map<string, OsmObject[]>, key: string) => {
const keyTotal: { value: string; features: Feature[] }[] = []
values.forEach((count, value) => {
keyTotal.push({ value, count })
keyTotal.push({ value, features: count.map((step) => step.asGeoJson()) })
})
let countForKey = 0
for (const { count } of keyTotal) {
countForKey += count
let countForKey: Feature[] = []
for (const { features } of keyTotal) {
countForKey.push(...features)
}
keyTotal.sort((a, b) => b.count - a.count)
keyTotal.sort((a, b) => b.features.length - a.features.length)
const tr = detectQuestion(key)
perKey.push({ count: countForKey, tr, key, values: keyTotal })
perKey.push({
count: countForKey.length,
tr,
key,
values: keyTotal.map(({ value, features }) => ({
value,
count: features.length,
})),
features: countForKey,
})
})
perKey.sort((a, b) => b.count - a.count)
@ -105,6 +123,19 @@
</li>
{/each}
</ul>
{#if diff.tr}
<div class="h-48 w-48">
<ToSvelte
construct={new TagRenderingChart(diff.features, diff.tr, {
groupToOtherCutoff: 0,
chartType: "pie",
sort: true,
})}
/>
</div>
{:else}
Could not create a graph
{/if}
</AccordionSingle>
{/each}
{/if}

View file

@ -44,22 +44,37 @@ export class HistoryUtils {
.filter((ch) => ch.oldValue !== ch.value)
}
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>) {
const allDiffs: { key: string; oldValue?: string; value?: string }[] = [].concat(
...histories.map((history) => {
const filtered = history.filter(
(step) =>
!onlyShowUsername ||
onlyShowUsername?.has(step.tags["_last_edit:contributor"])
)
const diffs: {
public static fullHistoryDiff(
histories: OsmObject[][],
onlyShowUsername?: Set<string>
): {
key: string
value?: string
oldValue?: string
step: OsmObject
}[] {
const allDiffs: {
key: string
value?: string
oldValue?: string
step: OsmObject
}[] = []
for (const history of histories) {
const filtered = history.filter(
(step) =>
!onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"])
)
for (const step of filtered) {
const diff: {
key: string
value?: string
oldValue?: string
}[][] = filtered.map((step) => HistoryUtils.tagHistoryDiff(step, history))
return [].concat(...diffs)
})
)
step: OsmObject
}[] = HistoryUtils.tagHistoryDiff(step, history)
allDiffs.push(...diff)
}
}
return allDiffs
}
}

View file

@ -70,6 +70,7 @@
searchIsRunning.set(false)
}
})
let state = {
mapProperties: maplibremap,
searchState: {
@ -164,6 +165,18 @@
let showPreviouslyVisited = new UIEventSource(true)
const t = Translations.t.inspector
function search(suggestion?: GeocodeResult) {
suggestion ??= searchSuggestions?.data?.[0]
console.log("Seaching", suggestion)
if (!suggestion) {
return
}
map.data.flyTo({
zoom: 14,
center: [suggestion.lon, suggestion.lat],
})
}
</script>
<div class="flex h-screen w-full flex-col">
@ -251,12 +264,13 @@
<MaplibreMap {map} mapProperties={maplibremap} autorecovery={true} />
<div class="absolute right-0 top-0 w-1/4 p-4">
<Searchbar
on:search={() => search()}
isFocused={searchIsFocussed}
value={searchvalue}
on:focus={() => state.searchState.showSearchDrawer.set(true)}
/>
{#if $searchSuggestions?.length > 0 || $searchIsFocussed}
<GeocodeResults {state} />
<GeocodeResults {state} on:select={(event) => search(event.detail)} />
{/if}
</div>
</div>

View file

@ -13,6 +13,7 @@
import FeaturePropertiesStore from "../../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import SearchState from "../../Logic/State/SearchState"
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
import { createEventDispatcher } from "svelte"
export let entry: GeocodeResult
export let state: {
@ -40,8 +41,9 @@
let mapRotation = state.mapProperties.rotation
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]))
let dispatch = createEventDispatcher<{ select: GeocodeResult }>()
function select() {
state.searchState.applyGeocodeResult(entry)
dispatch("select", entry)
}
</script>

View file

@ -55,7 +55,12 @@
{#if $allowFilters}
<FilterResults {state} />
{/if}
<GeocodeResults {state}>
<GeocodeResults
{state}
on:select={(select) => {
state.searchState.applyGeocodeResult(select.detail)
}}
>
<svelte:fragment slot="if-no-results">
{#if $recentlySeen?.length > 0}
<SidebarUnit>

View file

@ -403,7 +403,7 @@ export default class SpecialVisualizations {
},
{
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
docs: "Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer",
args: [],
constr: (state) => new SvelteUIElement(AllFeaturesStatistics, { state }),

View file

@ -1,11 +1,11 @@
{
"contributors": [
{
"commits": 9515,
"commits": 9531,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 546,
"commits": 557,
"contributor": "Robin van der Linde"
},
{

View file

@ -682,9 +682,9 @@
"vi"
],
"VU": [
"fr",
"en",
"bi"
"bi",
"fr"
],
"WS": [
"en",

View file

@ -11133,6 +11133,10 @@
"if": "value=memorial",
"then": "memorial - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
},
{
"if": "value=mobility_hub",
"then": "mobility_hub - Mobility hubs are places where different kinds of transit meet, making it easy to switch between them. These places are usually part of a larger network or system."
},
{
"if": "value=mountain_rescue",
"then": "mountain_rescue - A building where first aid responders store material and might be on watch"

View file

@ -1012,6 +1012,10 @@
"if": "value=memorial",
"then": "<b>memorial</b> (builtin) - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
},
{
"if": "value=mobility_hub",
"then": "<b>mobility_hub</b> (builtin) - Mobility hubs are places where different kinds of transit meet, making it easy to switch between them. These places are usually part of a larger network or system."
},
{
"if": "value=mountain_rescue",
"then": "<b>mountain_rescue</b> (builtin) - A building where first aid responders store material and might be on watch"
@ -13777,6 +13781,10 @@
"if": "value=memorial",
"then": "memorial - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
},
{
"if": "value=mobility_hub",
"then": "mobility_hub - Mobility hubs are places where different kinds of transit meet, making it easy to switch between them. These places are usually part of a larger network or system."
},
{
"if": "value=mountain_rescue",
"then": "mountain_rescue - A building where first aid responders store material and might be on watch"
@ -35578,6 +35586,10 @@
"if": "value=memorial",
"then": "memorial - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
},
{
"if": "value=mobility_hub",
"then": "mobility_hub - Mobility hubs are places where different kinds of transit meet, making it easy to switch between them. These places are usually part of a larger network or system."
},
{
"if": "value=mountain_rescue",
"then": "mountain_rescue - A building where first aid responders store material and might be on watch"