Finish importer, add applicable import layers to every theme by default

This commit is contained in:
Pieter Vander Vennet 2022-01-21 01:57:16 +01:00
parent 3402ac0954
commit ca1490902c
41 changed files with 1559 additions and 898 deletions

View file

@ -10,11 +10,12 @@ import {UIEventSource} from "./UIEventSource";
import {LocalStorageSource} from "./Web/LocalStorageSource"; import {LocalStorageSource} from "./Web/LocalStorageSource";
import LZString from "lz-string"; import LZString from "lz-string";
import * as personal from "../assets/themes/personal/personal.json"; import * as personal from "../assets/themes/personal/personal.json";
import {FixLegacyTheme, PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import SharedTagRenderings from "../Customizations/SharedTagRenderings"; import SharedTagRenderings from "../Customizations/SharedTagRenderings";
import * as known_layers from "../assets/generated/known_layers.json" import * as known_layers from "../assets/generated/known_layers.json"
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
export default class DetermineLayout { export default class DetermineLayout {

View file

@ -29,11 +29,11 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
}, },
tileIndex, tileIndex,
upstream: FeatureSourceForLayer, upstream: FeatureSourceForLayer,
metataggingUpdated: UIEventSource<any> metataggingUpdated?: UIEventSource<any>
) { ) {
this.name = "FilteringFeatureSource(" + upstream.name + ")" this.name = "FilteringFeatureSource(" + upstream.name + ")"
this.tileIndex = tileIndex this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex) this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex)
this.upstream = upstream this.upstream = upstream
this.state = state this.state = state
@ -55,7 +55,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
} }
}) })
metataggingUpdated.addCallback(_ => { metataggingUpdated?.addCallback(_ => {
self._is_dirty.setData(true) self._is_dirty.setData(true)
}) })
@ -63,6 +63,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
} }
private update() { private update() {
console.log("FIltering", this.upstream.name)
const self = this; const self = this;
const layer = this.upstream.layer; const layer = this.upstream.layer;
const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []); const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []);

View file

@ -28,7 +28,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
private readonly featureIdBlacklist?: UIEventSource<Set<string>> private readonly featureIdBlacklist?: UIEventSource<Set<string>>
public constructor(flayer: FilteredLayer, public constructor(flayer: FilteredLayer,
zxy?: [number, number, number], zxy?: [number, number, number] | BBox,
options?: { options?: {
featureIdBlacklist?: UIEventSource<Set<string>> featureIdBlacklist?: UIEventSource<Set<string>>
}) { }) {
@ -41,23 +41,32 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
this.featureIdBlacklist = options?.featureIdBlacklist this.featureIdBlacklist = options?.featureIdBlacklist
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
if (zxy !== undefined) { if (zxy !== undefined) {
const [z, x, y] = zxy; let tile_bbox: BBox;
let tile_bbox = BBox.fromTile(z, x, y) if (zxy instanceof BBox) {
tile_bbox = zxy;
} else {
const [z, x, y] = zxy;
tile_bbox = BBox.fromTile(z, x, y);
this.tileIndex = Tiles.tile_index(z, x, y)
this.bbox = BBox.fromTile(z, x, y)
url = url
.replace('{z}', "" + z)
.replace('{x}', "" + x)
.replace('{y}', "" + y)
}
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
if (this.layer.layerDef.source.mercatorCrs) { if (this.layer.layerDef.source.mercatorCrs) {
bounds = tile_bbox.toMercator() bounds = tile_bbox.toMercator()
} }
url = url url = url
.replace('{z}', "" + z)
.replace('{x}', "" + x)
.replace('{y}', "" + y)
.replace('{y_min}', "" + bounds.minLat) .replace('{y_min}', "" + bounds.minLat)
.replace('{y_max}', "" + bounds.maxLat) .replace('{y_max}', "" + bounds.maxLat)
.replace('{x_min}', "" + bounds.minLon) .replace('{x_min}', "" + bounds.minLon)
.replace('{x_max}', "" + bounds.maxLon) .replace('{x_max}', "" + bounds.maxLon)
this.tileIndex = Tiles.tile_index(z, x, y)
this.bbox = BBox.fromTile(z, x, y)
} else { } else {
this.tileIndex = Tiles.tile_index(0, 0, 0) this.tileIndex = Tiles.tile_index(0, 0, 0)
this.bbox = BBox.global; this.bbox = BBox.global;
@ -83,7 +92,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
if (self.layer.layerDef.source.mercatorCrs) { if (self.layer.layerDef.source.mercatorCrs) {
json = GeoOperations.GeoJsonToWGS84(json) json = GeoOperations.GeoJsonToWGS84(json)
} }
const time = new Date(); const time = new Date();
const newFeatures: { feature: any, freshness: Date } [] = [] const newFeatures: { feature: any, freshness: Date } [] = []
let i = 0; let i = 0;

View file

@ -683,6 +683,8 @@ export class GeoOperations {
throw "CalculateIntersection fallthrough: can not calculate an intersection between features" throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
} }
} }

View file

@ -18,13 +18,13 @@ export class ChangesetHandler {
private readonly allElements: ElementStorage; private readonly allElements: ElementStorage;
private osmConnection: OsmConnection; private osmConnection: OsmConnection;
private readonly changes: Changes; private readonly changes: Changes;
private readonly _dryRun: boolean; private readonly _dryRun: UIEventSource<boolean>;
private readonly userDetails: UIEventSource<UserDetails>; private readonly userDetails: UIEventSource<UserDetails>;
private readonly auth: any; private readonly auth: any;
private readonly backend: string; private readonly backend: string;
constructor(layoutName: string, constructor(layoutName: string,
dryRun: boolean, dryRun: UIEventSource<boolean>,
osmConnection: OsmConnection, osmConnection: OsmConnection,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
@ -67,7 +67,7 @@ export class ChangesetHandler {
this.userDetails.data.csCount = 1; this.userDetails.data.csCount = 1;
this.userDetails.ping(); this.userDetails.ping();
} }
if (this._dryRun) { if (this._dryRun.data) {
const changesetXML = generateChangeXML(123456); const changesetXML = generateChangeXML(123456);
console.log("Metatags are", extraMetaTags) console.log("Metatags are", extraMetaTags)
console.log(changesetXML); console.log(changesetXML);

View file

@ -19,7 +19,6 @@ export default class UserDetails {
public img: string; public img: string;
public unreadMessages = 0; public unreadMessages = 0;
public totalMessages = 0; public totalMessages = 0;
public dryRun: boolean;
home: { lon: number; lat: number }; home: { lon: number; lat: number };
public backend: string; public backend: string;
@ -47,7 +46,6 @@ export class OsmConnection {
public auth; public auth;
public userDetails: UIEventSource<UserDetails>; public userDetails: UIEventSource<UserDetails>;
public isLoggedIn: UIEventSource<boolean> public isLoggedIn: UIEventSource<boolean>
_dryRun: boolean;
public preferencesHandler: OsmPreferences; public preferencesHandler: OsmPreferences;
public changesetHandler: ChangesetHandler; public changesetHandler: ChangesetHandler;
public readonly _oauth_config: { public readonly _oauth_config: {
@ -55,6 +53,7 @@ export class OsmConnection {
oauth_secret: string, oauth_secret: string,
url: string url: string
}; };
private readonly _dryRun: UIEventSource<boolean>;
private fakeUser: boolean; private fakeUser: boolean;
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
private readonly _iframeMode: Boolean | boolean; private readonly _iframeMode: Boolean | boolean;
@ -62,7 +61,7 @@ export class OsmConnection {
private isChecking = false; private isChecking = false;
constructor(options: { constructor(options: {
dryRun?: false | boolean, dryRun?: UIEventSource<boolean>,
fakeUser?: false | boolean, fakeUser?: false | boolean,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
@ -82,7 +81,6 @@ export class OsmConnection {
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false);
if (options.fakeUser) { if (options.fakeUser) {
const ud = this.userDetails.data; const ud = this.userDetails.data;
ud.csCount = 5678 ud.csCount = 5678
@ -99,13 +97,13 @@ export class OsmConnection {
self.AttemptLogin() self.AttemptLogin()
} }
}); });
this._dryRun = options.dryRun; this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
this.updateAuthObject(); this.updateAuthObject();
this.preferencesHandler = new OsmPreferences(this.auth, this); this.preferencesHandler = new OsmPreferences(this.auth, this);
this.changesetHandler = new ChangesetHandler(options.layoutName, options.dryRun, this, options.allElements, options.changes, this.auth); this.changesetHandler = new ChangesetHandler(options.layoutName, this._dryRun, this, options.allElements, options.changes, this.auth);
if (options.oauth_token?.data !== undefined) { if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data) console.log(options.oauth_token.data)
const self = this; const self = this;
@ -223,7 +221,7 @@ export class OsmConnection {
if ((text ?? "") !== "") { if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text) textSuffix = "?text=" + encodeURIComponent(text)
} }
if (this._dryRun) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok, error) => { return new Promise((ok, error) => {
ok() ok()
@ -246,7 +244,7 @@ export class OsmConnection {
} }
public reopenNote(id: number | string, text?: string): Promise<any> { public reopenNote(id: number | string, text?: string): Promise<any> {
if (this._dryRun) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok, error) => { return new Promise((ok, error) => {
ok() ok()
@ -273,10 +271,10 @@ export class OsmConnection {
} }
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
if (this._dryRun) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually opening note with text ", text) console.warn("Dryrun enabled - not actually opening note with text ", text)
return new Promise((ok, error) => { return new Promise<{ id: number }>((ok, error) => {
ok() window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
}); });
} }
const auth = this.auth; const auth = this.auth;
@ -285,15 +283,18 @@ export class OsmConnection {
auth.xhr({ auth.xhr({
method: 'POST', method: 'POST',
path: `/api/0.6/notes.json`, path: `/api/0.6/notes.json`,
options: {header: options: {
{'Content-Type': 'application/json'}}, header:
{'Content-Type': 'application/json'}
},
content: JSON.stringify(content) content: JSON.stringify(content)
}, function (err, response) { }, function (err, response) {
if (err !== null) { if (err !== null) {
error(err) error(err)
} else { } else {
const id = Number(response.children[0].children[0].children.item("id").innerHTML)
const id = response.properties.id
console.log("OPENED NOTE", id) console.log("OPENED NOTE", id)
ok({id}) ok({id})
} }
@ -304,7 +305,7 @@ export class OsmConnection {
} }
public addCommentToNode(id: number | string, text: string): Promise<any> { public addCommentToNode(id: number | string, text: string): Promise<any> {
if (this._dryRun) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id) console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
return new Promise((ok, error) => { return new Promise((ok, error) => {
ok() ok()
@ -317,7 +318,7 @@ export class OsmConnection {
return new Promise((ok, error) => { return new Promise((ok, error) => {
this.auth.xhr({ this.auth.xhr({
method: 'POST', method: 'POST',
path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}` path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}`
}, function (err, response) { }, function (err, response) {
if (err !== null) { if (err !== null) {

View file

@ -43,7 +43,7 @@ export default class UserRelatedState extends ElementsState {
this.osmConnection = new OsmConnection({ this.osmConnection = new OsmConnection({
changes: this.changes, changes: this.changes,
dryRun: this.featureSwitchIsTesting.data, dryRun: this.featureSwitchIsTesting,
fakeUser: this.featureSwitchFakeUser.data, fakeUser: this.featureSwitchFakeUser.data,
allElements: this.allElements, allElements: this.allElements,
oauth_token: QueryParameters.GetQueryParameter( oauth_token: QueryParameters.GetQueryParameter(

View file

@ -10,6 +10,13 @@ export class RegexTag extends TagsFilter {
constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
super(); super();
this.key = key; this.key = key;
if (typeof value === "string") {
if (value.indexOf("^") < 0 && value.indexOf("$") < 0) {
value = "^" + value + "$"
}
value = new RegExp(value)
}
this.value = value; this.value = value;
this.invert = invert; this.invert = invert;
this.matchesEmpty = RegexTag.doesMatch("", this.value); this.matchesEmpty = RegexTag.doesMatch("", this.value);
@ -109,7 +116,7 @@ export class RegexTag extends TagsFilter {
console.error("Cannot export regex tag to asChange; ", this.key, this.value) console.error("Cannot export regex tag to asChange; ", this.key, this.value)
return [] return []
} }
AsJson() { AsJson() {
return this.asHumanString() return this.asHumanString()
} }

View file

@ -192,16 +192,16 @@ export class TagUtils {
} }
const f = (value: string | undefined) => { const f = (value: string | undefined) => {
if(value === undefined){ if (value === undefined) {
return false; return false;
} }
let b = Number(value?.trim() ) let b = Number(value?.trim())
if (isNaN(b)) { if (isNaN(b)) {
if(value.endsWith(" UTC")) { if (value.endsWith(" UTC")) {
value = value.replace(" UTC", "+00") value = value.replace(" UTC", "+00")
} }
b = new Date(value).getTime() b = new Date(value).getTime()
if(isNaN(b)){ if (isNaN(b)) {
return false return false
} }
} }
@ -218,7 +218,7 @@ export class TagUtils {
} }
return new RegexTag( return new RegexTag(
split[0], split[0],
new RegExp("^" + split[1] + "$"), split[1],
true true
); );
} }
@ -228,8 +228,8 @@ export class TagUtils {
split[1] = "..*" split[1] = "..*"
} }
return new RegexTag( return new RegexTag(
new RegExp("^" + split[0] + "$"), split[0],
new RegExp("^" + split[1] + "$") split[1]
); );
} }
if (tag.indexOf("!:=") >= 0) { if (tag.indexOf("!:=") >= 0) {
@ -248,7 +248,7 @@ export class TagUtils {
} }
return new RegexTag( return new RegexTag(
split[0], split[0],
new RegExp("^" + split[1] + "$"), new RegExp("^" + split[1] + "$"),
true true
); );
} }
@ -259,7 +259,7 @@ export class TagUtils {
} }
return new RegexTag( return new RegexTag(
split[0], split[0],
new RegExp("^" + split[1] + "$"), split[1],
true true
); );
} }
@ -273,7 +273,7 @@ export class TagUtils {
} }
return new RegexTag( return new RegexTag(
split[0], split[0],
new RegExp("^" + split[1] + "$") split[1]
); );
} }
if (tag.indexOf("=") >= 0) { if (tag.indexOf("=") >= 0) {

View file

@ -0,0 +1,171 @@
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {Utils} from "../../../Utils";
export interface DesugaringContext {
tagRenderings: Map<string, TagRenderingConfigJson>
sharedLayers: Map<string, LayerConfigJson>
}
export abstract class Conversion<TIn, TOut> {
public readonly modifiedAttributes: string[];
protected readonly doc: string;
constructor(doc: string, modifiedAttributes: string[] = []) {
this.modifiedAttributes = modifiedAttributes;
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
}
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
if (fixed?.errors?.length > 0) {
throw fixed.errors.join("\n");
}
fixed.warnings?.forEach(w => console.warn(w))
return fixed.result;
}
public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut {
const fixed = this.convert(state, json, context)
return DesugaringStep.strict(fixed)
}
abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] }
public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } {
const result = []
const errors = []
const warnings = []
for (let i = 0; i < jsons.length; i++) {
const json = jsons[i];
const r = this.convert(state, json, context + "[" + i + "]")
result.push(r.result)
errors.push(...r.errors)
warnings.push(...r.warnings)
}
return {
result,
errors,
warnings
}
}
}
export abstract class DesugaringStep<T> extends Conversion<T, T> {
}
export class OnEvery<X, T> extends DesugaringStep<T> {
private readonly key: string;
private readonly step: DesugaringStep<X>;
constructor(key: string, step: DesugaringStep<X>) {
super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]);
this.step = step;
this.key = key;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
json = {...json}
const step = this.step
const key = this.key;
const r = step.convertAll(state, (<X[]>json[key]), context + "." + key)
json[key] = r.result
return {
result: json,
errors: r.errors,
warnings: r.warnings
};
}
}
export class OnEveryConcat<X, T> extends DesugaringStep<T> {
private readonly key: string;
private readonly step: Conversion<X, X[]>;
constructor(key: string, step: Conversion<X, X[]>) {
super(`Applies ${step.constructor.name} onto every object of the list \`${key}\`. The results are concatenated and used as new list`, [key]);
this.step = step;
this.key = key;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
json = {...json}
const step = this.step
const key = this.key;
const values = json[key]
if (values === undefined) {
// Move on - nothing to see here!
return {
result: json,
errors: [],
warnings: []
}
}
const r = step.convertAll(state, (<X[]>values), context + "." + key)
const vals: X[][] = r.result
json[key] = [].concat(...vals)
return {
result: json,
errors: r.errors,
warnings: r.warnings
};
}
}
export class Fuse<T> extends DesugaringStep<T> {
private readonly steps: DesugaringStep<T>[];
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.constructor.name).join(", "),
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes)))
);
this.steps = steps;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")")
errors.push(...r.errors)
warnings.push(...r.warnings)
json = r.result
if (errors.length > 0) {
break;
}
}
return {
result: json,
errors,
warnings
};
}
}
export class SetDefault<T> extends DesugaringStep<T> {
private readonly value: any;
private readonly key: string;
private readonly _overrideEmptyString: boolean;
constructor(key: string, value: any, overrideEmptyString = false) {
super("Sets " + key + " to a default value if undefined");
this.key = key;
this.value = value;
this._overrideEmptyString = overrideEmptyString;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
json = {...json}
json[this.key] = this.value
}
return {
errors: [], warnings: [],
result: json
};
}
}

View file

@ -1,29 +1,69 @@
import {Conversion, DesugaringContext} from "./LegacyJsonConvert"; import {Conversion, DesugaringContext} from "./Conversion";
import LayerConfig from "../LayerConfig"; import LayerConfig from "../LayerConfig";
import {LayerConfigJson} from "../Json/LayerConfigJson"; import {LayerConfigJson} from "../Json/LayerConfigJson";
import Translations from "../../../UI/i18n/Translations"; import Translations from "../../../UI/i18n/Translations";
import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
import {And} from "../../../Logic/Tags/And";
export default class CreateNoteImportLayer extends Conversion<LayerConfig, LayerConfigJson> { export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
/**
* A closed note is included if it is less then 'n'-days closed
* @private
*/
private readonly _includeClosedNotesDays: number;
constructor() { constructor(includeClosedNotesDays= 0) {
super([ super([
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').", "Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
"The import buttons and matches will be based on the presets of the given theme", "The import buttons and matches will be based on the presets of the given theme",
].join("\n\n"), []) ].join("\n\n"), [])
this._includeClosedNotesDays = includeClosedNotesDays;
} }
convert(state: DesugaringContext, layer: LayerConfig, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { convert(state: DesugaringContext, layerJson: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
const errors = [] const errors = []
const warnings = [] const warnings = []
const t = Translations.t.importLayer; const t = Translations.t.importLayer;
const possibleTags: TagsFilter[] = layer.presets.map(p => new And(p.tags)) /**
* The note itself will contain `tags=k=v;k=v;k=v;...
* This must be matched with a regex.
* This is a simple JSON-object as how it'll be put into the layerConfigJson directly
*/
const isShownIfAny : any[] = []
const layer = new LayerConfig(layerJson, "while constructing a note-import layer")
for (const preset of layer.presets) {
const mustMatchAll = []
for (const tag of preset.tags) {
const key = tag.key
const value = tag.value
const condition = "_tags~(^|.*;)"+key+"\="+value+"($|;.*)"
mustMatchAll.push(condition)
}
isShownIfAny.push({and:mustMatchAll})
}
const pointRenderings = (layerJson.mapRendering??[]).filter(r => r!== null && r["location"] !== undefined);
const firstRender = <PointRenderingConfigJson>(pointRenderings [0])
const icon = firstRender.icon
const iconBadges = []
if(icon !== undefined){
iconBadges.push({
if: {and:[]},
then:icon
})
}
const importButton = {}
{
const translations = t.importButton.Subs({layerId: layer.id, title: layer.presets[0].title}).translations
for (const key in translations) {
importButton[key] = "{"+translations[key]+"}"
}
}
const result : LayerConfigJson = { const result : LayerConfigJson = {
"id": "note_import_"+layer.id, "id": "note_import_"+layer.id,
"name": t.layerName.Subs({title: layer.title.render}).translations, // By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
"description": t.description.Subs({title: layer.title.render}).translations, "description": t.description.Subs({title: layer.title.render}).translations,
"source": { "source": {
"osmTags": { "osmTags": {
@ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
"id~*" "id~*"
] ]
}, },
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}", "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed="+this._includeClosedNotesDays+"&bbox={x_min},{y_min},{x_max},{y_max}",
"geoJsonZoomLevel": 12, "geoJsonZoomLevel": 10,
"maxCacheAge": 0 "maxCacheAge": 0
}, },
"minzoom": 10, "minzoom": 12,
"title": { "title": {
"render": t.popupTitle.Subs({title: layer.presets[0].title}).translations "render": t.popupTitle.Subs({title: layer.presets[0].title}).translations
}, },
"calculatedTags": [ "calculatedTags": [
"_first_comment:=feat.get('comments')[0].text.toLowerCase()", "_first_comment=feat.get('comments')[0].text.toLowerCase()",
"_trigger_index:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()", "_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
"_intro:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.map(l => l == '' ? '<br/>' : l).join('');})()", "_comments_count=feat.get('comments').length",
"_tags:=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()" "_intro=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
"_tags=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
], ],
"isShown": { "isShown": {
"render": "no", "render": "no",
"mappings": [ "mappings": [
{
"if": "comments!~.*https://mapcomplete.osm.be.*",
"then":"no"
},
{ {
"if": {and: "if": {and:
["_trigger_index~*", ["_trigger_index~*",
{or: possibleTags.map(tf => tf.AsJson())} {or: isShownIfAny}
]}, ]},
"then": "yes" "then": "yes"
} }
@ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
} }
], ],
"tagRenderings": [ "tagRenderings": [
{
"id": "conversation",
"render": "{visualize_note_comments(comments,1)}"
},
{ {
"id": "Intro", "id": "Intro",
"render": "{_intro}" "render": "{_intro}"
}, },
{
"id": "conversation",
"render": "{visualize_note_comments(comments,1)}",
condition: "_comments_count>1"
},
{ {
"id": "import", "id": "import",
"render": "{import_button(public_bookcase, _tags, There might be a public bookcase here,./assets/svg/addSmall.svg,,,id)}" "render": importButton,
condition: "closed_at="
}, },
{ {
"id": "close_note_", "id": "close_note_",
"render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}" "render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}",
condition: "closed_at="
}, },
{ {
"id": "close_note_mapped", "id": "close_note_mapped",
"render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}" "render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}",
condition: "closed_at="
},
{
"id": "handled",
"render": t.importHandled.translations,
condition: "closed_at~*"
}, },
{ {
"id": "comment", "id": "comment",
@ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
{ {
"id": "add_image", "id": "add_image",
"render": "{add_image_to_note()}" "render": "{add_image_to_note()}"
},
{
id:"alltags",
render:"{all_tags()}"
} }
], ],
"mapRendering": [ "mapRendering": [
@ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
"centroid" "centroid"
], ],
"icon": { "icon": {
"render": "teardrop:#3333cc" "render": "circle:white;help:black",
mappings:[{
if: {or:["closed_at~*","_imported=yes"]},
then:"circle:white;checkmark:black"
}]
}, },
"iconSize": "40,40,bottom" iconBadges,
"iconSize": "40,40,center"
} }
] ]
} }

View file

@ -1,439 +1,12 @@
import {LayoutConfigJson} from "../Json/LayoutConfigJson"; import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import DependencyCalculator from "../DependencyCalculator";
import LayerConfig from "../LayerConfig"; import LayerConfig from "../LayerConfig";
import {Translation} from "../../../UI/i18n/Translation"; import {Translation} from "../../../UI/i18n/Translation";
import LayoutConfig from "../LayoutConfig"; import LayoutConfig from "../LayoutConfig";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
import {LayerConfigJson} from "../Json/LayerConfigJson"; import {LayerConfigJson} from "../Json/LayerConfigJson";
import Constants from "../../Constants"; import Constants from "../../Constants";
import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts"; import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion";
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
export interface DesugaringContext {
tagRenderings: Map<string, TagRenderingConfigJson>
sharedLayers: Map<string, LayerConfigJson>
}
export abstract class Conversion<TIn, TOut> {
public readonly modifiedAttributes: string[];
protected readonly doc: string;
constructor(doc: string, modifiedAttributes: string[] = []) {
this.modifiedAttributes = modifiedAttributes;
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
}
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
if (fixed?.errors?.length > 0) {
throw fixed.errors.join("\n");
}
fixed.warnings?.forEach(w => console.warn(w))
return fixed.result;
}
public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut {
const fixed = this.convert(state, json, context)
return DesugaringStep.strict(fixed)
}
abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] }
public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } {
const result = []
const errors = []
const warnings = []
for (let i = 0; i < jsons.length; i++) {
const json = jsons[i];
const r = this.convert(state, json, context + "[" + i + "]")
result.push(r.result)
errors.push(...r.errors)
warnings.push(...r.warnings)
}
return {
result,
errors,
warnings
}
}
}
export abstract class DesugaringStep<T> extends Conversion<T, T> {
}
class OnEvery<X, T> extends DesugaringStep<T> {
private readonly key: string;
private readonly step: DesugaringStep<X>;
constructor(key: string, step: DesugaringStep<X>) {
super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]);
this.step = step;
this.key = key;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
json = {...json}
const step = this.step
const key = this.key;
const r = step.convertAll(state, (<X[]>json[key]), context + "." + key)
json[key] = r.result
return {
result: json,
errors: r.errors,
warnings: r.warnings
};
}
}
class OnEveryConcat<X, T> extends DesugaringStep<T> {
private readonly key: string;
private readonly step: Conversion<X, X[]>;
constructor(key: string, step: Conversion<X, X[]>) {
super(`Applies ${step.constructor.name} onto every object of the list \`${key}\``, [key]);
this.step = step;
this.key = key;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
json = {...json}
const step = this.step
const key = this.key;
const values = json[key]
if (values === undefined) {
// Move on - nothing to see here!
return {
result: json,
errors: [],
warnings: []
}
}
const r = step.convertAll(state, (<X[]>values), context + "." + key)
const vals: X[][] = r.result
json[key] = [].concat(...vals)
return {
result: json,
errors: r.errors,
warnings: r.warnings
};
}
}
class Fuse<T> extends DesugaringStep<T> {
private readonly steps: DesugaringStep<T>[];
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.constructor.name).join(", "),
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes)))
);
this.steps = steps;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")")
errors.push(...r.errors)
warnings.push(...r.warnings)
json = r.result
if (errors.length > 0) {
break;
}
}
return {
result: json,
errors,
warnings
};
}
}
class AddMiniMap extends DesugaringStep<LayerConfigJson> {
constructor() {
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]);
}
/**
* Returns true if this tag rendering has a minimap in some language.
* Note: this minimap can be hidden by conditions
*/
private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
for (const translation of translations) {
for (const key in translation.translations) {
if (!translation.translations.hasOwnProperty(key)) {
continue
}
const template = translation.translations[key]
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
if (hasMiniMap) {
return true;
}
}
}
return false;
}
convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson> tr)) ?? true
if (!hasMinimap) {
layerConfig = {...layerConfig}
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
return {
errors:[],
warnings: [],
result: layerConfig
};
}
}
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
constructor() {
super("Converts a tagRenderingSpec into the full tagRendering", []);
}
convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
return {
result: this.convertUntilStable(state, json, warnings, errors, context),
errors, warnings
};
}
private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] {
if (state.tagRenderings.has(name)) {
return [state.tagRenderings.get(name)]
}
if (name.indexOf(".") >= 0) {
const spl = name.split(".");
const layer = state.sharedLayers.get(spl[0])
if (spl.length === 2 && layer !== undefined) {
const id = spl[1];
const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined)
let matchingTrs: TagRenderingConfigJson[]
if (id === "*") {
matchingTrs = layerTrs
} else if (id.startsWith("*")) {
const id_ = id.substring(1)
matchingTrs = layerTrs.filter(tr => tr.group === id_)
} else {
matchingTrs = layerTrs.filter(tr => tr.id === id)
}
for (let i = 0; i < matchingTrs.length; i++) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
const found = Utils.Clone(matchingTrs[i]);
if (found.condition === undefined) {
found.condition = layer.source.osmTags
} else {
found.condition = {and: [found.condition, layer.source.osmTags]}
}
matchingTrs[i] = found
}
if (matchingTrs.length !== 0) {
return matchingTrs
}
}
}
return undefined;
}
private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
if (tr === "questions") {
return [{
id: "questions"
}]
}
if (typeof tr === "string") {
const lookup = this.lookup(state, tr);
if (lookup !== undefined) {
return lookup
}
warnings.push(ctx + "A literal rendering was detected: " + tr)
return [{
render: tr,
id: tr.replace(/![a-zA-Z0-9]/g, "")
}]
}
if (tr["builtin"] !== undefined) {
let names = tr["builtin"]
if (typeof names === "string") {
names = [names]
}
for (const key of Object.keys(tr)) {
if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) {
continue
}
errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr))
}
const trs: TagRenderingConfigJson[] = []
for (const name of names) {
const lookup = this.lookup(state, name)
if (lookup === undefined) {
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
continue
}
for (let foundTr of lookup) {
foundTr = Utils.Clone<any>(foundTr)
Utils.Merge(tr["override"] ?? {}, foundTr)
trs.push(foundTr)
}
}
return trs;
}
return [tr]
}
private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
const trs = this.convertOnce(state, spec, warnings, errors, ctx);
const result = []
for (const tr of trs) {
if (tr["builtin"] !== undefined) {
const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
result.push(...stable)
} else {
result.push(tr)
}
}
return result;
}
}
class ExpandGroupRewrite extends Conversion<{
rewrite: {
sourceString: string,
into: string[]
}[],
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
} | TagRenderingConfigJson, TagRenderingConfigJson[]> {
private static expandSubTagRenderings = new ExpandTagRendering()
constructor() {
super(
"Converts a rewrite config for tagRenderings into the expanded form"
);
}
convert(state: DesugaringContext, json:
{
rewrite:
{ sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
} | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
if (json["rewrite"] === undefined) {
return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []}
}
let config = <{
rewrite:
{ sourceString: string; into: string[] }[];
renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
}>json;
const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context);
const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result);
const errors = subRenderingsRes.errors;
const warnings = subRenderingsRes.warnings;
const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>()
// The actual rewriting
for (const rewrite of config.rewrite) {
const source = rewrite.sourceString;
for (const target of rewrite.into) {
const groupName = target;
const trs: TagRenderingConfigJson[] = []
for (const tr of subRenderings) {
trs.push(this.prepConfig(source, target, tr))
}
if (rewrittenPerGroup.has(groupName)) {
rewrittenPerGroup.get(groupName).push(...trs)
} else {
rewrittenPerGroup.set(groupName, trs)
}
}
}
// Add questions box for this category
rewrittenPerGroup.forEach((group, groupName) => {
group.push(<TagRenderingConfigJson>{
id: "questions",
group: groupName
})
})
rewrittenPerGroup.forEach((group, _) => {
group.forEach(tr => {
if (tr.id === undefined || tr.id === "") {
errors.push("A tagrendering has an empty ID after expanding the tag")
}
})
})
return {
result: [].concat(...Array.from(rewrittenPerGroup.values())),
errors, warnings
};
}
/* Used for left|right group creation and replacement */
private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
function replaceRecursive(transl: string | any) {
if (typeof transl === "string") {
return transl.replace(keyToRewrite, 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
}
}
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> { export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
@ -822,235 +395,3 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
} }
} }
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"]);
}
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] {
const dependenciesToAdd: LayerConfigJson[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id));
// Verify cross-dependencies
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
do {
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
for (const layerConfig of alreadyLoaded) {
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
dependencies.push(...layerDeps)
}
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
// Their existance is checked elsewhere, so this is fine
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
for (const unmetDependency of unmetDependencies) {
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
continue
}
const dep = allKnownLayers.get(unmetDependency.neededLayer)
if (dep === undefined) {
const message =
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
"This layer is needed by " + unmetDependency.neededBy,
unmetDependency.reason + " (at " + unmetDependency.context + ")",
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
]
throw message.join("\n\t");
}
dependenciesToAdd.unshift(dep)
loadedLayerIds.add(dep.id);
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd;
}
convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
const errors = [];
const warnings = [];
const layers: LayerConfigJson[] = <LayerConfigJson[]> theme.layers; // Layers should be expanded at this point
knownTagRenderings.forEach((value, key) => {
value.id = key;
})
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
if (dependencies.length > 0) {
warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed")
}
layers.unshift(...dependencies);
return {
result: {
...theme,
layers: layers
},
errors,
warnings
};
}
}
class SetDefault<T> extends DesugaringStep<T> {
private readonly value: any;
private readonly key: string;
private readonly _overrideEmptyString: boolean;
constructor(key: string, value: any, overrideEmptyString = false) {
super("Sets " + key + " to a default value if undefined");
this.key = key;
this.value = value;
this._overrideEmptyString = overrideEmptyString;
}
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
json = {...json}
json[this.key] = this.value
}
return {
errors: [], warnings: [],
result: json
};
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor() {
super(
"Fully prepares and expands a layer for the LayerConfig.",
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()),
new OnEveryConcat("tagRenderings", new ExpandTagRendering()),
new SetDefault("titleIcons", ["defaults"]),
new OnEveryConcat("titleIcons", new ExpandTagRendering())
);
}
}
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
constructor() {
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []);
}
convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
if (typeof json === "string") {
const found = state.sharedLayers.get(json)
if (found === undefined) {
return {
result: null,
errors: [context + ": The layer with name " + json + " was not found as a builtin layer"],
warnings
}
}
return {
result: [found],
errors, warnings
}
}
if (json["builtin"] !== undefined) {
let names = json["builtin"]
if (typeof names === "string") {
names = [names]
}
const layers = []
for (const name of names) {
const found = Utils.Clone(state.sharedLayers.get(name))
if (found === undefined) {
errors.push(context + ": The layer with name " + json + " was not found as a builtin layer")
continue
}
if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) {
errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`)
}
try {
Utils.Merge(json["override"], found);
layers.push(found)
} catch (e) {
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
}
}
return {
result: layers,
errors, warnings
}
}
return {
result: [json],
errors, warnings
};
}
}
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]);
}
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
json.layers = [...json.layers]
if (json.id === "personal") {
json.layers = []
for (const publicLayer of AllKnownLayouts.AllPublicLayers()) {
const id = publicLayer.id
const config = state.sharedLayers.get(id)
if(Constants.added_by_default.indexOf(id) >= 0){
continue;
}
if(config === undefined){
// This is a layer which is coded within a public theme, not as separate .json
continue
}
json.layers.push(config)
}
const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id)
publicIds.map(id => state.sharedLayers.get(id))
}
for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName)
if (v === undefined) {
errors.push("Default layer " + layerName + " not found")
}
json.layers.push(v)
}
return {
result: json,
errors,
warnings
};
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
constructor() {
super(
"Fully prepares and expands a theme",
new OnEveryConcat("layers", new SubstituteLayer()),
new SetDefault("socialImage", "assets/SocialImage.png", true),
new AddDefaultLayers(),
new AddDependencyLayersToTheme(),
new OnEvery("layers", new PrepareLayer()),
new OnEvery("layers", new AddMiniMap())
);
}
}

View file

@ -0,0 +1,252 @@
import {Conversion, DesugaringContext, Fuse, OnEveryConcat, SetDefault} from "./Conversion";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {Utils} from "../../../Utils";
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
constructor() {
super("Converts a tagRenderingSpec into the full tagRendering", []);
}
convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
return {
result: this.convertUntilStable(state, json, warnings, errors, context),
errors, warnings
};
}
private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] {
if (state.tagRenderings.has(name)) {
return [state.tagRenderings.get(name)]
}
if (name.indexOf(".") >= 0) {
const spl = name.split(".");
const layer = state.sharedLayers.get(spl[0])
if (spl.length === 2 && layer !== undefined) {
const id = spl[1];
const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined)
let matchingTrs: TagRenderingConfigJson[]
if (id === "*") {
matchingTrs = layerTrs
} else if (id.startsWith("*")) {
const id_ = id.substring(1)
matchingTrs = layerTrs.filter(tr => tr.group === id_)
} else {
matchingTrs = layerTrs.filter(tr => tr.id === id)
}
for (let i = 0; i < matchingTrs.length; i++) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
const found = Utils.Clone(matchingTrs[i]);
if (found.condition === undefined) {
found.condition = layer.source.osmTags
} else {
found.condition = {and: [found.condition, layer.source.osmTags]}
}
matchingTrs[i] = found
}
if (matchingTrs.length !== 0) {
return matchingTrs
}
}
}
return undefined;
}
private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
if (tr === "questions") {
return [{
id: "questions"
}]
}
if (typeof tr === "string") {
const lookup = this.lookup(state, tr);
if (lookup !== undefined) {
return lookup
}
warnings.push(ctx + "A literal rendering was detected: " + tr)
return [{
render: tr,
id: tr.replace(/![a-zA-Z0-9]/g, "")
}]
}
if (tr["builtin"] !== undefined) {
let names = tr["builtin"]
if (typeof names === "string") {
names = [names]
}
for (const key of Object.keys(tr)) {
if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) {
continue
}
errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr))
}
const trs: TagRenderingConfigJson[] = []
for (const name of names) {
const lookup = this.lookup(state, name)
if (lookup === undefined) {
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
continue
}
for (let foundTr of lookup) {
foundTr = Utils.Clone<any>(foundTr)
Utils.Merge(tr["override"] ?? {}, foundTr)
trs.push(foundTr)
}
}
return trs;
}
return [tr]
}
private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
const trs = this.convertOnce(state, spec, warnings, errors, ctx);
const result = []
for (const tr of trs) {
if (tr["builtin"] !== undefined) {
const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
result.push(...stable)
} else {
result.push(tr)
}
}
return result;
}
}
class ExpandGroupRewrite extends Conversion<{
rewrite: {
sourceString: string,
into: string[]
}[],
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
} | TagRenderingConfigJson, TagRenderingConfigJson[]> {
private static expandSubTagRenderings = new ExpandTagRendering()
constructor() {
super(
"Converts a rewrite config for tagRenderings into the expanded form"
);
}
convert(state: DesugaringContext, json:
{
rewrite:
{ sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
} | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
if (json["rewrite"] === undefined) {
return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []}
}
let config = <{
rewrite:
{ sourceString: string; into: string[] }[];
renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
}>json;
const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context);
const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result);
const errors = subRenderingsRes.errors;
const warnings = subRenderingsRes.warnings;
const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>()
// The actual rewriting
for (const rewrite of config.rewrite) {
const source = rewrite.sourceString;
for (const target of rewrite.into) {
const groupName = target;
const trs: TagRenderingConfigJson[] = []
for (const tr of subRenderings) {
trs.push(this.prepConfig(source, target, tr))
}
if (rewrittenPerGroup.has(groupName)) {
rewrittenPerGroup.get(groupName).push(...trs)
} else {
rewrittenPerGroup.set(groupName, trs)
}
}
}
// Add questions box for this category
rewrittenPerGroup.forEach((group, groupName) => {
group.push(<TagRenderingConfigJson>{
id: "questions",
group: groupName
})
})
rewrittenPerGroup.forEach((group, _) => {
group.forEach(tr => {
if (tr.id === undefined || tr.id === "") {
errors.push("A tagrendering has an empty ID after expanding the tag")
}
})
})
return {
result: [].concat(...Array.from(rewrittenPerGroup.values())),
errors, warnings
};
}
/* Used for left|right group creation and replacement */
private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
function replaceRecursive(transl: string | any) {
if (typeof transl === "string") {
return transl.replace(keyToRewrite, 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
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor() {
super(
"Fully prepares and expands a layer for the LayerConfig.",
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()),
new OnEveryConcat("tagRenderings", new ExpandTagRendering()),
new SetDefault("titleIcons", ["defaults"]),
new OnEveryConcat("titleIcons", new ExpandTagRendering())
);
}
}

View file

@ -0,0 +1,316 @@
import {Conversion, DesugaringContext, DesugaringStep, Fuse, OnEvery, OnEveryConcat, SetDefault} from "./Conversion";
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {PrepareLayer} from "./PrepareLayer";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {Utils} from "../../../Utils";
import Constants from "../../Constants";
import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts";
import CreateNoteImportLayer from "./CreateNoteImportLayer";
import LayerConfig from "../LayerConfig";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {Translation} from "../../../UI/i18n/Translation";
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
import DependencyCalculator from "../DependencyCalculator";
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
constructor() {
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []);
}
convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
if (typeof json === "string") {
const found = state.sharedLayers.get(json)
if (found === undefined) {
return {
result: null,
errors: [context + ": The layer with name " + json + " was not found as a builtin layer"],
warnings
}
}
return {
result: [found],
errors, warnings
}
}
if (json["builtin"] !== undefined) {
let names = json["builtin"]
if (typeof names === "string") {
names = [names]
}
const layers = []
for (const name of names) {
const found = Utils.Clone(state.sharedLayers.get(name))
if (found === undefined) {
errors.push(context + ": The layer with name " + json + " was not found as a builtin layer")
continue
}
if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) {
errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`)
}
try {
Utils.Merge(json["override"], found);
layers.push(found)
} catch (e) {
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
}
}
return {
result: layers,
errors, warnings
}
}
return {
result: [json],
errors, warnings
};
}
}
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]);
}
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
json.layers = [...json.layers]
if (json.id === "personal") {
json.layers = []
for (const publicLayer of AllKnownLayouts.AllPublicLayers()) {
const id = publicLayer.id
const config = state.sharedLayers.get(id)
if (Constants.added_by_default.indexOf(id) >= 0) {
continue;
}
if (config === undefined) {
// This is a layer which is coded within a public theme, not as separate .json
continue
}
json.layers.push(config)
}
const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id)
publicIds.map(id => state.sharedLayers.get(id))
}
for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName)
if (v === undefined) {
errors.push("Default layer " + layerName + " not found")
}
json.layers.push(v)
}
return {
result: json,
errors,
warnings
};
}
}
class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"]);
}
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
json = {...json}
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers;
json.layers = [...json.layers]
const creator = new CreateNoteImportLayer()
for (let i1 = 0; i1 < allLayers.length; i1++) {
const layer = allLayers[i1];
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
// Priviliged layers are skipped
continue
}
if (layer.source["geoJson"] !== undefined) {
// Layer which don't get their data from OSM are skipped
continue
}
if (layer.title === undefined || layer.name === undefined) {
// Anonymous layers and layers without popup are skipped
continue
}
if (layer.presets === undefined || layer.presets.length == 0) {
// A preset is needed to be able to generate a new point
continue;
}
try {
const importLayerResult = creator.convert(state, layer, context + ".(noteimportlayer)[" + i1 + "]")
errors.push(...importLayerResult.errors)
warnings.push(...importLayerResult.warnings)
if (importLayerResult.result !== undefined) {
warnings.push("Added an import layer to theme " + json.id + ", namely " + importLayerResult.result.id)
json.layers.push(importLayerResult.result)
}
} catch (e) {
errors.push("Could not generate an import-layer for " + layer.id + " due to " + e)
}
}
return {
errors,
warnings,
result: json
};
}
}
class AddMiniMap extends DesugaringStep<LayerConfigJson> {
constructor() {
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]);
}
/**
* Returns true if this tag rendering has a minimap in some language.
* Note: this minimap can be hidden by conditions
*/
private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
for (const translation of translations) {
for (const key in translation.translations) {
if (!translation.translations.hasOwnProperty(key)) {
continue
}
const template = translation.translations[key]
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
if (hasMiniMap) {
return true;
}
}
}
return false;
}
convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson>tr)) ?? true
if (!hasMinimap) {
layerConfig = {...layerConfig}
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
}
return {
errors: [],
warnings: [],
result: layerConfig
};
}
}
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"]);
}
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] {
const dependenciesToAdd: LayerConfigJson[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id));
// Verify cross-dependencies
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
do {
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
for (const layerConfig of alreadyLoaded) {
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
dependencies.push(...layerDeps)
}
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
// Their existance is checked elsewhere, so this is fine
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
for (const unmetDependency of unmetDependencies) {
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
continue
}
const dep = allKnownLayers.get(unmetDependency.neededLayer)
if (dep === undefined) {
const message =
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
"This layer is needed by " + unmetDependency.neededBy,
unmetDependency.reason + " (at " + unmetDependency.context + ")",
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
]
throw message.join("\n\t");
}
dependenciesToAdd.unshift(dep)
loadedLayerIds.add(dep.id);
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd;
}
convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
const errors = [];
const warnings = [];
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers; // Layers should be expanded at this point
knownTagRenderings.forEach((value, key) => {
value.id = key;
})
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
if (dependencies.length > 0) {
warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed")
}
layers.unshift(...dependencies);
return {
result: {
...theme,
layers: layers
},
errors,
warnings
};
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
constructor() {
super(
"Fully prepares and expands a theme",
new OnEveryConcat("layers", new SubstituteLayer()),
new SetDefault("socialImage", "assets/SocialImage.png", true),
new AddDefaultLayers(),
new AddDependencyLayersToTheme(),
new OnEvery("layers", new PrepareLayer()),
new AddImportLayers(),
new OnEvery("layers", new AddMiniMap())
);
}
}

View file

@ -1,11 +1,11 @@
import {Translation} from "../i18n/Translation";
import Combine from "./Combine"; import Combine from "./Combine";
import Svg from "../../Svg"; import Svg from "../../Svg";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import BaseUIElement from "../BaseUIElement";
export default class Loading extends Combine { export default class Loading extends Combine {
constructor(msg?: Translation | string) { constructor(msg?: BaseUIElement | string) {
const t = Translations.T(msg) ?? Translations.t.general.loading.Clone(); const t = Translations.W(msg) ?? Translations.t.general.loading;
t.SetClass("pl-2") t.SetClass("pl-2")
super([ super([
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"), Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),

View file

@ -37,7 +37,7 @@ export default class Minimap {
/** /**
* Construct a minimap * Construct a minimap
*/ */
public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => { public static createMiniMap: (options?: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => {
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
} }

View file

@ -28,7 +28,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
private readonly _addLayerControl: boolean; private readonly _addLayerControl: boolean;
private readonly _options: MinimapOptions; private readonly _options: MinimapOptions;
private constructor(options: MinimapOptions) { private constructor(options?: MinimapOptions) {
super() super()
options = options ?? {} options = options ?? {}
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined) this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
@ -290,12 +290,6 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
map.setView([loc.lat, loc.lon], loc.zoom) map.setView([loc.lat, loc.lon], loc.zoom)
}) })
location.map(loc => loc.zoom)
.addCallback(zoom => {
if (Math.abs(map.getZoom() - zoom) > 0.1) {
map.setZoom(zoom, {});
}
})
if (self.bounds !== undefined) { if (self.bounds !== undefined) {
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))

View file

@ -43,6 +43,7 @@ export default class SimpleAddUI extends Toggle {
constructor(isShown: UIEventSource<boolean>, constructor(isShown: UIEventSource<boolean>,
filterViewIsOpened: UIEventSource<boolean>, filterViewIsOpened: UIEventSource<boolean>,
state: { state: {
featureSwitchIsTesting: UIEventSource<boolean>,
layoutToUse: LayoutConfig, layoutToUse: LayoutConfig,
osmConnection: OsmConnection, osmConnection: OsmConnection,
changes: Changes, changes: Changes,
@ -155,6 +156,7 @@ export default class SimpleAddUI extends Toggle {
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
state: { state: {
featureSwitchIsTesting: UIEventSource<boolean>;
filteredLayers: UIEventSource<FilteredLayer[]>, filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>, featureSwitchFilter: UIEventSource<boolean>,
osmConnection: OsmConnection osmConnection: OsmConnection
@ -162,10 +164,9 @@ export default class SimpleAddUI extends Toggle {
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
let intro: BaseUIElement = Translations.t.general.add.intro; let intro: BaseUIElement = Translations.t.general.add.intro;
let testMode: BaseUIElement = undefined; let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"),
if (state.osmConnection?.userDetails?.data?.dryRun) { undefined,
testMode = Translations.t.general.testing.Clone().SetClass("alert") state.featureSwitchIsTesting);
}
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")

View file

@ -73,10 +73,11 @@ export default class UserBadge extends Toggle {
).SetClass("alert") ).SetClass("alert")
} }
let dryrun = new FixedUiElement(""); let dryrun = new Toggle(
if (user.dryRun) { new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"),
dryrun = new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"); undefined,
} state.featureSwitchIsTesting
)
const settings = const settings =
new Link(Svg.gear, new Link(Svg.gear,

View file

@ -0,0 +1,100 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {UIEventSource} from "../../Logic/UIEventSource";
import ValidatedTextField from "../Input/ValidatedTextField";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import Title from "../Base/Title";
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
import {DropDown} from "../Input/DropDown";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export class AskMetadata extends Combine implements FlowStep<{
features: any[],
wikilink: string,
intro: string,
source: string,
theme: string
}> {
public readonly Value: UIEventSource<{
features: any[],
wikilink: string,
intro: string,
source: string,
theme: string
}>;
public readonly IsValid: UIEventSource<boolean>;
constructor(params: ({ features: any[], layer: LayerConfig })) {
const introduction = ValidatedTextField.InputForType("text", {
value: LocalStorageSource.Get("import-helper-introduction-text"),
inputStyle: "width: 100%"
})
const wikilink = ValidatedTextField.InputForType("string", {
value: LocalStorageSource.Get("import-helper-wikilink-text"),
inputStyle: "width: 100%"
})
const source = ValidatedTextField.InputForType("string", {
value: LocalStorageSource.Get("import-helper-source-text"),
inputStyle: "width: 100%"
})
let options : {value: string, shown: BaseUIElement}[]= AllKnownLayouts.layoutsList
.filter(th => th.layers.some(l => l.id === params.layer.id))
.filter(th => th.id !== "personal")
.map(th => ({
value: th.id,
shown: th.title
}))
options.splice(0,0, {
shown: new FixedUiElement("Select a theme"),
value: undefined
})
const theme = new DropDown("Which theme should be linked in the note?",options)
ValidatedTextField.InputForType("string", {
value: LocalStorageSource.Get("import-helper-theme-text"),
inputStyle: "width: 100%"
})
super([
new Title("Set metadata"),
"Before adding " + params.features.length + " notes, please provide some extra information.",
"Please, write an introduction for someone who sees the note",
introduction.SetClass("w-full border border-black"),
"What is the source of this data? If 'source' is set in the feature, this value will be ignored",
source.SetClass("w-full border border-black"),
"On what wikipage can one find more information about this import?",
wikilink.SetClass("w-full border border-black"),
theme
]);
this.SetClass("flex flex-col")
this.Value = introduction.GetValue().map(intro => {
return {
features: params.features,
wikilink: wikilink.GetValue().data,
intro,
source: source.GetValue().data,
theme: theme.GetValue().data
}
}, [wikilink.GetValue(), source.GetValue(), theme.GetValue()])
this.IsValid = this.Value.map(obj => {
if(obj === undefined){
return false;
}
return obj.theme !== undefined && obj.features !== undefined && obj.wikilink !== undefined && obj.intro !== undefined && obj.source !== undefined;
})
}
}

View file

@ -0,0 +1,134 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {BBox} from "../../Logic/BBox";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
import {DesugaringContext} from "../../Models/ThemeConfig/Conversion/Conversion";
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource";
import MetaTagging from "../../Logic/MetaTagging";
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ImportUtils} from "./ImportUtils";
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import Title from "../Base/Title";
import Toggle from "../Input/Toggle";
import Loading from "../Base/Loading";
import {FixedUiElement} from "../Base/FixedUiElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import * as known_layers from "../../assets/generated/known_layers.json"
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
/**
* Filters out points for which the import-note already exists, to prevent duplicates
*/
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, geojson: any }> {
public IsValid: UIEventSource<boolean>
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }>
constructor(state, params: { bbox: BBox, layer: LayerConfig, geojson: { features: any[] } }) {
const convertState: DesugaringContext = {
sharedLayers: new Map(),
tagRenderings: new Map()
}
const layerConfig = known_layers.filter(l => l.id === params.layer.id)[0]
const importLayerJson = new CreateNoteImportLayer(365).convertStrict(convertState, <LayerConfigJson> layerConfig, "CompareToAlreadyExistingNotes")
const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic")
const flayer: FilteredLayer = {
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()),
isDisplayed: new UIEventSource<boolean>(true),
layerDef: importLayer
}
const unfiltered = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
unfiltered.features.map(f => MetaTagging.addMetatags(
f,
{
memberships: new RelationsTracker(),
getFeaturesWithin: (layerId, bbox: BBox) => [],
getFeatureById: (id: string) => undefined
},
importLayer,
state,
{
includeDates: true,
// We assume that the non-dated metatags are already set by the cache generator
includeNonDates: true
}
)
)
const data = new FilteringFeatureSource(state, undefined, unfiltered)
data.features.addCallbackD(features => console.log("Loaded and filtered features are", features))
const map = Minimap.createMiniMap()
map.SetClass("w-full").SetStyle("height: 500px")
const comparison = Minimap.createMiniMap({
location: map.location,
})
comparison.SetClass("w-full").SetStyle("height: 500px")
new ShowDataLayer({
layerToShow: importLayer,
state,
zoomToFeatures: true,
leafletMap: map.leafletMap,
features: data,
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
})
const maxDistance = new UIEventSource<number>(5)
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params.geojson, data.features
.map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance)
new ShowDataLayer({
layerToShow: new LayerConfig(import_candidate),
state,
zoomToFeatures: true,
leafletMap: comparison.leafletMap,
features: new StaticFeatureSource(partitionedImportPoints.map(p => p.hasNearby), false),
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state)
})
super([
new Title("Compare with already existing 'to-import'-notes"),
new Toggle(
new Loading("Fetching notes from OSM"),
new Combine([
map,
"The following (red) elements are elements to import which are nearby a matching element that is already up for import. These won't be imported",
new Toggle(
new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem"),
new VariableUiElement(partitionedImportPoints.map(({noNearby}) => noNearby.length + " elements can be imported")).SetClass("thanks p-8"),
partitionedImportPoints.map(({noNearby}) => noNearby.length === 0)
).SetClass("w-full"),
comparison,
]).SetClass("flex flex-col"),
unfiltered.features.map(ff => ff === undefined || ff.length === 0)
),
]);
this.SetClass("flex flex-col")
this.Value = partitionedImportPoints.map(({noNearby}) => ({
geojson: {features: noNearby, type: "FeatureCollection"},
bbox: params.bbox,
layer: params.layer
}))
this.IsValid = data.features.map(ff => ff.length > 0 && partitionedImportPoints.data.noNearby.length > 0, [partitionedImportPoints])
}
}

View file

@ -0,0 +1,32 @@
import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {UIEventSource} from "../../Logic/UIEventSource";
import Link from "../Base/Link";
import {FixedUiElement} from "../Base/FixedUiElement";
import CheckBoxes from "../Input/Checkboxes";
import Title from "../Base/Title";
export class ConfirmProcess<T> extends Combine implements FlowStep<T> {
public IsValid: UIEventSource<boolean>
public Value: UIEventSource<T>
constructor(v: T) {
const toConfirm = [
new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]),
new FixedUiElement("I did contact the (local) community about this import"),
new FixedUiElement("The license of the data to import allows it to be imported into OSM. They are allowed to be redistributed commercially, with only minimal attribution"),
new FixedUiElement("The process is documented on the OSM-wiki (you'll need this link later)")
];
const licenseClear = new CheckBoxes(toConfirm)
super([
new Title("Did you go through the import process?"),
licenseClear
]);
this.SetClass("link-underline")
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
this.Value = new UIEventSource<T>(v)
}
}

View file

@ -27,10 +27,12 @@ import * as currentview from "../../assets/layers/current_view/current_view.json
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import FeatureInfoBox from "../Popup/FeatureInfoBox"; import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ImportUtils} from "./ImportUtils";
/** /**
* Given the data to import, the bbox and the layer, will query overpass for similar items * Given the data to import, the bbox and the layer, will query overpass for similar items
*/ */
export default class ConflationChecker extends Combine implements FlowStep<any> { export default class ConflationChecker extends Combine implements FlowStep<{features: any[], layer: LayerConfig}> {
public readonly IsValid public readonly IsValid
public readonly Value public readonly Value
@ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
const layer = params.layer; const layer = params.layer;
const toImport = params.geojson; const toImport = params.geojson;
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle") let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle")
const cacheAge = new UIEventSource<number>(undefined);
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
whenLoaded: (v) => { whenLoaded: (v) => {
if (v !== undefined) { if (v !== undefined) {
console.log("Loaded from local storage:", v) console.log("Loaded from local storage:", v)
const [geojson, date] = v; const [geojson, date] = v;
const timeDiff = (new Date().getTime() - date.getTime()) / 1000; const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
console.log("The cache is ", timeDiff, "seconds old") console.log("Loaded ", geojson.features.length," features; cache is ", timeDiff, "seconds old")
cacheAge.setData(timeDiff)
if (timeDiff < 24 * 60 * 60) { if (timeDiff < 24 * 60 * 60) {
// Recently cached! // Recently cached!
overpassStatus.setData("cached") overpassStatus.setData("cached")
return; return;
} }
cacheAge.setData(-1)
} }
// Load the data! // Load the data!
const url = Constants.defaultOverpassUrls[1] const url = Constants.defaultOverpassUrls[1]
@ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
layerToShow:new LayerConfig(currentview), layerToShow:new LayerConfig(currentview),
state, state,
leafletMap: osmLiveData.leafletMap, leafletMap: osmLiveData.leafletMap,
enablePopups: undefined, popup: undefined,
zoomToFeatures: true, zoomToFeatures: true,
features: new StaticFeatureSource([ features: new StaticFeatureSource([
bbox.asGeoJson({}) bbox.asGeoJson({})
@ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
toImport.features.some(imp => toImport.features.some(imp =>
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
}, [nearbyCutoff.GetValue()]), false); }, [nearbyCutoff.GetValue()]), false);
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
// Featuresource showing OSM-features which are nearby a toImport-feature // Featuresource showing OSM-features which are nearby a toImport-feature
const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => { const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false);
if(osmData?.features === undefined){
return []
}
const maxDist = Number(nearbyCutoff.GetValue().data)
return toImport.features.filter(imp =>
osmData.features.some(f =>
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
}, [nearbyCutoff.GetValue()]), false);
new ShowDataLayer({ new ShowDataLayer({
layerToShow:layer, layerToShow:layer,
@ -192,6 +189,38 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
}) })
const conflationMaps = new Combine([
new VariableUiElement(
geojson.map(geojson => {
if (geojson === undefined) {
return undefined;
}
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
mimetype: "application/json+geo"
})
});
})),
new VariableUiElement(cacheAge.map(age => {
if(age === undefined){
return undefined;
}
if(age < 0){
return new FixedUiElement("Cache was expired")
}
return new FixedUiElement("Loaded data is from the cache and is "+Utils.toHumanTime(age)+" old")
})),
new Title("Live data on OSM"),
osmLiveData,
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
new Title("Nearby features"),
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
"Set the range to 0 or 1 if you want to import them all",
matchedFeaturesMap]).SetClass("flex flex-col")
super([ super([
new Title("Comparison with existing data"), new Title("Comparison with existing data"),
new VariableUiElement(overpassStatus.map(d => { new VariableUiElement(overpassStatus.map(d => {
@ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
return new Loading("Querying overpass...") return new Loading("Querying overpass...")
} }
if(d === "cached"){ if(d === "cached"){
return new FixedUiElement("Fetched data from local storage") return conflationMaps
} }
if(d === "success"){ if(d === "success"){
return new FixedUiElement("Data loaded") return conflationMaps
} }
return new FixedUiElement("Unexpected state "+d).SetClass("alert") return new FixedUiElement("Unexpected state "+d).SetClass("alert")
})), }))
new VariableUiElement(
geojson.map(geojson => {
if (geojson === undefined) {
return undefined;
}
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
mimetype: "application/json+geo"
})
});
})),
new Title("Live data on OSM"),
osmLiveData,
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
new Title("Nearby features"),
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
"Set the range to 0 or 1 if you want to import them all",
matchedFeaturesMap
]) ])
this.IsValid = new UIEventSource(false) this.Value = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer}))
this.Value = new UIEventSource(undefined) this.Value.addCallbackAndRun(v => console.log("ConflationChecker-step value is ", v))
} this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0)
}
} }

View file

@ -0,0 +1,82 @@
import Combine from "../Base/Combine";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {UIEventSource} from "../../Logic/UIEventSource";
import Title from "../Base/Title";
import Toggle from "../Input/Toggle";
import Loading from "../Base/Loading";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import Link from "../Base/Link";
export class CreateNotes extends Combine {
constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) {
const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([])
const failed = new UIEventSource<string[]>([])
const currentNote = createdNotes.map(n => n.length)
for (const f of v.features) {
const src = f.properties["source"] ?? f.properties["src"] ?? v.source
delete f.properties["source"]
delete f.properties["src"]
const tags: string [] = []
for (const key in f.properties) {
if(f.properties[key] === ""){
continue
}
tags.push(key + "=" + f.properties[key].replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
}
const lat = f.geometry.coordinates[1]
const lon = f.geometry.coordinates[0]
const text = [v.intro,
'',
"Source: " + src,
'More information at ' + v.wikilink,
'',
'Import this point easily with',
`https://mapcomplete.osm.be/${v.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
...tags].join("\n")
state.osmConnection.openNote(
lat, lon, text)
.then(({id}) => {
createdNotes.data.push(id)
createdNotes.ping()
}, err => {
failed.data.push(err)
failed.ping()
})
}
super([
new Title("Creating notes"),
"Hang on while we are importing...",
new Toggle(
new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported <b>" + count + "</b> out of " + v.features.length + " notes")))),
new FixedUiElement("All done!"),
currentNote.map(count => count < v.features.length)
),
new VariableUiElement(failed.map(failed => {
if (failed.length === 0) {
return undefined
}
return new Combine([
new FixedUiElement("Some entries failed").SetClass("alert"),
...failed
]).SetClass("flex flex-col")
})),
new VariableUiElement(createdNotes.map(notes => {
const links = notes.map(n =>
new Link(new FixedUiElement("https://openstreetmap.org/note/" + n), "https://openstreetmap.org/note/" + n, true));
return new Combine(links).SetClass("flex flex-col");
}))
])
this.SetClass("flex flex-col");
}
}

View file

@ -21,7 +21,23 @@ import Table from "../Base/Table";
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {FlowStep} from "./FlowStep"; import {FlowStep} from "./FlowStep";
import {Layer} from "leaflet"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import {AllTagsPanel} from "../SpecialVisualizations";
import Title from "../Base/Title";
class PreviewPanel extends ScrollableFullScreen {
constructor(tags, layer) {
super(
_ => new FixedUiElement("Element to import"),
_ => new Combine(["The tags are:",
new AllTagsPanel(tags)
]).SetClass("flex flex-col"),
"element"
);
}
}
/** /**
* Shows the data to import on a map, asks for the correct layer to be selected * Shows the data to import on a map, asks for the correct layer to be selected
@ -36,7 +52,6 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
const t = Translations.t.importHelper; const t = Translations.t.importHelper;
const propertyKeys = new Set<string>() const propertyKeys = new Set<string>()
console.log("Datapanel input got ", geojson)
for (const f of geojson.features) { for (const f of geojson.features) {
Object.keys(f.properties).forEach(key => propertyKeys.add(key)) Object.keys(f.properties).forEach(key => propertyKeys.add(key))
} }
@ -56,6 +71,7 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
!layer.source.osmTags.matchesProperties(f.properties) !layer.source.osmTags.matchesProperties(f.properties)
) )
if (!mismatched) { if (!mismatched) {
console.log("Autodected layer", layer.id)
layerPicker.GetValue().setData(layer); layerPicker.GetValue().setData(layer);
layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
autodetected.setData(true) autodetected.setData(true)
@ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
map.SetClass("w-full").SetStyle("height: 500px") map.SetClass("w-full").SetStyle("height: 500px")
new ShowDataMultiLayer({ new ShowDataMultiLayer({
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers().map(l => ({ layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers()
.filter(l => l.source.geojsonSource === undefined)
.map(l => ({
layerDef: l, layerDef: l,
isDisplayed: new UIEventSource<boolean>(true), isDisplayed: new UIEventSource<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
}))), }))),
zoomToFeatures: true, zoomToFeatures: true,
features: new StaticFeatureSource(matching, false), features: new StaticFeatureSource(matching, false),
state: {
...state,
filteredLayers: new UIEventSource<FilteredLayer[]>(undefined),
backgroundLayer: background
},
leafletMap: map.leafletMap, leafletMap: map.leafletMap,
popup: (tag, layer) => new PreviewPanel(tag, layer).SetClass("font-lg")
}) })
var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates]))))
super([ super([
"Has " + geojson.features.length + " features", new Title(geojson.features.length + " features to import"),
layerPicker, layerPicker,
new Toggle("Automatically detected layer", undefined, autodetected), new Toggle("Automatically detected layer", undefined, autodetected),
new Table(["", "Key", "Values", "Unique values seen"], new Table(["", "Key", "Values", "Unique values seen"],

View file

@ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import {UIElement} from "../UIElement"; import {UIElement} from "../UIElement";
export interface FlowStep<T> extends BaseUIElement{ export interface FlowStep<T> extends BaseUIElement {
readonly IsValid: UIEventSource<boolean> readonly IsValid: UIEventSource<boolean>
readonly Value: UIEventSource<T> readonly Value: UIEventSource<T>
} }
@ -16,70 +16,97 @@ export interface FlowStep<T> extends BaseUIElement{
export class FlowPanelFactory<T> { export class FlowPanelFactory<T> {
private _initial: FlowStep<any>; private _initial: FlowStep<any>;
private _steps: ((x: any) => FlowStep<any>)[]; private _steps: ((x: any) => FlowStep<any>)[];
private _stepNames: string[]; private _stepNames: (string | BaseUIElement)[];
private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) { private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) {
this._initial = initial; this._initial = initial;
this._steps = steps; this._steps = steps;
this._stepNames = stepNames; this._stepNames = stepNames;
} }
public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{ public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
return new FlowPanelFactory(step, [], []) return new FlowPanelFactory(step, [], [name])
} }
public then<TOut>(name: string, construct: ((t:T) => FlowStep<TOut>)): FlowPanelFactory<TOut>{ public then<TOut>(name: string | BaseUIElement, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
return new FlowPanelFactory<TOut>( return new FlowPanelFactory<TOut>(
this._initial, this._initial,
this._steps.concat([construct]), this._steps.concat([construct]),
this._stepNames.concat([name]) this._stepNames.concat([name])
) )
} }
public finish(construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)) : BaseUIElement { public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): {
flow: BaseUIElement,
furthestStep: UIEventSource<number>,
titles: (string | BaseUIElement)[]
} {
const furthestStep = new UIEventSource(0)
// Construct all the flowpanels step by step (in reverse order) // Construct all the flowpanels step by step (in reverse order)
const nextConstr : ((t:any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined) const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
nextConstr.push(construct) nextConstr.push(construct)
for (let i = this._steps.length - 1; i >= 0; i--) {
for (let i = this._steps.length - 1; i >= 0; i--){ const createFlowStep: (value) => FlowStep<any> = this._steps[i];
const createFlowStep : (value) => FlowStep<any> = this._steps[i]; const isConfirm = i == this._steps.length - 1;
nextConstr[i] = (value, backButton) => { nextConstr[i] = (value, backButton) => {
console.log("Creating flowSTep ", this._stepNames[i])
const flowStep = createFlowStep(value) const flowStep = createFlowStep(value)
return new FlowPanel(flowStep, nextConstr[i + 1], backButton); furthestStep.setData(i + 1);
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm);
panel.isActive.addCallbackAndRun(active => {
if (active) {
furthestStep.setData(i + 1);
}
})
return panel
} }
} }
return new FlowPanel(this._initial, nextConstr[0],undefined) const flow = new FlowPanel(this._initial, nextConstr[0])
flow.isActive.addCallbackAndRun(active => {
if (active) {
furthestStep.setData(0);
}
})
return {
flow,
furthestStep,
titles: this._stepNames
}
} }
} }
export class FlowPanel<T> extends Toggle { export class FlowPanel<T> extends Toggle {
public isActive: UIEventSource<boolean>
constructor( constructor(
initial: (FlowStep<T>), initial: (FlowStep<T>),
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
backbutton?: BaseUIElement backbutton?: BaseUIElement,
isConfirm = false
) { ) {
const t = Translations.t.general; const t = Translations.t.general;
const currentStepActive = new UIEventSource(true); const currentStepActive = new UIEventSource(true);
let nextStep: UIEventSource<BaseUIElement>= new UIEventSource<BaseUIElement>(undefined) let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined)
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => { const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
currentStepActive.setData(true) currentStepActive.setData(true)
}) })
let elements : (BaseUIElement | string)[] = [] let elements: (BaseUIElement | string)[] = []
if(initial !== undefined){ if (initial !== undefined) {
// Startup the flow // Startup the flow
elements = [ elements = [
initial, initial,
new Combine([ new Combine([
backbutton, backbutton,
new Toggle( new Toggle(
new SubtleButton(Svg.back_svg().SetStyle("transform: rotate(180deg);"), t.next).onClick(() => { new SubtleButton(
isConfirm ? Svg.checkmark_svg() :
Svg.back_svg().SetStyle("transform: rotate(180deg);"),
isConfirm ? t.confirm : t.next
).onClick(() => {
const v = initial.Value.data; const v = initial.Value.data;
nextStep.setData(constructNextstep(v, backButtonForNextStep)) nextStep.setData(constructNextstep(v, backButtonForNextStep))
currentStepActive.setData(false) currentStepActive.setData(false)
@ -88,18 +115,18 @@ export class FlowPanel<T> extends Toggle {
initial.IsValid initial.IsValid
) )
]).SetClass("flex w-full justify-end space-x-2") ]).SetClass("flex w-full justify-end space-x-2")
] ]
} }
super( super(
new Combine(elements).SetClass("h-full flex flex-col justify-between"), new Combine(elements).SetClass("h-full flex flex-col justify-between"),
new VariableUiElement(nextStep), new VariableUiElement(nextStep),
currentStepActive currentStepActive
); );
this.isActive = currentStepActive
} }
} }

View file

@ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen";
import MinimapImplementation from "../Base/MinimapImplementation"; import MinimapImplementation from "../Base/MinimapImplementation";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import {FlowPanel, FlowPanelFactory} from "./FlowStep"; import {FlowPanelFactory} from "./FlowStep";
import {RequestFile} from "./RequestFile"; import {RequestFile} from "./RequestFile";
import {DataPanel} from "./DataPanel"; import {DataPanel} from "./DataPanel";
import {FixedUiElement} from "../Base/FixedUiElement";
import ConflationChecker from "./ConflationChecker"; import ConflationChecker from "./ConflationChecker";
import {AskMetadata} from "./AskMetadata";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {ConfirmProcess} from "./ConfirmProcess";
import {CreateNotes} from "./CreateNotes";
import {FixedUiElement} from "../Base/FixedUiElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import List from "../Base/List";
import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes";
export default class ImportHelperGui extends LoginToggle { export default class ImportHelperGui extends LoginToggle {
constructor() { constructor() {
@ -24,28 +31,51 @@ export default class ImportHelperGui extends LoginToggle {
// We disable the userbadge, as various 'showData'-layers will give a read-only view in this case // We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
state.featureSwitchUserbadge.setData(false) state.featureSwitchUserbadge.setData(false)
const {flow, furthestStep, titles} =
FlowPanelFactory
.start("Select file", new RequestFile())
.then("Inspect data", geojson => new DataPanel(state, geojson))
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
.then("Compare with existing data", v => new ConflationChecker(state, v))
.then("License and community check", v => new ConfirmProcess(v))
.then("Metadata", (v:{features:any[], layer: LayerConfig}) => new AskMetadata(v))
.finish("Note creation", v => new CreateNotes(state, v));
const toc = new List(
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
if(i > currentStep){
return new Combine([title]).SetClass("subtle");
}
if(i == currentStep){
return new Combine([title]).SetClass("font-bold");
}
if(i < currentStep){
return title
}
})))
, true)
const leftContents: BaseUIElement[] = [ const leftContents: BaseUIElement[] = [
new BackToIndex().SetClass("block pl-4"), new BackToIndex().SetClass("block pl-4"),
toc,
new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting),
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
].map(el => el?.SetClass("pl-4")) ].map(el => el?.SetClass("pl-4"))
const leftBar = new Combine([ const leftBar = new Combine([
new Combine(leftContents).SetClass("sticky top-4 m-4") new Combine(leftContents).SetClass("sticky top-4 m-4"),
]).SetClass("block w-full md:w-2/6 lg:w-1/6") ]).SetClass("block w-full md:w-2/6 lg:w-1/6")
const mainPanel =
FlowPanelFactory
.start(new RequestFile())
.then("datapanel", geojson => new DataPanel(state, geojson))
.then("conflation", v => new ConflationChecker(state, v))
.finish(_ => new FixedUiElement("All done!"))
super( super(
new Toggle( new Toggle(
new Combine([ new Combine([
leftBar, leftBar,
mainPanel.SetClass("m-8 w-full mb-24") flow.SetClass("m-8 w-full mb-24")
]).SetClass("h-full block md:flex") ]).SetClass("h-full block md:flex")
, ,

View file

@ -0,0 +1,28 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {GeoOperations} from "../../Logic/GeoOperations";
export class ImportUtils {
public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: UIEventSource<{ features: any[] }>, cutoffDistanceInMeters: UIEventSource<number>): UIEventSource<{ hasNearby: any[], noNearby: any[] }> {
return compareWith.map(osmData => {
if (osmData?.features === undefined) {
return undefined
}
const maxDist = cutoffDistanceInMeters.data
const hasNearby = []
const noNearby = []
for (const toImportElement of toPartitionFeatureCollection.features) {
const hasNearbyFeature = osmData.features.some(f =>
maxDist >= GeoOperations.distanceBetween(toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))
if (hasNearbyFeature) {
hasNearby.push(toImportElement)
} else {
noNearby.push(toImportElement)
}
}
return {hasNearby, noNearby}
}, [cutoffDistanceInMeters]);
}
}

View file

@ -504,7 +504,8 @@ export default class ValidatedTextField {
mapBackgroundLayer?: UIEventSource<any>, mapBackgroundLayer?: UIEventSource<any>,
unit?: Unit, unit?: Unit,
args?: (string | number | boolean)[] // Extra arguments for the inputHelper, args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
feature?: any feature?: any,
inputStyle?: string
}): InputElement<string> { }): InputElement<string> {
options = options ?? {}; options = options ?? {};
options.placeholder = options.placeholder ?? type; options.placeholder = options.placeholder ?? type;

View file

@ -19,6 +19,7 @@ export default class ConfirmLocationOfPoint extends Combine {
constructor( constructor(
state: { state: {
featureSwitchIsTesting: UIEventSource<boolean>;
osmConnection: OsmConnection, osmConnection: OsmConnection,
featurePipeline: FeaturePipeline, featurePipeline: FeaturePipeline,
backgroundLayer?: UIEventSource<BaseLayer> backgroundLayer?: UIEventSource<BaseLayer>
@ -167,8 +168,11 @@ export default class ConfirmLocationOfPoint extends Combine {
).onClick(cancel) ).onClick(cancel)
super([ super([
state.osmConnection.userDetails.data.dryRun ? new Toggle(
Translations.t.general.testing.Clone().SetClass("alert") : undefined, Translations.t.general.testing.SetClass("alert"),
undefined,
state.featureSwitchIsTesting
),
disableFiltersOrConfirm, disableFiltersOrConfirm,
cancelButton, cancelButton,
preset.description, preset.description,

View file

@ -141,7 +141,7 @@ ${Utils.special_visualizations_importRequirementDocs}
if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){ if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){
// This is probably a key // This is probably a key
tagSpec = tagSource.data[args.tags] tagSpec = tagSource.data[args.tags]
console.warn("Using tagspec tagSource.data["+args.tags+"] which is ",tagSpec) console.debug("The import button is using tags from properties["+args.tags+"] of this object, namely ",tagSpec)
} }
const importClicked = new UIEventSource(false); const importClicked = new UIEventSource(false);
@ -201,7 +201,7 @@ ${Utils.special_visualizations_importRequirementDocs}
if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){ if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){
// This might be a property to expand... // This might be a property to expand...
const items : string = originalFeatureTags.data[tags] const items : string = originalFeatureTags.data[tags]
console.warn("Using tagspec tagSource.data["+tags+"] which is ",items) console.debug("The import button is using tags from properties["+tags+"] of this object, namely ",items)
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags) baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
}else{ }else{
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)

View file

@ -55,6 +55,45 @@ export interface SpecialVisualization {
getLayerDependencies?: (argument: string[]) => string[] getLayerDependencies?: (argument: string[]) => string[]
} }
export class AllTagsPanel extends VariableUiElement {
constructor(tags: UIEventSource<any>, state?) {
const calculatedTags = [].concat(
SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? []))
super(tags.map(tags => {
const parts = [];
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"]);
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
parts.push(["<i>" + key + "</i>", value])
}
return new Table(
["key", "value"],
parts
)
.SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table")
}))
}
}
export default class SpecialVisualizations { export default class SpecialVisualizations {
public static specialVisualizations = SpecialVisualizations.init() public static specialVisualizations = SpecialVisualizations.init()
@ -99,37 +138,7 @@ export default class SpecialVisualizations {
funcName: "all_tags", funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging", docs: "Prints all key-value pairs of the object - used for debugging",
args: [], args: [],
constr: ((state, tags: UIEventSource<any>) => { constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state))
const calculatedTags = [].concat(
SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? []))
return new VariableUiElement(tags.map(tags => {
const parts = [];
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"]);
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
parts.push(["<i>" + key + "</i>", value])
}
return new Table(
["key", "value"],
parts
)
})).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table")
})
}, },
{ {
funcName: "image_carousel", funcName: "image_carousel",
@ -339,7 +348,7 @@ export default class SpecialVisualizations {
const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat),
encodeURIComponent(subject), encodeURIComponent(subject),
state.mangroveIdentity, state.mangroveIdentity,
state.osmConnection._dryRun state.featureSwitchIsTesting.data
); );
const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection);
return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form);
@ -743,10 +752,6 @@ export default class SpecialVisualizations {
return t.addCommentAndClose return t.addCommentAndClose
}))).onClick(() => { }))).onClick(() => {
const id = tags.data[args[1] ?? "id"] const id = tags.data[args[1] ?? "id"]
if (state.featureSwitchIsTesting.data) {
console.log("Testmode: Not actually closing note...")
return;
}
state.osmConnection.closeNote(id, txt.data).then(_ => { state.osmConnection.closeNote(id, txt.data).then(_ => {
tags.data["closed_at"] = new Date().toISOString(); tags.data["closed_at"] = new Date().toISOString();
tags.ping() tags.ping()
@ -760,10 +765,6 @@ export default class SpecialVisualizations {
return t.reopenNoteAndComment return t.reopenNoteAndComment
}))).onClick(() => { }))).onClick(() => {
const id = tags.data[args[1] ?? "id"] const id = tags.data[args[1] ?? "id"]
if (state.featureSwitchIsTesting.data) {
console.log("Testmode: Not actually reopening note...")
return;
}
state.osmConnection.reopenNote(id, txt.data).then(_ => { state.osmConnection.reopenNote(id, txt.data).then(_ => {
tags.data["closed_at"] = undefined; tags.data["closed_at"] = undefined;
tags.ping() tags.ping()

View file

@ -6,7 +6,7 @@
"description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)", "description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)",
"source": { "source": {
"osmTags": "id~*", "osmTags": "id~*",
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}", "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=7&bbox={x_min},{y_min},{x_max},{y_max}",
"geoJsonZoomLevel": 12, "geoJsonZoomLevel": 12,
"maxCacheAge": 0 "maxCacheAge": 0
}, },
@ -29,7 +29,8 @@
"_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined",
"_first_user:=feat.get('comments')[0].user", "_first_user:=feat.get('comments')[0].user",
"_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()", "_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()",
"_first_user_id:=feat.get('comments')[0].uid" "_first_user_id:=feat.get('comments')[0].uid",
"_is_import_note:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()"
], ],
"titleIcons": [ "titleIcons": [
{ {
@ -201,6 +202,17 @@
} }
} }
] ]
},
{
"id": "no_imports",
"options": [
{
"osmTags": "_is_import_note=",
"question": {
"en": "Hide import notes"
}
}
]
} }
] ]
} }

View file

@ -1493,6 +1493,10 @@ video {
padding: 0.125rem; padding: 0.125rem;
} }
.p-8 {
padding: 2rem;
}
.pb-12 { .pb-12 {
padding-bottom: 3rem; padding-bottom: 3rem;
} }

View file

@ -465,7 +465,9 @@
"importLayer": { "importLayer": {
"layerName": "Possible {title}", "layerName": "Possible {title}",
"description": "A layer which imports entries for {title}", "description": "A layer which imports entries for {title}",
"popupTitle": "Possible {title}" "popupTitle": "Possible {title}",
"importButton": "import_button({layerId}, _tags, There might be a {title} here,./assets/svg/addSmall.svg,,,id)",
"importHandled": "<div class='thanks'>This feature has been handled! Thanks for your effort</div>"
}, },
"importHelper": { "importHelper": {
"title": "Import helper", "title": "Import helper",
@ -477,5 +479,5 @@
"selectLayer": "Select a layer...", "selectLayer": "Select a layer...",
"selectFileTitle": "Select file", "selectFileTitle": "Select file",
"validateDataTitle": "Validate data" "validateDataTitle": "Validate data"
} }
} }

View file

@ -3354,6 +3354,13 @@
"question": "Only show open notes" "question": "Only show open notes"
} }
} }
},
"8": {
"options": {
"0": {
"question": "Hide import notes"
}
}
} }
}, },
"name": "OpenStreetMap notes", "name": "OpenStreetMap notes",

View file

@ -314,5 +314,12 @@
}, },
"multi_apply": { "multi_apply": {
"autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten." "autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten."
},
"importLayer": {
"layerName": "Hier is misschien een {title}",
"description": "Deze laag toont kaart-nota's die wijzen op een {title}",
"popupTitle": "Mogelijkse {title}",
"importButton": "import_button({layerId}, _tags, Hier is een {title}, voeg toe...,./assets/svg/addSmall.svg,,,id)",
"importHandled": "<div class='thanks'>Dit punt is afgehandeld. Bedankt om mee te helpen!</div>"
} }
} }

View file

@ -5,9 +5,6 @@ import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import Constants from "../Models/Constants"; import Constants from "../Models/Constants";
import { import {
DesugaringContext,
PrepareLayer,
PrepareTheme,
ValidateLayer, ValidateLayer,
ValidateThemeAndLayers ValidateThemeAndLayers
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
@ -16,6 +13,9 @@ import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingCon
import * as questions from "../assets/tagRenderings/questions.json"; import * as questions from "../assets/tagRenderings/questions.json";
import * as icons from "../assets/tagRenderings/icons.json"; import * as icons from "../assets/tagRenderings/icons.json";
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"; import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson";
import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer";
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion";
// This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files. // This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them

View file

@ -1,10 +1,11 @@
import T from "./TestHelper"; import T from "./TestHelper";
import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json" import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json"
import {DesugaringContext, PrepareLayer} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer";
export default class CreateNoteImportLayerSpec extends T { export default class CreateNoteImportLayerSpec extends T {
@ -17,7 +18,7 @@ export default class CreateNoteImportLayerSpec extends T {
} }
const layerPrepare = new PrepareLayer() const layerPrepare = new PrepareLayer()
const layer = new LayerConfig(layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases"), "ImportLayerGeneratorTest: init bookcases-layer") const layer =layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases")
const generator = new CreateNoteImportLayer() const generator = new CreateNoteImportLayer()
const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert") const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert")
// fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, " "), "utf8") // fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, " "), "utf8")

View file

@ -112,6 +112,12 @@ export default class TagSpec extends T {
equal(compare.matchesProperties({"key": "5"}), true); equal(compare.matchesProperties({"key": "5"}), true);
equal(compare.matchesProperties({"key": "4.2"}), false); equal(compare.matchesProperties({"key": "4.2"}), false);
const importMatch = TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)")
equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}), true)
equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase"}), true)
equal(importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}), true)
equal(importMatch.matchesProperties({"tags": "amenity=bench"}), false)
})], })],
["Is equivalent test", (() => { ["Is equivalent test", (() => {

View file

@ -3,10 +3,10 @@ import * as assert from "assert";
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json" import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json"
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import Constants from "../Models/Constants"; import Constants from "../Models/Constants";
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
export default class ThemeSpec extends T { export default class ThemeSpec extends T {
constructor() { constructor() {