forked from MapComplete/MapComplete
Merge branch 'develop' into feature/json-editor
This commit is contained in:
commit
2c018d7af3
233 changed files with 10302 additions and 4571 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue