First usable sidewalks theme
This commit is contained in:
parent
02a1d9696f
commit
ff0ee35ec1
19 changed files with 537 additions and 174 deletions
|
@ -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";
|
||||
|
|
107
Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts
Normal file
107
Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)[]
|
||||
}) [],
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }[];
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
41
assets/layers/left_right_style/left_right_style.json
Normal file
41
assets/layers/left_right_style/left_right_style.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue