Merge branch 'develop' into feature/json-editor

This commit is contained in:
Robin van der Linde 2024-02-13 00:54:04 +01:00
commit 2c018d7af3
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
233 changed files with 10302 additions and 4571 deletions

View file

@ -1,5 +1,5 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Map as MlMap } from "maplibre-gl"
import type { AddLayerObject, Map as MlMap } from "maplibre-gl"
import { GeoJSONSource, Marker } from "maplibre-gl"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { GeoOperations } from "../../Logic/GeoOperations"
@ -15,6 +15,7 @@ import * as range_layer from "../../../assets/layers/range/range.json"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
@ -36,7 +37,7 @@ class PointRenderingLayer {
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void,
selectedElement?: Store<{ properties: { id?: string } }>
selectedElement?: Store<{ properties: { id?: string } }>,
) {
this._visibility = visibility
this._config = config
@ -89,7 +90,7 @@ class PointRenderingLayer {
" while rendering",
location,
"of",
this._config
this._config,
)
}
const id = feature.properties.id + "-" + location
@ -97,7 +98,7 @@ class PointRenderingLayer {
const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>feature,
location
location,
)
if (loc === undefined) {
continue
@ -153,7 +154,7 @@ class PointRenderingLayer {
if (this._onClick) {
const self = this
el.addEventListener("click", function (ev) {
el.addEventListener("click", function(ev) {
ev.preventDefault()
self._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click
@ -221,7 +222,7 @@ class LineRenderingLayer {
config: LineRenderingConfig,
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void
onClick?: (feature: Feature) => void,
) {
this._layername = layername
this._map = map
@ -235,53 +236,60 @@ class LineRenderingLayer {
map.on("styledata", () => self.update(features.features))
}
private async addSymbolLayer(sourceId: string, url: string = "./assets/png/oneway.png") {
const map = this._map
const imgId = url.replaceAll(/[/.-]/g, "_")
if (map.getImage(imgId) === undefined) {
await new Promise<void>((resolve, reject) => {
map.loadImage(url, (err, image) => {
if (err) {
console.error("Could not add symbol layer to line due to", err)
reject(err)
return
}
map.addImage(imgId, image)
resolve()
})
})
}
map.addLayer({
"id": "symbol-layer_" + this._layername + "-" + imgId,
'type': 'symbol',
'source': sourceId,
'layout': {
'symbol-placement': 'line',
'symbol-spacing': 10,
'icon-allow-overlap': true,
'icon-rotation-alignment':'map',
'icon-pitch-alignment':'map',
'icon-image': imgId,
'icon-size': 0.055,
'visibility': 'visible'
}
});
}
public destruct(): void {
this._map.removeLayer(this._layername + "_polygon")
}
private async addSymbolLayer(sourceId: string, imageAlongWay: { if?: TagsFilter, then: string }[]) {
const map = this._map
await Promise.allSettled(imageAlongWay.map(async (img, i) => {
const imgId = img.then.replaceAll(/[/.-]/g, "_")
if (map.getImage(imgId) === undefined) {
await new Promise<void>((resolve, reject) => {
map.loadImage(img.then, (err, image) => {
if (err) {
console.error("Could not add symbol layer to line due to", err)
return
}
map.addImage(imgId, image)
resolve()
})
})
}
const spec: AddLayerObject = {
"id": "symbol-layer_" + this._layername + "-" + i,
"type": "symbol",
"source": sourceId,
"layout": {
"symbol-placement": "line",
"symbol-spacing": 10,
"icon-allow-overlap": true,
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
"icon-image": imgId,
"icon-size": 0.055,
},
}
const filter = img.if?.asMapboxExpression()
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
if (filter) {
spec.filter = filter
}
map.addLayer(spec)
}))
}
/**
* Calculate the feature-state for maplibre
* @param properties
* @private
*/
private calculatePropsFor(
properties: Record<string, string>
properties: Record<string, string>,
): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> {
const config = this._config
@ -357,11 +365,8 @@ class LineRenderingLayer {
},
})
if(this._layername.startsWith("mapcomplete_ski_piste") || this._layername.startsWith("mapcomplete_aerialway")){
// TODO FIXME properly enable this so that more layers can use this if appropriate
this.addSymbolLayer(this._layername)
}else{
console.log("No oneway arrow for", this._layername)
if (this._config.imageAlongWay) {
this.addSymbolLayer(this._layername, this._config.imageAlongWay)
}
@ -372,7 +377,7 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id: feature.properties.id },
this.calculatePropsFor(feature.properties)
this.calculatePropsFor(feature.properties),
)
}
@ -415,7 +420,7 @@ class LineRenderingLayer {
"Error while setting visibility of layers ",
linelayer,
polylayer,
e
e,
)
}
})
@ -436,7 +441,7 @@ class LineRenderingLayer {
console.trace(
"Got a feature without ID; this causes rendering bugs:",
feature,
"from"
"from",
)
LineRenderingLayer.missingIdTriggered = true
}
@ -448,7 +453,7 @@ class LineRenderingLayer {
if (this._fetchStore === undefined) {
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties)
this.calculatePropsFor(feature.properties),
)
} else {
const tags = this._fetchStore(id)
@ -465,7 +470,7 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(properties)
this.calculatePropsFor(properties),
)
})
}
@ -489,7 +494,7 @@ export default class ShowDataLayer {
layer: LayerConfig
drawMarkers?: true | boolean
drawLines?: true | boolean
}
},
) {
this._options = options
const self = this
@ -500,7 +505,7 @@ export default class ShowDataLayer {
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions>
options?: Partial<ShowDataLayerOptions>,
) {
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
new PerLayerFeatureSourceSplitter(
@ -508,7 +513,7 @@ export default class ShowDataLayer {
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
}
},
)
perLayer.forEach((fs) => {
new ShowDataLayer(mlmap, {
@ -522,7 +527,7 @@ export default class ShowDataLayer {
public static showRange(
map: Store<MlMap>,
features: FeatureSource,
doShowLayer?: Store<boolean>
doShowLayer?: Store<boolean>,
): ShowDataLayer {
return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer,
@ -531,7 +536,8 @@ export default class ShowDataLayer {
})
}
public destruct() {}
public destruct() {
}
private zoomToCurrentFeatures(map: MlMap) {
if (this._options.zoomToFeatures) {
@ -552,9 +558,9 @@ export default class ShowDataLayer {
(this._options.layer.title === undefined
? undefined
: (feature: Feature) => {
selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer)
})
selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer)
})
if (this._options.drawLines !== false) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
const lineRenderingConfig = this._options.layer.lineRendering[i]
@ -565,7 +571,7 @@ export default class ShowDataLayer {
lineRenderingConfig,
doShowLayer,
fetchStore,
onClick
onClick,
)
this.onDestroy.push(l.destruct)
}
@ -580,7 +586,7 @@ export default class ShowDataLayer {
doShowLayer,
fetchStore,
onClick,
selectedElement
selectedElement,
)
}
}

View file

@ -75,6 +75,7 @@ export default class NoteCommentElement extends Combine {
const extension = link.substring(lastDotIndex + 1, link.length)
return Utils.imageExtensions.has(extension)
})
.filter(link => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map((i) =>

View file

@ -153,7 +153,7 @@
}
})
$: {
if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && mappings?.length === 0) {
if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && (mappings?.length ?? 0) === 0) {
selectedTags = new Tag(config.freeform.key, "")
} else {
@ -394,7 +394,7 @@
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
{#if allowDeleteOfFreeform && mappings?.length === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
<TrashIcon class="w-6 h-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue}/>

View file

@ -1,60 +1,72 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import StarsBar from "./StarsBar.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Checkbox from "../Base/Checkbox.svelte"
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import Loading from "../Base/Loading.svelte"
import { Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { placeholder } from "../../Utils/placeholder"
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import StarsBar from "./StarsBar.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Checkbox from "../Base/Checkbox.svelte"
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import Loading from "../Base/Loading.svelte"
import { Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { placeholder } from "../../Utils/placeholder"
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews
let score = 0
let confirmedScore = undefined
let isAffiliated = new UIEventSource(false)
let opinion = new UIEventSource<string>(undefined)
let score = 0
let confirmedScore = undefined
let isAffiliated = new UIEventSource(false)
let opinion = new UIEventSource<string>(undefined)
const t = Translations.t.reviews
const t = Translations.t.reviews
let _state: "ask" | "saving" | "done" = "ask"
let _state: "ask" | "saving" | "done" = "ask"
const connection = state.osmConnection
const connection = state.osmConnection
async function save() {
_state = "saving"
let nickname = undefined
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name
const hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
if (tooLong) {
return "too_long"
}
return undefined
})
async function save() {
if (hasError.data) {
return
}
_state = "saving"
let nickname = undefined
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data },
}
if (state.featureSwitchIsTesting?.data ?? true) {
console.log("Testing - not actually saving review", review)
await Utils.waitFor(1000)
} else {
await reviews.createReview(review)
}
_state = "done"
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data },
}
if (state.featureSwitchIsTesting?.data ?? true) {
console.log("Testing - not actually saving review", review)
await Utils.waitFor(1000)
} else {
await reviews.createReview(review)
}
_state = "done"
}
</script>
{#if _state === "done"}
@ -95,6 +107,12 @@
class="mb-1 w-full"
use:placeholder={t.reviewPlaceholder}
/>
{#if $hasError === "too_long"}
<div class="alert flex items-center px-2">
<ExclamationTriangle class="w-12 h-12"/>
<Tr t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}> </Tr>
</div>
{/if}
</label>
<Checkbox selected={isAffiliated}>
@ -108,7 +126,8 @@
<Tr t={t.reviewing_as.Subs({ nickname: state.osmConnection.userDetails.data.name })} />
<Tr slot="else" t={t.reviewing_as_anonymous} />
</If>
<button class="primary" on:click={save}>
<button class="primary" class:disabled={$hasError !== undefined}
on:click={save}>
<Tr t={t.save} />
</button>
</div>

View file

@ -833,8 +833,7 @@ export default class SpecialVisualizations {
return value
}
const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry)
return unit.asHumanLongValue(value, getCountry)
})
)
},
@ -1172,7 +1171,7 @@ export default class SpecialVisualizations {
},
{
name: "href",
doc: "The URL to link to",
doc: "The URL to link to. Note that this will be URI-encoded before ",
required: true,
},
{
@ -1181,7 +1180,7 @@ export default class SpecialVisualizations {
},
{
name: "download",
doc: "If set, this link will act as a download-button. The contents of `href` will be offered for download; this parameter will act as the proposed filename",
doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button.",
},
{
name: "arialabel",
@ -1204,7 +1203,7 @@ export default class SpecialVisualizations {
(tags) =>
new SvelteUIElement(Link, {
text: Utils.SubstituteKeys(text, tags),
href: Utils.SubstituteKeys(href, tags),
href: Utils.SubstituteKeys(href, tags).replaceAll(/ /g, '%20') /* Chromium based browsers eat the spaces */,
classnames,
download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),