Themes: add proper 'image along way' possibility

This commit is contained in:
Pieter Vander Vennet 2024-02-12 12:39:35 +01:00
parent 29f8e08509
commit 584fb3cb57
17 changed files with 259 additions and 70 deletions

View file

@ -134,6 +134,27 @@
}
},
"opening_hours",
{
"id": "oneway",
"question": {
"en": "In what direction can this aerialway be taken?"
},
"mappings": [
{
"if": "oneway=yes",
"alsoShowIf": "oneway=",
"then": {
"en": "This aerialway can only be taken to the top"
}
},
{
"if": "oneway=no",
"then": {
"en": "This aerialway can be taken in both directions"
}
}
]
},
{
"id": "length",
"render": {
@ -144,7 +165,20 @@
"lineRendering": [
{
"width": "4",
"color": "black"
"color": "black",
"imageAlongWay": [ {
"if": "oneway=no",
"then": "./assets/png/twoway.png"
},{
"if": {
"or": [
"oneway=yes",
"oneway="
]
},
"then": "./assets/png/oneway.png"
}
]
}
],
"id": "aerialway",

View file

@ -96,7 +96,8 @@
"then": "gray"
}
]
}
},
"imageAlongWay": "./assets/png/oneway.png"
}
],
"id": "ski_piste",

View file

@ -14,5 +14,21 @@
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "twoway.png",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "twoway.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
}
]

BIN
assets/png/twoway.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0

58
assets/png/twoway.svg Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="400"
height="400"
viewBox="0 0 400 400"
version="1.1"
id="svg1"
sodipodi:docname="twoway.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
inkscape:export-filename="twoway.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#efe1c6"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="2.1376401"
inkscape:cx="153.67414"
inkscape:cy="183.14589"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1"
showgrid="false"
showguides="true">
<sodipodi:guide
position="-44.441532,199.7792"
orientation="0,-1"
id="guide1"
inkscape:locked="false" />
</sodipodi:namedview>
<g
id="surface1"
transform="matrix(0,0.46822921,-0.46822921,0,397.53821,111.31934)"
style="stroke:none">
<path
id="path1"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:bevel;stroke-opacity:1"
d="M 0.35313437,468.46201 187.80431,842.77256 375.40782,467.93858 230.57188,530.22569 V 312.546 L 375.40782,374.83506 187.80431,-8.76875e-4 0.35313437,374.30967 c 1e-8,0 42.56450563,-17.52151 93.90429663,-39.77539 L 144.63048,312.69444 V 530.07725 L 94.257431,508.2374 C 42.917692,485.98356 0.35313438,468.46201 0.35313437,468.46201 Z" />
</g>
<g
id="g2"
transform="matrix(0,1,1,0,4.4769698,8.5883084)"
style="stroke:none" />
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0

View file

@ -7,7 +7,7 @@
"en": "Everything you need to go skiing"
},
"icon": "./assets/layers/aerialway/chair_lift.svg",
"enableTerrain": true,
"enableTerrain": false,
"layers": [
"ski_piste",
"aerialway",

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.37.3",
"version": "0.37.4",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",

View file

@ -4,6 +4,7 @@ import { TagUtils } from "./TagUtils"
import { Tag } from "./Tag"
import { RegexTag } from "./RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class And extends TagsFilter {
public and: TagsFilter[]
@ -429,4 +430,8 @@ export class And extends TagsFilter {
f(this)
this.and.forEach((sub) => sub.visit(f))
}
asMapboxExpression(): ExpressionSpecification {
return ["all", ...this.and.map(t => t.asMapboxExpression())]
}
}

View file

@ -2,6 +2,7 @@ import { TagsFilter } from "./TagsFilter"
import { TagUtils } from "./TagUtils"
import { And } from "./And"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class Or extends TagsFilter {
public or: TagsFilter[]
@ -288,4 +289,8 @@ export class Or extends TagsFilter {
f(this)
this.or.forEach((t) => t.visit(f))
}
asMapboxExpression(): ExpressionSpecification {
return ["any", ...this.or.map(t => t.asMapboxExpression())]
}
}

View file

@ -1,6 +1,7 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class RegexTag extends TagsFilter {
public readonly key: RegExp | string
@ -357,4 +358,11 @@ export class RegexTag extends TagsFilter {
visit(f: (TagsFilter) => void) {
f(this)
}
asMapboxExpression(): ExpressionSpecification {
if(typeof this.key=== "string" && typeof this.value === "string" ) {
return [this.invert ? "!=" : "==", ["get",this.key], this.value]
}
throw "TODO"
}
}

View file

@ -1,10 +1,12 @@
import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export class Tag extends TagsFilter {
public key: string
public value: string
constructor(key: string, value: string) {
super()
this.key = key
@ -63,7 +65,7 @@ export class Tag extends TagsFilter {
asOverpass(): string[] {
if (this.value === "") {
// NOT having this key
return ['[!"' + this.key + '"]']
return ["[!\"" + this.key + "\"]"]
}
return [`["${this.key}"="${this.value}"]`]
}
@ -81,7 +83,7 @@ export class Tag extends TagsFilter {
asHumanString(
linkToWiki?: boolean,
shorten?: boolean,
currentProperties?: Record<string, string>
currentProperties?: Record<string, string>,
) {
let v = this.value
if (typeof v !== "string") {
@ -165,4 +167,16 @@ export class Tag extends TagsFilter {
visit(f: (tagsFilter: TagsFilter) => void) {
f(this)
}
asMapboxExpression(): ExpressionSpecification {
if (this.value === "") {
return [
"any",
["!", ["has", this.key]],
["==", ["get", this.key], ""],
]
}
return ["==", ["get", this.key], this.value]
}
}

View file

@ -1,4 +1,5 @@
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
export abstract class TagsFilter {
abstract asOverpass(): string[]
@ -63,4 +64,6 @@ export abstract class TagsFilter {
* Walks the entire tree, every tagsFilter will be passed into the function once
*/
abstract visit(f: (tagsFilter: TagsFilter) => void)
abstract asMapboxExpression(): ExpressionSpecification
}

View file

@ -1,4 +1,7 @@
import { MinimalTagRenderingConfigJson } from "./TagRenderingConfigJson"
import { MappingConfigJson } from "./QuestionableTagRenderingConfigJson"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { TagConfigJson } from "./TagConfigJson"
/**
* The LineRenderingConfig gives all details onto how to render a single line of a feature.
@ -74,4 +77,12 @@ export default interface LineRenderingConfigJson {
* type: int
*/
offset?: number | MinimalTagRenderingConfigJson
/**
* question: What PNG-image should be shown along the way?
*
* ifunset: no image is shown along the way
* suggestions: [{if: "./assets/png/oneway.png", then: "Show a oneway error"}]
* type: image
*/
imageAlongWay?: {if: TagConfigJson, then: string}[] | string
}

View file

@ -1,7 +1,8 @@
import WithContextLoader from "./WithContextLoader"
import TagRenderingConfig from "./TagRenderingConfig"
import { Utils } from "../../Utils"
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
export default class LineRenderingConfig extends WithContextLoader {
public readonly color: TagRenderingConfig
@ -12,6 +13,7 @@ export default class LineRenderingConfig extends WithContextLoader {
public readonly fill: TagRenderingConfig
public readonly fillColor: TagRenderingConfig
public readonly leftRightSensitive: boolean
public readonly imageAlongWay: { if?: TagsFilter, then: string }[]
constructor(json: LineRenderingConfigJson, context: string) {
super(json, context)
@ -21,6 +23,28 @@ export default class LineRenderingConfig extends WithContextLoader {
this.lineCap = this.tr("lineCap", "round")
this.fill = this.tr("fill", undefined)
this.fillColor = this.tr("fillColor", undefined)
this.imageAlongWay = []
if (json.imageAlongWay) {
if (typeof json.imageAlongWay === "string") {
this.imageAlongWay.push({
then: json.imageAlongWay,
})
} else {
for (let i = 0; i < json.imageAlongWay.length; i++) {
const imgAlong = json.imageAlongWay[i]
const ctx = context + ".imageAlongWay[" + i + "]"
if(!imgAlong.then.endsWith(".png")){
throw "An imageAlongWay should always be a PNG image"
}
this.imageAlongWay.push(
{
if: TagUtils.Tag(imgAlong.if, ctx),
then: imgAlong.then,
},
)
}
}
}
if (typeof json.offset === "string") {
json.offset = parseFloat(json.offset)

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,
)
}
}