First usable sidewalks theme

This commit is contained in:
pietervdvn 2021-10-22 18:53:07 +02:00
parent 02a1d9696f
commit ff0ee35ec1
19 changed files with 537 additions and 174 deletions

View file

@ -12,7 +12,7 @@ import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
import {Changes} from "../Osm/Changes";
import GeoJsonSource from "./Sources/GeoJsonSource";
import Loc from "../../Models/Loc";
import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource";
import WayHandlingApplyingFeatureSource from "./Sources/RenderingMultiPlexerFeatureSource";
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource";
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";

View file

@ -0,0 +1,107 @@
/**
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered.
*/
import {UIEventSource} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
import FeatureSource from "../FeatureSource";
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export default class RenderingMultiPlexerFeatureSource {
public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
constructor(upstream: FeatureSource, layer: LayerConfig) {
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
rendering: r,
index: i
}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
const lineRenderObjects = layer.lineRendering
const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[] = [];
function addAsPoint(feat, rendering, coordinate) {
const patched = {
...feat,
pointRenderingIndex: rendering.index
}
patched.geometry = {
type: "Point",
coordinates: coordinate
}
withIndex.push(patched)
}
for (const f of features) {
const feat = f.feature;
if (feat.geometry.type === "Point") {
for (const rendering of pointRenderings) {
withIndex.push({
...feat,
pointRenderingIndex: rendering.index
})
}
} else {
// This is a a line
for (const rendering of centroidRenderings) {
addAsPoint(feat, rendering, GeoOperations.centerpointCoordinates(feat))
}
if (feat.geometry.type === "LineString") {
const coordinates = feat.geometry.coordinates
for (const rendering of startRenderings) {
addAsPoint(feat, rendering, coordinates[0])
}
for (const rendering of endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
}
if (feat.geometry.type === "MultiLineString") {
const lineList = feat.geometry.coordinates
for (const coordinates of lineList) {
for (const rendering of startRenderings) {
const coordinate = coordinates[0]
addAsPoint(feat, rendering, coordinate)
}
for (const rendering of endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
}
}
for (let i = 0; i < lineRenderObjects.length; i++) {
withIndex.push({
...feat,
lineRenderingIndex: i
})
}
}
}
return withIndex;
}
);
}
}

View file

@ -1,65 +0,0 @@
/**
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered.
*/
import {UIEventSource} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
import FeatureSource from "../FeatureSource";
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export default class RenderingMultiPlexerFeatureSource {
public readonly features: UIEventSource<(any & {pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined})[]>;
constructor(upstream: FeatureSource, layer: LayerConfig) {
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({rendering: r, index: i}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const lineRenderObjects = layer.lineRendering
const withIndex : (any & {pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined})[] = [];
for (const f of features) {
const feat = f.feature;
if(feat.geometry.type === "Point"){
for (const rendering of pointRenderings) {
withIndex.push({
...feat,
pointRenderingIndex: rendering.index
})
}
}else{
// This is a a line
for (const rendering of centroidRenderings) {
withIndex.push({
...GeoOperations.centerpoint(feat),
pointRenderingIndex: rendering.index
})
}
for (let i = 0; i < lineRenderObjects.length; i++){
withIndex.push({
...feat,
lineRenderingIndex:i
})
}
}
}
return withIndex;
}
);
}
}

View file

@ -49,7 +49,7 @@ export default class ElementsState extends FeatureSwitchState {
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse);
this.changes = new Changes(layoutToUse.isLeftRightSensitive())
this.changes = new Changes(layoutToUse?.isLeftRightSensitive() ?? false)
{
// -- Location control initialization

View file

@ -109,6 +109,20 @@ export class UIEventSource<T> {
promise?.catch(err => src.setData({error: err}))
return src
}
public withEqualityStabilized(comparator: (t:T | undefined, t1:T | undefined) => boolean): UIEventSource<T>{
let oldValue = undefined;
return this.map(v => {
if(v == oldValue){
return oldValue
}
if(comparator(oldValue, v)){
return oldValue
}
oldValue = v;
return v;
})
}
/**
* Given a UIEVentSource with a list, returns a new UIEventSource which is only updated if the _contents_ of the list are different.

View file

@ -197,7 +197,12 @@ export interface LayerConfigJson {
* A special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.
*
*/
tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson) [],
tagRenderings?: (string | {builtin: string, override: any} | TagRenderingConfigJson | {
leftRightKeys: string[],
renderings: (string | {builtin: string, override: any} | TagRenderingConfigJson)[]
}) [],
/**

View file

@ -15,7 +15,7 @@ export default interface PointRenderingConfigJson {
* All the locations that this point should be rendered at.
* Using `location: ["point", "centroid"] will always render centerpoint
*/
location: ("point" | "centroid")[]
location: ("point" | "centroid" | "start" | "end")[]
/**
* The icon for an element.

View file

@ -11,6 +11,12 @@ export interface TagRenderingConfigJson {
* Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise
*/
id?: string,
/**
* Optional: this can group questions together in one question box.
* Written by 'left-right'-keys automatically
*/
group?: string
/**
* Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element.

View file

@ -15,8 +15,9 @@ import WithContextLoader from "./WithContextLoader";
import LineRenderingConfig from "./LineRenderingConfig";
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
export default class LayerConfig extends WithContextLoader{
export default class LayerConfig extends WithContextLoader {
id: string;
name: Translation;
@ -31,7 +32,7 @@ export default class LayerConfig extends WithContextLoader{
maxzoom: number;
title?: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
public readonly mapRendering: PointRenderingConfig[]
public readonly lineRendering: LineRenderingConfig[]
@ -82,7 +83,7 @@ export default class LayerConfig extends WithContextLoader{
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
}
this. source = new SourceConfig(
this.source = new SourceConfig(
{
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
@ -92,14 +93,15 @@ export default class LayerConfig extends WithContextLoader{
},
json.id
);
}else if(legacy === undefined){
throw "No valid source defined ("+context+")"
} else {
this. source = new SourceConfig({
this.source = new SourceConfig({
osmTags: legacy,
});
}
this.id = json.id;
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, context + ".name");
@ -191,22 +193,21 @@ export default class LayerConfig extends WithContextLoader{
return config;
});
if(json.mapRendering === undefined){
throw "MapRendering is undefined in "+context
if (json.mapRendering === undefined) {
throw "MapRendering is undefined in " + context
}
this.mapRendering = json.mapRendering
.filter(r => r["icon"] !== undefined || r["label"] !== undefined)
.map((r, i) => new PointRenderingConfig(<PointRenderingConfigJson>r, context+".mapRendering["+i+"]"))
.map((r, i) => new PointRenderingConfig(<PointRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
this.lineRendering = json.mapRendering
.filter(r => r["icon"] === undefined && r["label"] === undefined)
.map((r, i) => new LineRenderingConfig(<LineRenderingConfigJson>r, context+".mapRendering["+i+"]"))
.map((r, i) => new LineRenderingConfig(<LineRenderingConfigJson>r, context + ".mapRendering[" + i + "]"))
this.tagRenderings = this.trs(json.tagRenderings, false);
const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined) ?? [];
this.tagRenderings = this.ExtractLayerTagRenderings(json)
const missingIds = json.tagRenderings?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["leftRightKeys"] === undefined) ?? [];
if (missingIds.length > 0 && official) {
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
@ -237,11 +238,11 @@ export default class LayerConfig extends WithContextLoader{
}
}
this.titleIcons = this.trs(titleIcons, true);
this.titleIcons = this.ParseTagRenderings(titleIcons, true);
this.title = this.tr("title", undefined);
this.isShown = this.tr("isShown", "yes");
this.deletion = null;
if (json.deletion === true) {
json.deletion = {};
@ -269,6 +270,98 @@ export default class LayerConfig extends WithContextLoader{
}
}
public ExtractLayerTagRenderings(json: LayerConfigJson): TagRenderingConfig[] {
if (json.tagRenderings === undefined) {
return []
}
const normalTagRenderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] = []
const leftRightRenderings: ({ leftRightKeys: string[], renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[] })[] = []
for (let i = 0; i < json.tagRenderings.length; i++) {
const tr = json.tagRenderings[i];
const lrkDefined = tr["leftRightKeys"] !== undefined
const renderingsDefined = tr["renderings"]
if (!lrkDefined && !renderingsDefined) {
// @ts-ignore
normalTagRenderings.push(tr)
continue
}
if (lrkDefined && renderingsDefined) {
// @ts-ignore
leftRightRenderings.push(tr)
continue
}
throw `Error in ${this._context}.tagrenderings[${i}]: got a value which defines either \`leftRightKeys\` or \`renderings\`, but not both. Either define both or move the \`renderings\` out of this scope`
}
const allRenderings = this.ParseTagRenderings(normalTagRenderings, false);
if(leftRightRenderings.length === 0){
return allRenderings
}
const leftRenderings : TagRenderingConfig[] = []
const rightRenderings : TagRenderingConfig[] = []
function prepConfig(target:string, tr: TagRenderingConfigJson){
function replaceRecursive(transl: string | any){
if(typeof transl === "string"){
return transl.replace("left|right", target)
}
if(transl.map !== undefined){
return transl.map(o => replaceRecursive(o))
}
transl = {...transl}
for (const key in transl) {
transl[key] = replaceRecursive(transl[key])
}
return transl
}
const orig = tr;
tr = replaceRecursive(tr)
tr.id = target+"-"+orig.id
tr.group = target
return tr
}
for (const leftRightRendering of leftRightRenderings) {
const keysToRewrite = leftRightRendering.leftRightKeys
const tagRenderings = leftRightRendering.renderings
const left = this.ParseTagRenderings(tagRenderings, false, tr => prepConfig("left", tr))
const right = this.ParseTagRenderings(tagRenderings, false, tr => prepConfig("right", tr))
leftRenderings.push(...left)
rightRenderings.push(...right)
}
leftRenderings.push(new TagRenderingConfig(<TagRenderingConfigJson>{
id: "questions",
group:"left",
}, "layerConfig.ts.leftQuestionBox"))
rightRenderings.push(new TagRenderingConfig(<TagRenderingConfigJson>{
id: "questions",
group:"right",
}, "layerConfig.ts.rightQuestionBox"))
allRenderings.push(...leftRenderings)
allRenderings.push(...rightRenderings)
return allRenderings;
}
public CustomCodeSnippets(): string[] {
if (this.calculatedTags === undefined) {
return [];
@ -277,8 +370,6 @@ export default class LayerConfig extends WithContextLoader{
}
public ExtractImages(): Set<string> {
const parts: Set<string>[] = [];
parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false)));
@ -293,12 +384,11 @@ export default class LayerConfig extends WithContextLoader{
for (const part of parts) {
part?.forEach(allIcons.add, allIcons);
}
return allIcons;
}
public isLeftRightSensitive() : boolean{
public isLeftRightSensitive(): boolean {
return this.lineRendering.some(lr => lr.leftRightSensitive)
}
}

View file

@ -15,7 +15,7 @@ import {VariableUiElement} from "../../UI/Base/VariableUIElement";
export default class PointRenderingConfig extends WithContextLoader {
public readonly location: Set<"point" | "centroid">
public readonly location: Set<"point" | "centroid" | "start" | "end">
public readonly icon: TagRenderingConfig;
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];

View file

@ -14,6 +14,7 @@ import {Utils} from "../../Utils";
export default class TagRenderingConfig {
readonly id: string;
readonly group: string;
readonly render?: Translation;
readonly question?: Translation;
readonly condition?: TagsFilter;
@ -46,7 +47,8 @@ export default class TagRenderingConfig {
this.question = null;
this.condition = null;
}
if(typeof json === "number"){
this.render = Translations.WT( ""+json)
return;
@ -64,6 +66,7 @@ export default class TagRenderingConfig {
this.id = json.id ?? "";
this.group = json.group ?? "";
this.render = Translations.T(json.render, context + ".render");
this.question = Translations.T(json.question, context + ".question");
this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);

View file

@ -5,8 +5,8 @@ import {Utils} from "../../Utils";
export default class WithContextLoader {
private readonly _json: any;
private readonly _context: string;
protected readonly _context: string;
constructor(json: any, context: string) {
this._json = json;
this._context = context;
@ -43,20 +43,22 @@ export default class WithContextLoader {
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call
*/
public trs(
public ParseTagRenderings(
tagRenderings?: (string | { builtin: string, override: any } | TagRenderingConfigJson)[],
readOnly = false
readOnly = false,
prepConfig: ((config: TagRenderingConfigJson) => TagRenderingConfigJson) = undefined
) {
if (tagRenderings === undefined) {
return [];
}
const context = this._context
const renderings: TagRenderingConfig[] = []
const context = this._context
const renderings: TagRenderingConfig[] = []
if (prepConfig === undefined) {
prepConfig = c => c
}
for (let i = 0; i < tagRenderings.length; i++) {
let renderingJson= tagRenderings[i]
let renderingJson = tagRenderings[i]
if (typeof renderingJson === "string") {
renderingJson = {builtin: renderingJson, override: undefined}
}
@ -70,41 +72,29 @@ export default class WithContextLoader {
)}`;
}
const tr = new TagRenderingConfig("questions", context);
renderings.push(tr)
const tr = new TagRenderingConfig("questions", context);
renderings.push(tr)
continue;
}
if (renderingJson["override"] !== undefined) {
const sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId)
const tr = new TagRenderingConfig(
Utils.Merge(renderingJson["override"], sharedJson),
`${context}.tagrendering[${i}]+override`
);
renderings.push(tr)
continue
}
let sharedJson = SharedTagRenderings.SharedTagRenderingJson.get(renderingId)
const shared = SharedTagRenderings.SharedTagRendering.get(renderingId);
if (sharedJson === undefined) {
const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys());
throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join(
", "
)}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
}
if (shared !== undefined) {
renderings.push( shared)
continue
renderingJson = Utils.Merge(renderingJson["override"], sharedJson)
}
if (Utils.runningFromConsole) {
continue
}
const keys = Array.from( SharedTagRenderings.SharedTagRendering.keys() );
throw `Predefined tagRendering ${renderingId} not found in ${context}.\n Try one of ${keys.join(
", "
)}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
}
const tr = new TagRenderingConfig(
<TagRenderingConfigJson>renderingJson,
`${context}.tagrendering[${i}]`
);
const patchedConfig = prepConfig(<TagRenderingConfigJson>renderingJson)
const tr = new TagRenderingConfig(patchedConfig, `${context}.tagrendering[${i}]`);
renderings.push(tr)
}

View file

@ -117,7 +117,8 @@ export default class ValidatedTextField {
if (args[0]) {
zoom = Number(args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ",args[0]," (using 19 instead)")
zoom = 19
}
}

View file

@ -18,6 +18,7 @@ import {Translation} from "../i18n/Translation";
import {Utils} from "../../Utils";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import MoveWizard from "./MoveWizard";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class FeatureInfoBox extends ScrollableFullScreen {
@ -52,26 +53,33 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig): BaseUIElement {
let questionBox: BaseUIElement = undefined;
let questionBoxes: Map<string, BaseUIElement> = new Map<string, BaseUIElement>();
if (State.state.featureSwitchUserbadge.data) {
questionBox = new QuestionBox(tags, layerConfig.tagRenderings, layerConfig.units);
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group))
for (const groupName of allGroupNames) {
const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
const questionBox = new QuestionBox(tags, questions, layerConfig.units);
console.log("Groupname:", groupName)
questionBoxes.set(groupName, questionBox)
}
}
let questionBoxIsUsed = false;
const renderings: BaseUIElement[] = layerConfig.tagRenderings.map(tr => {
if (tr.question === null) {
// This is the question box!
questionBoxIsUsed = true;
if (tr.question === null || tr.id === "questions") {
console.log("Rendering is", tr)
// This is a question box!
const questionBox = questionBoxes.get(tr.group)
questionBoxes.delete(tr.group)
return questionBox;
}
return new EditableTagRendering(tags, tr, layerConfig.units);
});
let editElements: BaseUIElement[] = []
if (!questionBoxIsUsed) {
questionBoxes.forEach(questionBox => {
editElements.push(questionBox);
}
})
if(layerConfig.allowMove) {
editElements.push(

View file

@ -4,7 +4,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
import {ElementStorage} from "../../Logic/ElementStorage";
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource";
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource";
/*
// import 'leaflet-polylineoffset';
We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object.
@ -161,7 +161,18 @@ export default class ShowDataLayer {
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties);
let offsettedLine;
tagsSource.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)).addCallbackAndRunD(lineStyle => {
tagsSource
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
.withEqualityStabilized((a, b) => {
if(a === b){
return true
}
if(a === undefined || b === undefined){
return false
}
return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray
})
.addCallbackAndRunD(lineStyle => {
if (offsettedLine !== undefined) {
self.geoLayer.removeLayer(offsettedLine)
}
@ -261,7 +272,7 @@ export default class ShowDataLayer {
let infobox: FeatureInfoBox = undefined;
const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointerRenderingIndex ?? feature.lineRenderingIndex}`
const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}`
popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`)
leafletLayer.on("popupopen", () => {
if (infobox === undefined) {

View file

@ -29,6 +29,8 @@ import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
import WikipediaBox from "./Wikipedia/WikipediaBox";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
import MultiApply from "./Popup/MultiApply";
import AllKnownLayers from "../Customizations/AllKnownLayers";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
export interface SpecialVisualization {
funcName: string,
@ -49,7 +51,7 @@ export default class SpecialVisualizations {
constr: ((state: State, tags: UIEventSource<any>) => {
const calculatedTags = [].concat(
SimpleMetaTagger.lazyTags,
... state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? []))
...state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? []))
return new VariableUiElement(tags.map(tags => {
const parts = [];
for (const key in tags) {
@ -57,20 +59,20 @@ export default class SpecialVisualizations {
continue
}
let v = tags[key]
if(v === ""){
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"]);
}
for(const key of calculatedTags){
for (const key of calculatedTags) {
const value = tags[key]
if(value === undefined){
if (value === undefined) {
continue
}
parts.push([ "<i>"+key+"</i>", value ])
parts.push(["<i>" + key + "</i>", value])
}
return new Table(
["key", "value"],
parts
@ -88,7 +90,7 @@ export default class SpecialVisualizations {
}],
constr: (state: State, tags, args) => {
let imagePrefixes: string[] = undefined;
if(args.length > 0){
if (args.length > 0) {
imagePrefixes = [].concat(...args.map(a => a.split(",")));
}
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, imagePrefixes);
@ -101,9 +103,9 @@ export default class SpecialVisualizations {
name: "image-key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "image"
},{
name:"label",
doc:"The text to show on the button",
}, {
name: "label",
doc: "The text to show on the button",
defaultValue: "Add image"
}],
constr: (state: State, tags, args) => {
@ -125,17 +127,16 @@ export default class SpecialVisualizations {
new VariableUiElement(
tagsSource.map(tags => tags[args[0]])
.map(wikidata => {
const wikidatas : string[] =
const wikidatas: string[] =
Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? [])
return new WikipediaBox(wikidatas)
})
)
},
{
funcName: "minimap",
docs: "A small map showing the selected feature. Note that no styling is applied, wrap this in a div",
docs: "A small map showing the selected feature.",
args: [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
@ -214,6 +215,54 @@ export default class SpecialVisualizations {
)
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap;
}
},
{
funcName: "sided_minimap",
docs: "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced",
args: [
{
doc: "The side to show, either `left` or `right`",
name: "side",
}
],
example: "`{sided_minimap(left)}`",
constr: (state, tagSource, args) => {
const properties = tagSource.data;
const locationSource = new UIEventSource<Loc>({
lat: Number(properties._lat),
lon: Number(properties._lon),
zoom: 18
})
const minimap = Minimap.createMiniMap(
{
background: state.backgroundLayer,
location: locationSource,
allowMoving: false
}
)
const side = args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
const copy = {...feature}
copy.properties = {
id: side
}
new ShowDataLayer(
{
leafletMap: minimap["leafletMap"],
enablePopups: false,
zoomToFeatures: true,
layerToShow: AllKnownLayers.sharedLayers.get("left_right_style"),
features: new StaticFeatureSource([copy], false),
allElements: State.state.allElements
}
)
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap;
}
@ -432,9 +481,11 @@ export default class SpecialVisualizations {
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg"
},
{name:"minzoom",
doc: "How far the contributor must zoom in before being able to import the point",
defaultValue: "18"}],
{
name: "minzoom",
doc: "How far the contributor must zoom in before being able to import the point",
defaultValue: "18"
}],
docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
If you want to import a dataset, make sure that:
@ -487,14 +538,24 @@ There are also some technicalities in your theme to keep in mind:
)
}
},
{funcName: "multi_apply",
{
funcName: "multi_apply",
docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags",
args:[
args: [
{name: "feature_ids", doc: "A JSOn-serialized list of IDs of features to apply the tagging on"},
{name: "keys", doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." },
{
name: "keys",
doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features."
},
{name: "text", doc: "The text to show on the button"},
{name:"autoapply",doc:"A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown"},
{name:"overwrite",doc:"If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change"}
{
name: "autoapply",
doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown"
},
{
name: "overwrite",
doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change"
}
],
example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}",
constr: (state, tagsSource, args) => {
@ -503,14 +564,14 @@ There are also some technicalities in your theme to keep in mind:
const text = args[2]
const autoapply = args[3]?.toLowerCase() === "true"
const overwrite = args[4]?.toLowerCase() === "true"
const featureIds : UIEventSource<string[]> = tagsSource.map(tags => {
const ids = tags[featureIdsKey]
try{
if(ids === undefined){
const featureIds: UIEventSource<string[]> = tagsSource.map(tags => {
const ids = tags[featureIdsKey]
try {
if (ids === undefined) {
return []
}
return JSON.parse(ids);
}catch(e){
} catch (e) {
console.warn("Could not parse ", ids, "as JSON to extract IDS which should be shown on the map.")
return []
}
@ -526,7 +587,7 @@ There are also some technicalities in your theme to keep in mind:
state
}
);
}
}
]

View file

@ -0,0 +1,41 @@
{
"id": "left_right_style",
"description": "Special meta-style which will show one single line, either on the left or on the right depending on the id. This is used in the small popups with left_right roads",
"source": {
"osmTags": {
"or": [
"id=left",
"id=right"
]
}
},
"mapRendering": [
{
"width": 6,
"offset": {
"mappings": [
{
"if": "id=left",
"then": "-5"
},
{
"if": "id=right",
"then": "5"
}
]
},
"color": {
"mappings": [
{
"if": "id=left",
"then": "#00f"
},
{
"if": "id=right",
"then": "#f00"
}
]
}
}
]
}

View file

@ -32,20 +32,111 @@
},
"title": {
"render": {
"en": "Street {name}"
}
"en": "{name}"
},
"mappings": [
{
"if": "name=",
"then": "Nameless street"
}
]
},
"description": {
"en": "Layer showing sidewalks of highways"
},
"tagRenderings": [],
"tagRenderings": [
{
"id": "streetname",
"render": {
"en": "This street is named {name}"
}
},
{
"leftRightKeys": "sidewalk:left|right",
"renderings": [
{
"id": "sidewalk_minimap",
"render": "{sided_minimap(left|right):height:3rem;border-radius:0.5rem;overflow:hidden}"
},
{
"id": "has_sidewalk",
"question": "Is there a sidewalk on the left|right side of the road?",
"mappings": [{
"if": "sidewalk:left|right=yes",
"then": "Yes, there is a sidewalk on this side of the road"
},
{
"if": "sidewalk:left|right=no",
"then": "No, there is no seperated sidewalk to walk on"
}]
},
{
"id": "sidewalk_width",
"question": "What is the width of the sidewalk on this side of the road?",
"render": "This sidewalk is {sidewalk:left|right:width}m wide",
"condition": "sidewalk:left|right=yes",
"freeform": {
"key": "sidewalk:left|right:width",
"type": "length",
"helperArgs": ["21", "map"]
}
}
]
}],
"mapRendering": [
{
"location": [
"point"
]
"location": ["start","end"],
"icon": "circle:#ccc",
"iconSize": "20,20,center"
},
{}
{
"color": "#ffffff55",
"width": 8
},
{
"color": {
"render": "#888"
},
"width": {
"render": 6,
"mappings": [
{
"if": {
"or": [
"sidewalk:left=",
"sidewalk:left=no",
"sidewalk:left=separate"
]
},
"then": 0
}
]
},
"offset": -6
},
{
"color": "#888",
"width": {
"render": 6,
"mappings": [
{
"if": {
"or": [
"sidewalk:right=",
"sidewalk:right=no",
"sidewalk:right=separate"
]
},
"then": 0
}
]
},
"offset": 6
}
],
"allowSplit": true
}

View file

@ -32,7 +32,7 @@ function fixLayerConfig(config: LayerConfigJson): void {
}
}
if (config.mapRendering === undefined || true) {
if (config.mapRendering === undefined) {
// This is a legacy format, lets create a pointRendering
let location: ("point" | "centroid")[] = ["point"]
let wayHandling: number = config["wayHandling"] ?? 0