Add 'steal' as special rendering, update 'multi', add entrance overview to onwheels layer

This commit is contained in:
Pieter Vander Vennet 2022-07-29 20:04:36 +02:00
parent 181c5583d2
commit 7e32413113
11 changed files with 462 additions and 73 deletions

View file

@ -35,6 +35,7 @@ class EnclosingFunc implements ExtraFunction {
"The result is a list of features: `{feat: Polygon}[]`", "The result is a list of features: `{feat: Polygon}[]`",
"This function will never return the feature itself."].join("\n") "This function will never return the feature itself."].join("\n")
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
_f(params: ExtraFuncParams, feat: Feature<Geometry, any>) { _f(params: ExtraFuncParams, feat: Feature<Geometry, any>) {
return (...layerIds: string[]) => { return (...layerIds: string[]) => {
const result: { feat: any }[] = [] const result: { feat: any }[] = []
@ -89,6 +90,7 @@ class OverlapFunc implements ExtraFunction {
_f(params, feat) { _f(params, feat) {
return (...layerIds: string[]) => { return (...layerIds: string[]) => {
const result: { feat: any, overlap: number }[] = [] const result: { feat: any, overlap: number }[] = []
const seenIds = new Set<string>()
const bbox = BBox.get(feat) const bbox = BBox.get(feat)
for (const layerId of layerIds) { for (const layerId of layerIds) {
const otherFeaturess = params.getFeaturesWithin(layerId, bbox) const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
@ -99,12 +101,18 @@ class OverlapFunc implements ExtraFunction {
continue; continue;
} }
for (const otherFeatures of otherFeaturess) { for (const otherFeatures of otherFeaturess) {
result.push(...GeoOperations.calculateOverlap(feat, otherFeatures)); const overlap = GeoOperations.calculateOverlap(feat, otherFeatures)
for (const overlappingFeature of overlap) {
if(seenIds.has(overlappingFeature.feat.properties.id)){
continue
}
seenIds.add(overlappingFeature.feat.properties.id)
result.push(overlappingFeature)
}
} }
} }
result.sort((a, b) => b.overlap - a.overlap) result.sort((a, b) => b.overlap - a.overlap)
return result; return result;
} }
} }

View file

@ -35,6 +35,7 @@ export default class MetaTagging {
return; return;
} }
console.log("Recalculating metatags...")
const metatagsToApply: SimpleMetaTagger[] = [] const metatagsToApply: SimpleMetaTagger[] = []
for (const metatag of SimpleMetaTaggers.metatags) { for (const metatag of SimpleMetaTaggers.metatags) {
if (metatag.includesDates) { if (metatag.includesDates) {

View file

@ -93,7 +93,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
// Project the point onto the way // Project the point onto the way
console.log("Snapping a node onto an existing way...")
const geojson = this._snapOnto.asGeoJson() const geojson = this._snapOnto.asGeoJson()
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat]) const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
const projectedCoor= <[number, number]>projected.geometry.coordinates const projectedCoor= <[number, number]>projected.geometry.coordinates

View file

@ -219,6 +219,9 @@ export abstract class OsmObject {
/** /**
* Uses the list of polygon features to determine if the given tags are a polygon or not. * Uses the list of polygon features to determine if the given tags are a polygon or not.
*
* OsmObject.isPolygon({"building":"yes"}) // => true
* OsmObject.isPolygon({"highway":"residential"}) // => false
* */ * */
protected static isPolygon(tags: any): boolean { protected static isPolygon(tags: any): boolean {
for (const tagsKey in tags) { for (const tagsKey in tags) {

View file

@ -378,6 +378,25 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const errors = [] * const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined * RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"] * errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"]
*
*
* // an actual test
* const special = {"special": {
* "type": "multi",
* "before": {
* "en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:"
* },
* "after": {
* "en": "{_entrances_count_without_width_count} entrances don't have width information yet"
* },
* "key": "_entrance_properties_with_width",
* "tagrendering": {
* "en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
* }
* }}
* const errors = []
* RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances: {multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}An <a href='#{id}'>entrance</a> of {canonical(width)}"}
* errors // => []
*/ */
private static convertIfNeeded(input: (object & { special: { type: string } }) | any, errors: string[], context: string): any { private static convertIfNeeded(input: (object & { special: { type: string } }) | any, errors: string[], context: string): any {
const special = input["special"] const special = input["special"]
@ -385,10 +404,6 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
return input return input
} }
for (const wrongKey of Object.keys(input).filter(k => k !== "special" && k !== "before" && k !== "after")) {
errors.push(`At ${context}: Unexpected key in a special block: ${wrongKey}`)
}
const type = special["type"] const type = special["type"]
if (type === undefined) { if (type === undefined) {
errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used") errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used")
@ -406,10 +421,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
// Check for obsolete and misspelled arguments // Check for obsolete and misspelled arguments
errors.push(...Object.keys(special) errors.push(...Object.keys(special)
.filter(k => !argNames.has(k)) .filter(k => !argNames.has(k))
.filter(k => k !== "type") .filter(k => k !== "type" && k !== "before" && k !== "after")
.map(wrongArg => { .map(wrongArg => {
const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x) const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x)
return `Unexpected argument with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${argNamesList.join(", ")}`; return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${argNamesList.join(", ")}`;
})) }))
// Check that all obligated arguments are present. They are obligated if they don't have a preset value // Check that all obligated arguments are present. They are obligated if they don't have a preset value
@ -496,12 +511,21 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]} * const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]}
* result // => expected * result // => expected
* *
* // Should put text before if specified
* const tr = { * const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} }, * render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} },
* } * }
* const result = new RewriteSpecial().convert(tr,"test").result * const result = new RewriteSpecial().convert(tr,"test").result
* const expected = {render: {'en': "Some introduction{image_carousel(image)}"}} * const expected = {render: {'en': "Some introduction{image_carousel(image)}"}}
* result // => expected * result // => expected
*
* // Should put text after if specified
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, after: {en: "Some footer"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
* result // => expected
*/ */
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const errors = [] const errors = []

View file

@ -42,7 +42,6 @@ import NoteCommentElement from "./Popup/NoteCommentElement";
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"; import ImgurUploader from "../Logic/ImageProviders/ImgurUploader";
import FileSelectorButton from "./Input/FileSelectorButton"; import FileSelectorButton from "./Input/FileSelectorButton";
import {LoginToggle} from "./Popup/LoginButton"; import {LoginToggle} from "./Popup/LoginButton";
import {start} from "repl";
import {SubstitutedTranslation} from "./SubstitutedTranslation"; import {SubstitutedTranslation} from "./SubstitutedTranslation";
import {TextField} from "./Input/TextField"; import {TextField} from "./Input/TextField";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
@ -60,7 +59,8 @@ import Slider from "./Input/Slider";
import List from "./Base/List"; import List from "./Base/List";
import StatisticsPanel from "./BigComponents/StatisticsPanel"; import StatisticsPanel from "./BigComponents/StatisticsPanel";
import {OsmFeature} from "../Models/OsmFeature"; import {OsmFeature} from "../Models/OsmFeature";
import Link from "./Base/Link"; import EditableTagRendering from "./Popup/EditableTagRendering";
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -334,6 +334,13 @@ export default class SpecialVisualizations {
render: { render: {
special: { special: {
type: "some_special_visualisation", type: "some_special_visualisation",
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)"
},
after: {
en: "Some text to put after the element, e.g. a footer"
},
"argname": "some_arg", "argname": "some_arg",
"message": { "message": {
en: "some other really long message", en: "some other really long message",
@ -1218,18 +1225,79 @@ export default class SpecialVisualizations {
} }
}, null, " ") + "```", }, null, " ") + "```",
args: [ args: [
{name: "key", {
doc: "The property to read and to interpret as a list of properties"}, name: "key",
doc: "The property to read and to interpret as a list of properties",
required: true
},
{ {
name: "tagrendering", name: "tagrendering",
doc: "An entire tagRenderingConfig" doc: "An entire tagRenderingConfig",
required: true
} }
] ]
, ,
constr(state, featureTags, args) { constr(state, featureTags, args) {
const [key, tr] = args const [key, tr] = args
console.log("MULTI: ", key, tr) const translation = new Translation({"*": tr})
return undefined return new VariableUiElement(featureTags.map(tags => {
const properties: object[] = JSON.parse(tags[key])
const elements = []
for (const property of properties) {
const subsTr = new SubstitutedTranslation(translation, new UIEventSource<any>(property), state)
elements.push(subsTr)
}
return new List(elements)
}))
}
},
{
funcName: "steal",
docs: "Shows a tagRendering from a different object as if this was the object itself",
args: [{
name: "featureId",
doc: "The key of the attribute which contains the id of the feature from which to use the tags",
required: true
},
{
name: "tagRenderingId",
doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection",
required: true
}],
constr(state, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) {
const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".")
const layer = state.layoutToUse.layers.find(l => l.id === layerId)
const tagRendering = layer.tagRenderings.find(tr => tr.id === tagRenderingId)
tagRenderings.push([layer, tagRendering])
}
return new VariableUiElement(featureTags.map(tags => {
const featureId = tags[featureIdKey]
if (featureId === undefined) {
return undefined;
}
const otherTags = state.allElements.getEventSourceById(featureId)
const elements: BaseUIElement[] = []
for (const [layer, tagRendering] of tagRenderings) {
const el = new EditableTagRendering(otherTags, tagRendering, layer.units, state, {})
elements.push(el)
}
if (elements.length === 1) {
return elements[0]
}
return new Combine(elements).SetClass("flex flex-col");
}))
},
getLayerDependencies(args): string[] {
const [_, tagRenderingId] = args
if (tagRenderingId.indexOf(".") < 0) {
throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot"
}
const [layerId, __] = tagRenderingId.split(".")
return [layerId]
} }
} }
] ]

View file

@ -47,33 +47,84 @@
} }
], ],
"calculatedTags": [ "calculatedTags": [
"_entrance_properties=feat.overlapWith('entrance')?.map(e => e.feat.properties).filter(p => p !== undefined).filter(p => p.width !== undefined)", "_entrance_properties=feat.overlapWith('entrance')?.map(e => e.feat.properties)?.filter(p => p !== undefined && p.indoor !== 'door')",
"_entrance:id=feat.get('_entrance_properties')?.map(e => e.id)?.at(0)", "_entrance_properties_with_width=feat.get('_entrance_properties')?.filter(p => p['width'] !== undefined)",
"_entrance:width=feat.get('_entrance_properties')?.map(e => e.width)?.at(0)" "_entrances_count=feat.get('_entrance_properties').length",
"_entrances_count_without_width_count= feat.get('_entrances_count') - feat.get('_entrance_properties_with_width').length",
"_biggest_width= Math.max( feat.get('_entrance_properties').map(p => p.width))",
"_biggest_width_properties= /* Can be a list! */ feat.get('_entrance_properties').filter(p => p.width === feat.get('_biggest_width'))",
"_biggest_width_id=feat.get('_biggest_width_properties').id"
],
"units": [
{
"appliesToKey": [
"width","_biggest_width"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"fr": "mètre",
"de": "Meter"
}
},
{
"default": true,
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"fr": "centimètre",
"de": "Zentimeter"
}
}
]
}
], ],
"tagRenderings": [ "tagRenderings": [
{ {
"id": "_entrance:width", "id": "entrance_info",
"render": { "render": {
"en": "<a href ='#{_entrance:id} '>This door has a width of {canonical(_entrance:width)} meters </a>", "before": {
"nl": "<a href ='#{_entrance:id} '>Deze deur heeft een breedte van {canonical(_entrance:width)} meter </a>", "en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:"
"de": "<a href ='#{_entrance:id} '>Diese Tür hat eine Durchgangsbreite von {canonical(_entrance:width)} Meter </a>",
"es": "<a href ='#{_entrance:id} '>Esta puerta tiene una ancho de {canonical(_entrance:width)} metros </a>",
"fr": "<a href ='#{_entrance:id} '>Cette porte a une largeur de {canonical(_entrance:width)} mètres </a>"
}, },
"freeform": { "after": {
"key": "_entrance:width" "en": "{_entrances_count_without_width_count} entrances don't have width information yet"
},
"special": {
"type": "multi",
"key": "_entrance_properties_with_width",
"tagrendering": {
"en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
}
}
}, },
"mappings": [ "mappings": [
{ {
"if": "_entrance:width=", "if": "_entrances_count=0",
"then": { "then": {
"en": "This entrance has no width information", "en": "No entrance has been marked"
"de": "Der Eingang hat keine Informationen zur Durchgangsbreite", }
"fr": "Cette entrée n'a pas d'informations sur sa largeur" },
{
"if": "_entrances_count_without_width:=_entrances_count",
"then": {
"en": "None of the {_entrance_count} entrances have width information yet"
} }
} }
] ]
},
{
"id": "biggest_width",
"render": "The <a href='#{_biggest_width_id}'>entrance with the biggest width</a> is {canonical(_biggest_width)} wide",
"condition": "_biggest_width_id~*"
} }
] ]
} }

View file

@ -17,6 +17,12 @@
"startZoom": 14, "startZoom": 14,
"widenFactor": 2, "widenFactor": 2,
"layers": [ "layers": [
"indoors" "indoors",
{
"builtin": ["walls_and_buildings"],
"override": {
"shownByDefault": true
}
}
] ]
} }

View file

@ -366,15 +366,11 @@
], ],
"overrideAll": { "overrideAll": {
"+calculatedTags": [ "+calculatedTags": [
"_poi_walls_and_buildings_entrance_properties=[].concat(...feat.closestn('walls_and_buildings',1, undefined, 500).map(w => ({id: w.feat.properties.id, width: w.feat.properties['_entrance_properties']})))", "_enclosing_building=feat.enclosingFeatures('walls_and_buildings')?.map(f => f.feat.properties.id)?.at(0)"
"_poi_walls_and_buildings_entrance_count=[].concat(...feat.overlapWith('walls_and_buildings').map(w => ({id: w.feat.properties.id, width: w.feat.properties['_entrance_properties']})))",
"_poi_walls_and_buildings_entrance_properties_with_width=feat.get('_poi_walls_and_buildings_entrance_properties').filter(p => p['width'] !== undefined)",
"_poi_entrance:id=JSON.parse(feat.properties._poi_walls_and_buildings_entrance_properteis)?.id",
"_poi_entrance:width=JSON.parse(feat.properties._poi_walls_and_buildings_entrance_properties)?.width"
], ],
"+tagRenderings": [ "tagRenderings+": [
{ {
"id": "_containing_poi_entrance:width", "id": "_stolen_entrances",
"condition": { "condition": {
"and": [ "and": [
"entrance=", "entrance=",
@ -383,21 +379,11 @@
"door=" "door="
] ]
}, },
"mappings": [{
"if": "_poi_walls_and_buildings_entrance_properties_with_width=[]",
"then": {
"en": "The containing building has {}"
}
}],
"render": { "render": {
"special": { "special": {
"type": "multi", "type": "steal",
"key": "_poi_walls_and_buildings_entrance_properties", "featureId": "_enclosing_building",
"tagrendering": { "tagRenderingId": "walls_and_buildings.entrance_info; walls_and_buildings.biggest_width"
"en": "The containing building can be entered via <a href='#{_poi_entrance:id}'>a door of {canonical(_poi_entrance:width)}</a>",
"fr": "On peut entrer dans ce batiment via <a href='#{_poi_entrance:id}'>une porte de {canonical(_poi_entrance:width)}</a>",
"de": "Das Gebäude kann über <a href='#{_poi_entrance:id}'>durch eine Tür von {canonical(_poi_entrance:width)} betreten werden.</a>"
}
} }
} }
} }

View file

@ -1,6 +1,5 @@
import {describe} from 'mocha' import {describe} from 'mocha'
import {expect} from 'chai' import {expect} from 'chai'
import {Utils} from "../Utils";
describe("TestSuite", () => { describe("TestSuite", () => {

View file

@ -0,0 +1,243 @@
import {describe} from 'mocha'
import {expect} from 'chai'
import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions";
import {OsmFeature} from "../../Models/OsmFeature";
describe("OverlapFunc", () => {
it("should give doors on the edge", () => {
const door: OsmFeature = {
"type": "Feature",
"id": "node/9909268725",
"properties": {
"automatic_door": "no",
"door": "hinged",
"indoor": "door",
"kerb:height": "0 cm",
"width": "1",
"id": "node/9909268725",
},
"geometry": {
"type": "Point",
"coordinates": [
4.3494436,
50.8657928
]
},
}
const hermanTeirlinck = {
"type": "Feature",
"id": "way/444059131",
"properties": {
"timestamp": "2022-07-27T15:15:01Z",
"version": 27,
"changeset": 124146283,
"user": "Pieter Vander Vennet",
"uid": 3818858,
"addr:city": "Bruxelles - Brussel",
"addr:housenumber": "88",
"addr:postcode": "1000",
"addr:street": "Avenue du Port - Havenlaan",
"building": "government",
"building:levels": "5",
"name": "Herman Teirlinckgebouw",
"operator": "Vlaamse overheid",
"wikidata": "Q47457146",
"wikipedia": "nl:Herman Teirlinckgebouw",
"id": "way/444059131",
"_backend": "https://www.openstreetmap.org",
"_lat": "50.86622355",
"_lon": "4.3501212",
"_layer": "walls_and_buildings",
"_length": "380.5933566256343",
"_length:km": "0.4",
"_now:date": "2022-07-29",
"_now:datetime": "2022-07-29 14:19:25",
"_loaded:date": "2022-07-29",
"_loaded:datetime": "2022-07-29 14:19:25",
"_last_edit:contributor": "Pieter Vander Vennet",
"_last_edit:contributor:uid": 3818858,
"_last_edit:changeset": 124146283,
"_last_edit:timestamp": "2022-07-27T15:15:01Z",
"_version_number": 27,
"_geometry:type": "Polygon",
"_surface": "7461.252251355437",
"_surface:ha": "0.7",
"_country": "be"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
4.3493369,
50.8658274
],
[
4.3493393,
50.8658266
],
[
4.3494436,
50.8657928
],
[
4.3495272,
50.8657658
],
[
4.349623,
50.8657348
],
[
4.3497442,
50.8656956
],
[
4.3498441,
50.8656632
],
[
4.3500768,
50.8655878
],
[
4.3501619,
50.8656934
],
[
4.3502113,
50.8657551
],
[
4.3502729,
50.8658321
],
[
4.3503063,
50.8658737
],
[
4.3503397,
50.8659153
],
[
4.3504159,
50.8660101
],
[
4.3504177,
50.8660123
],
[
4.3504354,
50.8660345
],
[
4.3505348,
50.8661584
],
[
4.3504935,
50.866172
],
[
4.3506286,
50.8663405
],
[
4.3506701,
50.8663271
],
[
4.3508563,
50.8665592
],
[
4.3509055,
50.8666206
],
[
4.3506278,
50.8667104
],
[
4.3504502,
50.8667675
],
[
4.3503132,
50.8668115
],
[
4.3502162,
50.8668427
],
[
4.3501645,
50.8668593
],
[
4.3499296,
50.8665664
],
[
4.3498821,
50.8665073
],
[
4.3498383,
50.8664527
],
[
4.3498126,
50.8664207
],
[
4.3497459,
50.8663376
],
[
4.3497227,
50.8663086
],
[
4.3496517,
50.8662201
],
[
4.3495158,
50.8660507
],
[
4.3493369,
50.8658274
]
]
]
},
"bbox": {
"maxLat": 50.8668593,
"maxLon": 4.3509055,
"minLat": 50.8655878,
"minLon": 4.3493369
}
}
const params: ExtraFuncParams = {
getFeatureById: id => undefined,
getFeaturesWithin: () => [[door]],
memberships: undefined
}
ExtraFunctions.FullPatchFeature(params, hermanTeirlinck)
const overlap = (<any>hermanTeirlinck).overlapWith("*")
console.log(JSON.stringify(overlap))
expect(overlap[0].feat == door).true
})
})