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

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

View file

@ -13,9 +13,9 @@ export interface ExtraFuncParams {
* Note that more features then requested can be given back.
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
*/
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, {id: string}>[][],
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][],
memberships: RelationsTracker
getFeatureById: (id: string) => Feature<Geometry, {id: string}>
getFeatureById: (id: string) => Feature<Geometry, { id: string }>
}
/**
@ -31,10 +31,11 @@ interface ExtraFunction {
class EnclosingFunc implements ExtraFunction {
_name = "enclosingFeatures"
_doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)","",
_doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", "",
"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)"]
_f(params: ExtraFuncParams, feat: Feature<Geometry, any>) {
return (...layerIds: string[]) => {
const result: { feat: any }[] = []
@ -51,14 +52,14 @@ class EnclosingFunc implements ExtraFunction {
}
for (const otherFeatures of otherFeaturess) {
for (const otherFeature of otherFeatures) {
if(seenIds.has(otherFeature.properties.id)){
if (seenIds.has(otherFeature.properties.id)) {
continue
}
seenIds.add(otherFeature.properties.id)
if(otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon"){
if (otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon") {
continue;
}
if(GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>> otherFeature)){
if (GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>>otherFeature)) {
result.push({feat: otherFeature})
}
}
@ -75,10 +76,10 @@ class OverlapFunc implements ExtraFunction {
_name = "overlapWith";
_doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.",
"If the current feature is a point, all features that this point is embeded in are given." ,
"If the current feature is a point, all features that this point is embeded in are given.",
"",
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point." ,
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list." ,
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.",
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list.",
"",
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
"",
@ -89,6 +90,7 @@ class OverlapFunc implements ExtraFunction {
_f(params, feat) {
return (...layerIds: string[]) => {
const result: { feat: any, overlap: number }[] = []
const seenIds = new Set<string>()
const bbox = BBox.get(feat)
for (const layerId of layerIds) {
const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
@ -99,12 +101,18 @@ class OverlapFunc implements ExtraFunction {
continue;
}
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)
return result;
}
}

View file

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

View file

@ -93,7 +93,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
// Project the point onto the way
console.log("Snapping a node onto an existing way...")
const geojson = this._snapOnto.asGeoJson()
const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
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.
*
* OsmObject.isPolygon({"building":"yes"}) // => true
* OsmObject.isPolygon({"highway":"residential"}) // => false
* */
protected static isPolygon(tags: any): boolean {
for (const tagsKey in tags) {

View file

@ -378,6 +378,25 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* 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 {
const special = input["special"]
@ -385,10 +404,6 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
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"]
if (type === undefined) {
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
errors.push(...Object.keys(special)
.filter(k => !argNames.has(k))
.filter(k => k !== "type")
.filter(k => k !== "type" && k !== "before" && k !== "after")
.map(wrongArg => {
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
@ -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)}"}} ]}
* result // => expected
*
* // Should put text before if specified
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const expected = {render: {'en': "Some introduction{image_carousel(image)}"}}
* 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[] } {
const errors = []

View file

@ -42,7 +42,6 @@ import NoteCommentElement from "./Popup/NoteCommentElement";
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader";
import FileSelectorButton from "./Input/FileSelectorButton";
import {LoginToggle} from "./Popup/LoginButton";
import {start} from "repl";
import {SubstitutedTranslation} from "./SubstitutedTranslation";
import {TextField} from "./Input/TextField";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
@ -60,7 +59,8 @@ import Slider from "./Input/Slider";
import List from "./Base/List";
import StatisticsPanel from "./BigComponents/StatisticsPanel";
import {OsmFeature} from "../Models/OsmFeature";
import Link from "./Base/Link";
import EditableTagRendering from "./Popup/EditableTagRendering";
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
export interface SpecialVisualization {
funcName: string,
@ -334,6 +334,13 @@ export default class SpecialVisualizations {
render: {
special: {
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",
"message": {
en: "some other really long message",
@ -1206,7 +1213,7 @@ export default class SpecialVisualizations {
{
funcName: "multi",
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
example: "```json\n"+JSON.stringify({
example: "```json\n" + JSON.stringify({
render: {
special: {
type: "multi",
@ -1216,20 +1223,81 @@ export default class SpecialVisualizations {
}
}
}
}, null, " ")+"```",
}, null, " ") + "```",
args: [
{name: "key",
doc: "The property to read and to interpret as a list of properties"},
{
name:"tagrendering",
doc: "An entire tagRenderingConfig"
name: "key",
doc: "The property to read and to interpret as a list of properties",
required: true
},
{
name: "tagrendering",
doc: "An entire tagRenderingConfig",
required: true
}
]
,
,
constr(state, featureTags, args) {
const [key, tr] = args
console.log("MULTI: ", key, tr)
return undefined
const translation = new Translation({"*": tr})
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": [
"_entrance_properties=feat.overlapWith('entrance')?.map(e => e.feat.properties).filter(p => p !== undefined).filter(p => p.width !== undefined)",
"_entrance:id=feat.get('_entrance_properties')?.map(e => e.id)?.at(0)",
"_entrance:width=feat.get('_entrance_properties')?.map(e => e.width)?.at(0)"
"_entrance_properties=feat.overlapWith('entrance')?.map(e => e.feat.properties)?.filter(p => p !== undefined && p.indoor !== 'door')",
"_entrance_properties_with_width=feat.get('_entrance_properties')?.filter(p => p['width'] !== undefined)",
"_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"
],
"tagRenderings": [
"units": [
{
"id": "_entrance:width",
"render": {
"en": "<a href ='#{_entrance:id} '>This door has a width of {canonical(_entrance:width)} meters </a>",
"nl": "<a href ='#{_entrance:id} '>Deze deur heeft een breedte van {canonical(_entrance:width)} meter </a>",
"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": {
"key": "_entrance:width"
},
"mappings": [
"appliesToKey": [
"width","_biggest_width"
],
"applicableUnits": [
{
"if": "_entrance:width=",
"then": {
"en": "This entrance has no width information",
"de": "Der Eingang hat keine Informationen zur Durchgangsbreite",
"fr": "Cette entrée n'a pas d'informations sur sa largeur"
"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": [
{
"id": "entrance_info",
"render": {
"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"
},
"special": {
"type": "multi",
"key": "_entrance_properties_with_width",
"tagrendering": {
"en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
}
}
},
"mappings": [
{
"if": "_entrances_count=0",
"then": {
"en": "No entrance has been marked"
}
},
{
"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,
"widenFactor": 2,
"layers": [
"indoors"
"indoors",
{
"builtin": ["walls_and_buildings"],
"override": {
"shownByDefault": true
}
}
]
}

View file

@ -366,15 +366,11 @@
],
"overrideAll": {
"+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']})))",
"_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"
"_enclosing_building=feat.enclosingFeatures('walls_and_buildings')?.map(f => f.feat.properties.id)?.at(0)"
],
"+tagRenderings": [
"tagRenderings+": [
{
"id": "_containing_poi_entrance:width",
"id": "_stolen_entrances",
"condition": {
"and": [
"entrance=",
@ -383,21 +379,11 @@
"door="
]
},
"mappings": [{
"if": "_poi_walls_and_buildings_entrance_properties_with_width=[]",
"then": {
"en": "The containing building has {}"
}
}],
"render": {
"special": {
"type": "multi",
"key": "_poi_walls_and_buildings_entrance_properties",
"tagrendering": {
"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>"
}
"type": "steal",
"featureId": "_enclosing_building",
"tagRenderingId": "walls_and_buildings.entrance_info; walls_and_buildings.biggest_width"
}
}
}

View file

@ -1,6 +1,5 @@
import {describe} from 'mocha'
import {expect} from 'chai'
import {Utils} from "../Utils";
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
})
})