Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2024-07-17 18:45:28 +02:00
commit 196659649f
339 changed files with 34609 additions and 26392 deletions

View file

@ -11,7 +11,7 @@ export default class SvelteUIElement<
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends BaseUIElement {
private readonly _svelteComponent: {
public readonly _svelteComponent: {
new (args: {
target: HTMLElement
props: Props
@ -19,10 +19,11 @@ export default class SvelteUIElement<
slots?: Slots
}): SvelteComponentTyped<Props, Events, Slots>
}
private readonly _props: Props
private readonly _events: Events
private readonly _slots: Slots
public readonly _props: Props
public readonly _events: Events
public readonly _slots: Slots
private tag: "div" | "span" = "div"
public readonly isSvelte = true
constructor(svelteElement, props?: Props, events?: Events, slots?: Slots) {
super()
@ -47,4 +48,18 @@ export default class SvelteUIElement<
})
return el
}
public getClass(){
if(this.clss.size === 0){
return undefined
}
return this.clss
}
public getStyle(){
if(this.style === ""){
return undefined
}
return this.style
}
}

View file

@ -98,7 +98,7 @@ export default class TableOfContents {
const intro = md.substring(0, firstTitleIndex)
const splitPoint = intro.lastIndexOf("\n")
return md.substring(0, splitPoint) +"\n" toc + md.substring(splitPoint)
return md.substring(0, splitPoint) +"\n" + toc + md.substring(splitPoint)
}
public static generateStructure(

View file

@ -1,13 +1,24 @@
<script lang="ts">
import BaseUIElement from "../BaseUIElement.js"
import { onDestroy, onMount } from "svelte"
import SvelteUIElement from "./SvelteUIElement"
export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement
let html: HTMLElement
let isSvelte = false
let uiElement : BaseUIElement | SvelteUIElement | undefined
let svelteElem: SvelteUIElement
onMount(() => {
const uiElem = typeof construct === "function" ? construct() : construct
html = uiElem?.ConstructElement()
uiElement = typeof construct === "function" ? construct() : construct
if (uiElement?.["isSvelte"]) {
isSvelte = true
svelteElem = <SvelteUIElement> uiElement
return
}
html = uiElement?.ConstructElement()
if (html !== undefined) {
elem?.replaceWith(html)
@ -16,7 +27,12 @@
onDestroy(() => {
html?.remove()
uiElement?.Destroy()
})
</script>
<span bind:this={elem} />
{#if isSvelte}
<svelte:component this={svelteElem?._svelteComponent} {...svelteElem._props} class={svelteElem.getClass()} style={svelteElem.getStyle()}/>
{:else}
<span bind:this={elem} />
{/if}

View file

@ -63,7 +63,7 @@ export default abstract class BaseUIElement {
/**
* Adds all the relevant classes, space separated
*/
public SetClass(clss: string) {
public SetClass(clss: string): this {
if (clss == undefined) {
return this
}
@ -101,7 +101,7 @@ export default abstract class BaseUIElement {
return this.clss.has(clss)
}
public SetStyle(style: string): BaseUIElement {
public SetStyle(style: string): this {
this.style = style
if (this._constructedHtmlElement !== undefined) {
this._constructedHtmlElement.style.cssText = style

View file

@ -17,7 +17,7 @@
const t = Translations.t.general.wikipedia
export let searchValue = new UIEventSource("Tom boonen")
export let searchValue = new UIEventSource(undefined)
export let placeholder = t.searchWikidata
export let allowMultiple = false
@ -37,7 +37,6 @@
}
$:{
console.log(selectedMany)
const v = []
for (const id in selectedMany) {
if (selectedMany[id]) {
@ -72,7 +71,6 @@
let selectedWithoutSearch: Store<WikidataResponse[]> = searchResult.map(sr => {
for (const wikidataItem of sr?.success ?? []) {
console.log("Saving", wikidataItem.id)
previouslySeen.set(wikidataItem.id, wikidataItem)
}
let knownIds: Set<string> = new Set(sr?.success?.map(item => item.id))

View file

@ -26,7 +26,6 @@ let searchFor: string =
.find((foundValue) => !!foundValue) ?? ""
const options: any = args[1]
console.log(">>>", args)
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)

View file

@ -22,11 +22,16 @@
export let mapProperties: MapProperties = undefined
export let interactive: boolean = true
/**
* If many maps are shown (> ~15), Chromium will drop some because of "too Much WEBGL-contexts" (see issue #2024 or https://webglfundamentals.org/webgl/lessons/webgl-multiple-views.html)
* For important maps (e.g. the main map), we want to recover from this
*/
export let autorecovery: boolean = false
let container: HTMLElement
let _map: Map
onMount(() => {
function initMap() {
const { lon, lat } = mapProperties?.location?.data ?? { lon: 0, lat: 0 }
const rasterLayer: RasterLayerProperties = mapProperties?.rasterLayer?.data?.properties
@ -50,15 +55,31 @@
center: { lng: lon, lat },
maxZoom: 24,
interactive: true,
attributionControl: false,
attributionControl: false
}
_map = new maplibre.Map(options)
window.requestAnimationFrame(() => {
_map.resize()
})
_map.on("load", function () {
_map.on("load", function() {
_map.resize()
const canvas = _map.getCanvas()
canvas.addEventListener("webglcontextlost", (e) => {
console.warn("A MapLibreMap lost their context. Recovery is", autorecovery, e)
try{
_map?.remove()
}catch (e) {
console.debug("Could not remove map due to", e)
}
if(autorecovery){
requestAnimationFrame(() => {
console.warn("Attempting map recovery")
_map = new maplibre.Map(options)
initMap()
})
}
})
if (interactive) {
ariaLabel(canvas, Translations.t.general.visualFeedback.navigation)
canvas.role = "application"
@ -69,15 +90,25 @@
}
})
map.set(_map)
})
}
onMount(() => initMap())
onDestroy(async () => {
await Utils.waitFor(250)
try {
if (_map) _map.remove()
map = null
} catch (e) {
console.error("Could not destroy map")
}
await Utils.waitFor(100)
requestAnimationFrame(
() => {
try {
_map?.remove()
console.log("Removed map")
map = null
} catch (e) {
console.error("Could not destroy map")
}
}
)
})
</script>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import ShowDataLayer from "../Map/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import MaplibreMap from "../Map/MaplibreMap.svelte"
export let state: SpecialVisualizationState
export let tagSource: UIEventSource<Record<string, string>>
export let args: string[]
export let feature: Feature
const keys = [...args]
keys.splice(0, 1)
let featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
(featuresById) => {
if (featuresById === undefined) {
return []
}
const properties = tagSource.data
const features: Feature[] = []
for (const key of keys) {
const value = properties[key]
if (value === undefined || value === null) {
continue
}
let idList = [value]
if (Array.isArray(value)) {
idList = value
} else if (
key !== "id" &&
typeof value === "string" &&
value?.startsWith("[")
) {
// This is a list of values
idList = JSON.parse(value)
}
for (const id of idList) {
const feature = featuresById.get(id)
if (feature === undefined) {
console.warn("No feature found for id ", id)
continue
}
features.push(feature)
}
}
return features
},
[tagSource]
)
let mlmap = new UIEventSource(undefined)
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
let mla = new MapLibreAdaptor(mlmap, {
rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource<number>(17),
maxzoom: new UIEventSource<number>(17)
})
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
mla.location.setData({ lon, lat })
if (args[0]) {
const parsed = Number(args[0])
if (!isNaN(parsed) && parsed > 0 && parsed < 25) {
mla.zoom.setData(parsed)
}
}
ShowDataLayer.showMultipleLayers(
mlmap,
new StaticFeatureSource(featuresToShow),
state.layout.layers,
{ zoomToFeatures: true }
)
</script>
<div class="h-40 rounded" style="overflow: hidden; pointer-events: none;">
<MaplibreMap interactive={false} map={mlmap} mapProperties={mla} />
</div>

View file

@ -1,115 +0,0 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import ShowDataLayer from "../Map/ShowDataLayer"
import { GeoOperations } from "../../Logic/GeoOperations"
import { BBox } from "../../Logic/BBox"
export class MinimapViz implements SpecialVisualization {
funcName = "minimap"
docs = "A small map showing the selected feature."
needsUrls = []
args = [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
name: "zoomlevel",
defaultValue: "18",
},
{
doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap. (Note: if the key is 'id', list interpration is disabled)",
name: "idKey",
defaultValue: "id",
},
]
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature
) {
if (state === undefined || feature === undefined) {
return undefined
}
const keys = [...args]
keys.splice(0, 1)
const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
(featuresById) => {
if (featuresById === undefined) {
return []
}
const properties = tagSource.data
const features: Feature[] = []
for (const key of keys) {
const value = properties[key]
if (value === undefined || value === null) {
continue
}
let idList = [value]
if (Array.isArray(value)) {
idList = value
} else if (
key !== "id" &&
typeof value === "string" &&
value?.startsWith("[")
) {
// This is a list of values
idList = JSON.parse(value)
}
for (const id of idList) {
const feature = featuresById.get(id)
if (feature === undefined) {
console.warn("No feature found for id ", id)
continue
}
features.push(feature)
}
}
return features
},
[tagSource]
)
const mlmap = new UIEventSource(undefined)
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
const mla = new MapLibreAdaptor(mlmap, {
rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource<number>(17),
maxzoom: new UIEventSource<number>(17),
})
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
mla.location.setData({ lon, lat })
if (args[0]) {
const parsed = Number(args[0])
if (!isNaN(parsed) && parsed > 0 && parsed < 25) {
mla.zoom.setData(parsed)
}
}
mlmap.addCallbackAndRun((map) => console.log("Map for minimap vis is now", map))
ShowDataLayer.showMultipleLayers(
mlmap,
new StaticFeatureSource(featuresToShow),
state.layout.layers,
{ zoomToFeatures: true }
)
return new SvelteUIElement(MaplibreMap, {
interactive: false,
map: mlmap,
mapProperties: mla,
})
.SetClass("h-40 rounded")
.SetStyle("overflow: hidden; pointer-events: none;")
}
}

View file

@ -8,7 +8,7 @@ import {
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz"
import MinimapViz from "./Popup/MinimapViz.svelte"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
@ -125,7 +125,7 @@ class NearbyImageVis implements SpecialVisualization {
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
): SvelteUIElement {
const isOpen = args[0] === "open"
const readonly = args[1] === "readonly"
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
@ -240,7 +240,7 @@ export class QuestionViz implements SpecialVisualization {
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
): SvelteUIElement {
const labels = args[0]
?.split(";")
?.map((s) => s.trim())
@ -346,26 +346,6 @@ export default class SpecialVisualizations {
return firstPart + "\n\n" + helpTexts.join("\n\n")
}
// noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial(
state: SpecialVisualizationState,
s: SpecialVisualization
): BaseUIElement {
const examples =
s.structuredExamples === undefined
? []
: s.structuredExamples().map((e) => {
return s.constr(
state,
new UIEventSource<Record<string, string>>(e.feature.properties),
e.args,
e.feature,
undefined
)
})
return new Combine([new Title(s.funcName), s.docs, ...examples])
}
private static initList(): SpecialVisualization[] {
const specialVisualizations: SpecialVisualization[] = [
new QuestionViz(),
@ -426,7 +406,34 @@ export default class SpecialVisualizations {
},
new HistogramViz(),
new StealViz(),
new MinimapViz(),
{
funcName : "minimap",
docs :"A small map showing the selected feature.",
needsUrls : [],
args : [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
name: "zoomlevel",
defaultValue: "18",
},
{
doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap. (Note: if the key is 'id', list interpration is disabled)",
name: "idKey",
defaultValue: "id",
},
],
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`",
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature
): SvelteUIElement {
return new SvelteUIElement(MinimapViz, {state, args, feature, tagSource})
}
},
{
funcName: "split_button",
docs: "Adds a button which allows to split a way",

View file

@ -97,11 +97,17 @@
currentValue.ping()
}
function genTitle(value: any, singular: string, i: number) {
if (schema.hints.title) {
return Function("value", "return " + schema.hints.title)(value)
function genTitle(value: any, singular: string, i: number): Translation {
try {
if (schema.hints.title) {
const v = Function("value", "return " + schema.hints.title)(value)
return Translations.T(v)
}
} catch (e) {
console.log("Warning: could not translate a title for " + `${singular} ${i} with function ` + schema.hints.title + " and value " + JSON.stringify(value))
}
return `${singular} ${i}`
return Translations.T(`${singular} ${i}`)
}
let genIconF: (x: any) => { icon: string; color: string } = <any>(
@ -166,7 +172,7 @@
{#if schema.hints.title}
<div class="subtle ml-2">
<Tr t={Translations.T(genTitle(value, singular, i))} />
<Tr t={genTitle(value, singular, i)} />
</div>
{/if}
</h3>
@ -182,7 +188,7 @@
{:else if typeof value === "string"}
Builtin: <b>{value}</b>
{:else}
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)}/>
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
{/if}
</span>
<div class="normal-background p-2">

View file

@ -213,7 +213,7 @@
<main>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
<MaplibreMap map={maplibremap} mapProperties={mapproperties} />
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true}/>
</div>
{#if $visualFeedback}
@ -286,9 +286,11 @@
on:keydown={forwardEventToMap}
htmlElem={openCurrentViewLayerButton}
>
<div class="w-8 h-8 cursor-pointer">
<ToSvelte
construct={() => currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")}
construct={() => currentViewLayer.defaultIcon()}
/>
</div>
</MapControlButton>
{/if}
<ExtraLinkButton {state} />
@ -297,6 +299,11 @@
<If condition={state.featureSwitchIsTesting}>
<div class="alert w-fit">Testmode</div>
</If>
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
<div class="thanks">
Testserver
</div>
{/if}
<If condition={state.featureSwitches.featureSwitchFakeUser}>
<div class="alert w-fit">Faking a user (Testmode)</div>
</If>