diff --git a/assets/layers/aerialway/aerialway.json b/assets/layers/aerialway/aerialway.json
index bc2427975..0b5944c70 100644
--- a/assets/layers/aerialway/aerialway.json
+++ b/assets/layers/aerialway/aerialway.json
@@ -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",
diff --git a/assets/layers/ski_piste/ski_piste.json b/assets/layers/ski_piste/ski_piste.json
index 1cd365916..b2063eab1 100644
--- a/assets/layers/ski_piste/ski_piste.json
+++ b/assets/layers/ski_piste/ski_piste.json
@@ -96,7 +96,8 @@
"then": "gray"
}
]
- }
+ },
+ "imageAlongWay": "./assets/png/oneway.png"
}
],
"id": "ski_piste",
diff --git a/assets/png/license_info.json b/assets/png/license_info.json
index d5571eb0c..548e52793 100644
--- a/assets/png/license_info.json
+++ b/assets/png/license_info.json
@@ -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": []
}
]
\ No newline at end of file
diff --git a/assets/png/twoway.png b/assets/png/twoway.png
new file mode 100644
index 000000000..e57d53302
Binary files /dev/null and b/assets/png/twoway.png differ
diff --git a/assets/png/twoway.png.license b/assets/png/twoway.png.license
new file mode 100644
index 000000000..2452bee1e
--- /dev/null
+++ b/assets/png/twoway.png.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: Pieter Vander Vennet
+SPDX-License-Identifier: CC0
\ No newline at end of file
diff --git a/assets/png/twoway.svg b/assets/png/twoway.svg
new file mode 100644
index 000000000..7a28a60e2
--- /dev/null
+++ b/assets/png/twoway.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/assets/png/twoway.svg.license b/assets/png/twoway.svg.license
new file mode 100644
index 000000000..2452bee1e
--- /dev/null
+++ b/assets/png/twoway.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: Pieter Vander Vennet
+SPDX-License-Identifier: CC0
\ No newline at end of file
diff --git a/assets/themes/ski/ski.json b/assets/themes/ski/ski.json
index 1209804a7..2211febf9 100644
--- a/assets/themes/ski/ski.json
+++ b/assets/themes/ski/ski.json
@@ -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",
diff --git a/package.json b/package.json
index 7b8e6915c..3eaa0a661 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/Logic/Tags/And.ts b/src/Logic/Tags/And.ts
index dff0919f8..6654ac99e 100644
--- a/src/Logic/Tags/And.ts
+++ b/src/Logic/Tags/And.ts
@@ -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())]
+ }
}
diff --git a/src/Logic/Tags/Or.ts b/src/Logic/Tags/Or.ts
index a0c0f6622..2bda9681c 100644
--- a/src/Logic/Tags/Or.ts
+++ b/src/Logic/Tags/Or.ts
@@ -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())]
+ }
}
diff --git a/src/Logic/Tags/RegexTag.ts b/src/Logic/Tags/RegexTag.ts
index f024f8251..4f3c82f9b 100644
--- a/src/Logic/Tags/RegexTag.ts
+++ b/src/Logic/Tags/RegexTag.ts
@@ -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"
+ }
}
diff --git a/src/Logic/Tags/Tag.ts b/src/Logic/Tags/Tag.ts
index b532b7053..cd8146ccd 100644
--- a/src/Logic/Tags/Tag.ts
+++ b/src/Logic/Tags/Tag.ts
@@ -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
+ currentProperties?: Record,
) {
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]
+ }
}
diff --git a/src/Logic/Tags/TagsFilter.ts b/src/Logic/Tags/TagsFilter.ts
index e925a76ef..c75b7f6f4 100644
--- a/src/Logic/Tags/TagsFilter.ts
+++ b/src/Logic/Tags/TagsFilter.ts
@@ -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
}
diff --git a/src/Models/ThemeConfig/Json/LineRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/LineRenderingConfigJson.ts
index ffc5d1ea8..e7bbc86dd 100644
--- a/src/Models/ThemeConfig/Json/LineRenderingConfigJson.ts
+++ b/src/Models/ThemeConfig/Json/LineRenderingConfigJson.ts
@@ -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
}
diff --git a/src/Models/ThemeConfig/LineRenderingConfig.ts b/src/Models/ThemeConfig/LineRenderingConfig.ts
index 57f01e8d1..bb52e2cff 100644
--- a/src/Models/ThemeConfig/LineRenderingConfig.ts
+++ b/src/Models/ThemeConfig/LineRenderingConfig.ts
@@ -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)
diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts
index e8824f417..3bae53507 100644
--- a/src/UI/Map/ShowDataLayer.ts
+++ b/src/UI/Map/ShowDataLayer.ts
@@ -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,
fetchStore?: (id: string) => Store>,
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(
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,
fetchStore?: (id: string) => Store>,
- 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((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((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
+ properties: Record,
): Partial> {
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,
features: FeatureSource,
layers: LayerConfig[],
- options?: Partial
+ options?: Partial,
) {
const perLayer: PerLayerFeatureSourceSplitter =
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,
features: FeatureSource,
- doShowLayer?: Store
+ doShowLayer?: Store,
): 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,
)
}
}