forked from MapComplete/MapComplete
Finish importer, add applicable import layers to every theme by default
This commit is contained in:
parent
3402ac0954
commit
ca1490902c
41 changed files with 1559 additions and 898 deletions
|
@ -10,11 +10,12 @@ import {UIEventSource} from "./UIEventSource";
|
|||
import {LocalStorageSource} from "./Web/LocalStorageSource";
|
||||
import LZString from "lz-string";
|
||||
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 SharedTagRenderings from "../Customizations/SharedTagRenderings";
|
||||
import * as known_layers from "../assets/generated/known_layers.json"
|
||||
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||
|
||||
export default class DetermineLayout {
|
||||
|
||||
|
|
|
@ -29,11 +29,11 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
},
|
||||
tileIndex,
|
||||
upstream: FeatureSourceForLayer,
|
||||
metataggingUpdated: UIEventSource<any>
|
||||
metataggingUpdated?: UIEventSource<any>
|
||||
) {
|
||||
this.name = "FilteringFeatureSource(" + upstream.name + ")"
|
||||
this.tileIndex = tileIndex
|
||||
this.bbox = BBox.fromTileIndex(tileIndex)
|
||||
this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex)
|
||||
this.upstream = upstream
|
||||
this.state = state
|
||||
|
||||
|
@ -55,7 +55,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
}
|
||||
})
|
||||
|
||||
metataggingUpdated.addCallback(_ => {
|
||||
metataggingUpdated?.addCallback(_ => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
|
||||
|
@ -63,6 +63,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
}
|
||||
|
||||
private update() {
|
||||
console.log("FIltering", this.upstream.name)
|
||||
const self = this;
|
||||
const layer = this.upstream.layer;
|
||||
const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []);
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
private readonly featureIdBlacklist?: UIEventSource<Set<string>>
|
||||
|
||||
public constructor(flayer: FilteredLayer,
|
||||
zxy?: [number, number, number],
|
||||
zxy?: [number, number, number] | BBox,
|
||||
options?: {
|
||||
featureIdBlacklist?: UIEventSource<Set<string>>
|
||||
}) {
|
||||
|
@ -41,23 +41,32 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
this.featureIdBlacklist = options?.featureIdBlacklist
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||
if (zxy !== undefined) {
|
||||
const [z, x, y] = zxy;
|
||||
let tile_bbox = BBox.fromTile(z, x, y)
|
||||
let tile_bbox: BBox;
|
||||
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
|
||||
if (this.layer.layerDef.source.mercatorCrs) {
|
||||
bounds = tile_bbox.toMercator()
|
||||
}
|
||||
|
||||
url = url
|
||||
.replace('{z}', "" + z)
|
||||
.replace('{x}', "" + x)
|
||||
.replace('{y}', "" + y)
|
||||
.replace('{y_min}', "" + bounds.minLat)
|
||||
.replace('{y_max}', "" + bounds.maxLat)
|
||||
.replace('{x_min}', "" + bounds.minLon)
|
||||
.replace('{x_max}', "" + bounds.maxLon)
|
||||
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
|
||||
} else {
|
||||
this.tileIndex = Tiles.tile_index(0, 0, 0)
|
||||
this.bbox = BBox.global;
|
||||
|
@ -83,7 +92,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
|||
if (self.layer.layerDef.source.mercatorCrs) {
|
||||
json = GeoOperations.GeoJsonToWGS84(json)
|
||||
}
|
||||
|
||||
|
||||
const time = new Date();
|
||||
const newFeatures: { feature: any, freshness: Date } [] = []
|
||||
let i = 0;
|
||||
|
|
|
@ -683,6 +683,8 @@ export class GeoOperations {
|
|||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,13 +18,13 @@ export class ChangesetHandler {
|
|||
private readonly allElements: ElementStorage;
|
||||
private osmConnection: OsmConnection;
|
||||
private readonly changes: Changes;
|
||||
private readonly _dryRun: boolean;
|
||||
private readonly _dryRun: UIEventSource<boolean>;
|
||||
private readonly userDetails: UIEventSource<UserDetails>;
|
||||
private readonly auth: any;
|
||||
private readonly backend: string;
|
||||
|
||||
constructor(layoutName: string,
|
||||
dryRun: boolean,
|
||||
dryRun: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
|
@ -67,7 +67,7 @@ export class ChangesetHandler {
|
|||
this.userDetails.data.csCount = 1;
|
||||
this.userDetails.ping();
|
||||
}
|
||||
if (this._dryRun) {
|
||||
if (this._dryRun.data) {
|
||||
const changesetXML = generateChangeXML(123456);
|
||||
console.log("Metatags are", extraMetaTags)
|
||||
console.log(changesetXML);
|
||||
|
|
|
@ -19,7 +19,6 @@ export default class UserDetails {
|
|||
public img: string;
|
||||
public unreadMessages = 0;
|
||||
public totalMessages = 0;
|
||||
public dryRun: boolean;
|
||||
home: { lon: number; lat: number };
|
||||
public backend: string;
|
||||
|
||||
|
@ -47,7 +46,6 @@ export class OsmConnection {
|
|||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public isLoggedIn: UIEventSource<boolean>
|
||||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
public readonly _oauth_config: {
|
||||
|
@ -55,6 +53,7 @@ export class OsmConnection {
|
|||
oauth_secret: string,
|
||||
url: string
|
||||
};
|
||||
private readonly _dryRun: UIEventSource<boolean>;
|
||||
private fakeUser: boolean;
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
private readonly _iframeMode: Boolean | boolean;
|
||||
|
@ -62,7 +61,7 @@ export class OsmConnection {
|
|||
private isChecking = false;
|
||||
|
||||
constructor(options: {
|
||||
dryRun?: false | boolean,
|
||||
dryRun?: UIEventSource<boolean>,
|
||||
fakeUser?: false | boolean,
|
||||
allElements: ElementStorage,
|
||||
changes: Changes,
|
||||
|
@ -82,7 +81,6 @@ export class OsmConnection {
|
|||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||
|
||||
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) {
|
||||
const ud = this.userDetails.data;
|
||||
ud.csCount = 5678
|
||||
|
@ -99,13 +97,13 @@ export class OsmConnection {
|
|||
self.AttemptLogin()
|
||||
}
|
||||
});
|
||||
this._dryRun = options.dryRun;
|
||||
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
|
||||
|
||||
this.updateAuthObject();
|
||||
|
||||
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) {
|
||||
console.log(options.oauth_token.data)
|
||||
const self = this;
|
||||
|
@ -223,7 +221,7 @@ export class OsmConnection {
|
|||
if ((text ?? "") !== "") {
|
||||
textSuffix = "?text=" + encodeURIComponent(text)
|
||||
}
|
||||
if (this._dryRun) {
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
||||
return new Promise((ok, error) => {
|
||||
ok()
|
||||
|
@ -246,7 +244,7 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
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)
|
||||
return new Promise((ok, error) => {
|
||||
ok()
|
||||
|
@ -273,10 +271,10 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
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)
|
||||
return new Promise((ok, error) => {
|
||||
ok()
|
||||
return new Promise<{ id: number }>((ok, error) => {
|
||||
window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
|
||||
});
|
||||
}
|
||||
const auth = this.auth;
|
||||
|
@ -285,15 +283,18 @@ export class OsmConnection {
|
|||
auth.xhr({
|
||||
method: 'POST',
|
||||
path: `/api/0.6/notes.json`,
|
||||
options: {header:
|
||||
{'Content-Type': 'application/json'}},
|
||||
options: {
|
||||
header:
|
||||
{'Content-Type': 'application/json'}
|
||||
},
|
||||
content: JSON.stringify(content)
|
||||
|
||||
}, function (err, response) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
const id = Number(response.children[0].children[0].children.item("id").innerHTML)
|
||||
|
||||
const id = response.properties.id
|
||||
console.log("OPENED NOTE", id)
|
||||
ok({id})
|
||||
}
|
||||
|
@ -304,7 +305,7 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
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)
|
||||
return new Promise((ok, error) => {
|
||||
ok()
|
||||
|
@ -317,7 +318,7 @@ export class OsmConnection {
|
|||
return new Promise((ok, error) => {
|
||||
this.auth.xhr({
|
||||
method: 'POST',
|
||||
|
||||
|
||||
path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}`
|
||||
}, function (err, response) {
|
||||
if (err !== null) {
|
||||
|
|
|
@ -43,7 +43,7 @@ export default class UserRelatedState extends ElementsState {
|
|||
|
||||
this.osmConnection = new OsmConnection({
|
||||
changes: this.changes,
|
||||
dryRun: this.featureSwitchIsTesting.data,
|
||||
dryRun: this.featureSwitchIsTesting,
|
||||
fakeUser: this.featureSwitchFakeUser.data,
|
||||
allElements: this.allElements,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
|
|
|
@ -10,6 +10,13 @@ export class RegexTag extends TagsFilter {
|
|||
constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
|
||||
super();
|
||||
this.key = key;
|
||||
if (typeof value === "string") {
|
||||
if (value.indexOf("^") < 0 && value.indexOf("$") < 0) {
|
||||
value = "^" + value + "$"
|
||||
}
|
||||
value = new RegExp(value)
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.invert = invert;
|
||||
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)
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
AsJson() {
|
||||
return this.asHumanString()
|
||||
}
|
||||
|
|
|
@ -192,16 +192,16 @@ export class TagUtils {
|
|||
}
|
||||
|
||||
const f = (value: string | undefined) => {
|
||||
if(value === undefined){
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
let b = Number(value?.trim() )
|
||||
let b = Number(value?.trim())
|
||||
if (isNaN(b)) {
|
||||
if(value.endsWith(" UTC")) {
|
||||
if (value.endsWith(" UTC")) {
|
||||
value = value.replace(" UTC", "+00")
|
||||
}
|
||||
b = new Date(value).getTime()
|
||||
if(isNaN(b)){
|
||||
if (isNaN(b)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -218,7 +218,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
new RegExp("^" + split[1] + "$"),
|
||||
split[1],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
@ -228,8 +228,8 @@ export class TagUtils {
|
|||
split[1] = "..*"
|
||||
}
|
||||
return new RegexTag(
|
||||
new RegExp("^" + split[0] + "$"),
|
||||
new RegExp("^" + split[1] + "$")
|
||||
split[0],
|
||||
split[1]
|
||||
);
|
||||
}
|
||||
if (tag.indexOf("!:=") >= 0) {
|
||||
|
@ -248,7 +248,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
new RegExp("^" + split[1] + "$"),
|
||||
new RegExp("^" + split[1] + "$"),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
new RegExp("^" + split[1] + "$"),
|
||||
split[1],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
new RegExp("^" + split[1] + "$")
|
||||
split[1]
|
||||
);
|
||||
}
|
||||
if (tag.indexOf("=") >= 0) {
|
||||
|
|
171
Models/ThemeConfig/Conversion/Conversion.ts
Normal file
171
Models/ThemeConfig/Conversion/Conversion.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,29 +1,69 @@
|
|||
import {Conversion, DesugaringContext} from "./LegacyJsonConvert";
|
||||
import {Conversion, DesugaringContext} from "./Conversion";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
|
||||
|
||||
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([
|
||||
"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",
|
||||
].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 warnings = []
|
||||
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 = {
|
||||
"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,
|
||||
"source": {
|
||||
"osmTags": {
|
||||
|
@ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
"id~*"
|
||||
]
|
||||
},
|
||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"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": 10,
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"minzoom": 10,
|
||||
"minzoom": 12,
|
||||
"title": {
|
||||
"render": t.popupTitle.Subs({title: layer.presets[0].title}).translations
|
||||
},
|
||||
"calculatedTags": [
|
||||
"_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] })()",
|
||||
"_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('');})()",
|
||||
"_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(';');})()"
|
||||
"_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] })()",
|
||||
"_comments_count=feat.get('comments').length",
|
||||
"_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": {
|
||||
"render": "no",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "comments!~.*https://mapcomplete.osm.be.*",
|
||||
"then":"no"
|
||||
},
|
||||
{
|
||||
"if": {and:
|
||||
["_trigger_index~*",
|
||||
{or: possibleTags.map(tf => tf.AsJson())}
|
||||
{or: isShownIfAny}
|
||||
]},
|
||||
"then": "yes"
|
||||
}
|
||||
|
@ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "conversation",
|
||||
"render": "{visualize_note_comments(comments,1)}"
|
||||
},
|
||||
{
|
||||
"id": "Intro",
|
||||
"render": "{_intro}"
|
||||
},
|
||||
{
|
||||
"id": "conversation",
|
||||
"render": "{visualize_note_comments(comments,1)}",
|
||||
condition: "_comments_count>1"
|
||||
},
|
||||
{
|
||||
"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_",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
{
|
||||
"id": "add_image",
|
||||
"render": "{add_image_to_note()}"
|
||||
},
|
||||
{
|
||||
id:"alltags",
|
||||
render:"{all_tags()}"
|
||||
}
|
||||
],
|
||||
"mapRendering": [
|
||||
|
@ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
|||
"centroid"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,439 +1,12 @@
|
|||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||
import DependencyCalculator from "../DependencyCalculator";
|
||||
import LayerConfig from "../LayerConfig";
|
||||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import LayoutConfig from "../LayoutConfig";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import Constants from "../../Constants";
|
||||
import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts";
|
||||
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
|
||||
}
|
||||
}
|
||||
import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion";
|
||||
|
||||
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
252
Models/ThemeConfig/Conversion/PrepareLayer.ts
Normal file
252
Models/ThemeConfig/Conversion/PrepareLayer.ts
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
316
Models/ThemeConfig/Conversion/PrepareTheme.ts
Normal file
316
Models/ThemeConfig/Conversion/PrepareTheme.ts
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import {Translation} from "../i18n/Translation";
|
||||
import Combine from "./Combine";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class Loading extends Combine {
|
||||
constructor(msg?: Translation | string) {
|
||||
const t = Translations.T(msg) ?? Translations.t.general.loading.Clone();
|
||||
constructor(msg?: BaseUIElement | string) {
|
||||
const t = Translations.W(msg) ?? Translations.t.general.loading;
|
||||
t.SetClass("pl-2")
|
||||
super([
|
||||
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),
|
||||
|
|
|
@ -37,7 +37,7 @@ export default class 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()"
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
private readonly _addLayerControl: boolean;
|
||||
private readonly _options: MinimapOptions;
|
||||
|
||||
private constructor(options: MinimapOptions) {
|
||||
private constructor(options?: MinimapOptions) {
|
||||
super()
|
||||
options = options ?? {}
|
||||
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)
|
||||
})
|
||||
|
||||
location.map(loc => loc.zoom)
|
||||
.addCallback(zoom => {
|
||||
if (Math.abs(map.getZoom() - zoom) > 0.1) {
|
||||
map.setZoom(zoom, {});
|
||||
}
|
||||
})
|
||||
|
||||
if (self.bounds !== undefined) {
|
||||
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
|
||||
|
|
|
@ -43,6 +43,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
constructor(isShown: UIEventSource<boolean>,
|
||||
filterViewIsOpened: UIEventSource<boolean>,
|
||||
state: {
|
||||
featureSwitchIsTesting: UIEventSource<boolean>,
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
changes: Changes,
|
||||
|
@ -155,6 +156,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
|
||||
state: {
|
||||
featureSwitchIsTesting: UIEventSource<boolean>;
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection
|
||||
|
@ -162,10 +164,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
|
||||
let intro: BaseUIElement = Translations.t.general.add.intro;
|
||||
|
||||
let testMode: BaseUIElement = undefined;
|
||||
if (state.osmConnection?.userDetails?.data?.dryRun) {
|
||||
testMode = Translations.t.general.testing.Clone().SetClass("alert")
|
||||
}
|
||||
let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"),
|
||||
undefined,
|
||||
state.featureSwitchIsTesting);
|
||||
|
||||
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
|
||||
|
||||
|
|
|
@ -73,10 +73,11 @@ export default class UserBadge extends Toggle {
|
|||
).SetClass("alert")
|
||||
}
|
||||
|
||||
let dryrun = new FixedUiElement("");
|
||||
if (user.dryRun) {
|
||||
dryrun = new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4");
|
||||
}
|
||||
let dryrun = new Toggle(
|
||||
new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"),
|
||||
undefined,
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
|
||||
const settings =
|
||||
new Link(Svg.gear,
|
||||
|
|
100
UI/ImportFlow/AskMetadata.ts
Normal file
100
UI/ImportFlow/AskMetadata.ts
Normal 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;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
134
UI/ImportFlow/CompareToAlreadyExistingNotes.ts
Normal file
134
UI/ImportFlow/CompareToAlreadyExistingNotes.ts
Normal 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])
|
||||
}
|
||||
|
||||
}
|
32
UI/ImportFlow/ConfirmProcess.ts
Normal file
32
UI/ImportFlow/ConfirmProcess.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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 {GeoOperations} from "../../Logic/GeoOperations";
|
||||
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
|
||||
*/
|
||||
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 Value
|
||||
|
@ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
const layer = params.layer;
|
||||
const toImport = params.geojson;
|
||||
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, {
|
||||
whenLoaded: (v) => {
|
||||
if (v !== undefined) {
|
||||
console.log("Loaded from local storage:", v)
|
||||
const [geojson, date] = v;
|
||||
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) {
|
||||
// Recently cached!
|
||||
overpassStatus.setData("cached")
|
||||
return;
|
||||
}
|
||||
cacheAge.setData(-1)
|
||||
}
|
||||
// Load the data!
|
||||
const url = Constants.defaultOverpassUrls[1]
|
||||
|
@ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
layerToShow:new LayerConfig(currentview),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
enablePopups: undefined,
|
||||
popup: undefined,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource([
|
||||
bbox.asGeoJson({})
|
||||
|
@ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
toImport.features.some(imp =>
|
||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
||||
}, [nearbyCutoff.GetValue()]), false);
|
||||
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => {
|
||||
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);
|
||||
const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false);
|
||||
|
||||
new ShowDataLayer({
|
||||
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([
|
||||
new Title("Comparison with existing data"),
|
||||
new VariableUiElement(overpassStatus.map(d => {
|
||||
|
@ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
|||
return new Loading("Querying overpass...")
|
||||
}
|
||||
if(d === "cached"){
|
||||
return new FixedUiElement("Fetched data from local storage")
|
||||
return conflationMaps
|
||||
}
|
||||
if(d === "success"){
|
||||
return new FixedUiElement("Data loaded")
|
||||
return conflationMaps
|
||||
}
|
||||
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 = new UIEventSource(undefined)
|
||||
}
|
||||
this.Value = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer}))
|
||||
this.Value.addCallbackAndRun(v => console.log("ConflationChecker-step value is ", v))
|
||||
this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0)
|
||||
}
|
||||
|
||||
}
|
82
UI/ImportFlow/CreateNotes.ts
Normal file
82
UI/ImportFlow/CreateNotes.ts
Normal 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");
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,23 @@ import Table from "../Base/Table";
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
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
|
||||
|
@ -36,7 +52,6 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
const t = Translations.t.importHelper;
|
||||
|
||||
const propertyKeys = new Set<string>()
|
||||
console.log("Datapanel input got ", geojson)
|
||||
for (const f of geojson.features) {
|
||||
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)
|
||||
)
|
||||
if (!mismatched) {
|
||||
console.log("Autodected layer", layer.id)
|
||||
layerPicker.GetValue().setData(layer);
|
||||
layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
|
||||
autodetected.setData(true)
|
||||
|
@ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
|||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
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,
|
||||
isDisplayed: new UIEventSource<boolean>(true),
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
|
||||
}))),
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching, false),
|
||||
state: {
|
||||
...state,
|
||||
filteredLayers: new UIEventSource<FilteredLayer[]>(undefined),
|
||||
backgroundLayer: background
|
||||
},
|
||||
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]))))
|
||||
|
||||
super([
|
||||
"Has " + geojson.features.length + " features",
|
||||
new Title(geojson.features.length + " features to import"),
|
||||
layerPicker,
|
||||
new Toggle("Automatically detected layer", undefined, autodetected),
|
||||
new Table(["", "Key", "Values", "Unique values seen"],
|
||||
|
|
|
@ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import {UIElement} from "../UIElement";
|
||||
|
||||
export interface FlowStep<T> extends BaseUIElement{
|
||||
export interface FlowStep<T> extends BaseUIElement {
|
||||
readonly IsValid: UIEventSource<boolean>
|
||||
readonly Value: UIEventSource<T>
|
||||
}
|
||||
|
@ -16,70 +16,97 @@ export interface FlowStep<T> extends BaseUIElement{
|
|||
export class FlowPanelFactory<T> {
|
||||
private _initial: FlowStep<any>;
|
||||
private _steps: ((x: any) => FlowStep<any>)[];
|
||||
private _stepNames: string[];
|
||||
|
||||
private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) {
|
||||
private _stepNames: (string | BaseUIElement)[];
|
||||
|
||||
private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) {
|
||||
this._initial = initial;
|
||||
this._steps = steps;
|
||||
this._stepNames = stepNames;
|
||||
}
|
||||
|
||||
public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{
|
||||
return new FlowPanelFactory(step, [], [])
|
||||
|
||||
public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
|
||||
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>(
|
||||
this._initial,
|
||||
this._steps.concat([construct]),
|
||||
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)
|
||||
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)
|
||||
|
||||
for (let i = this._steps.length - 1; i >= 0; i--){
|
||||
const createFlowStep : (value) => FlowStep<any> = this._steps[i];
|
||||
for (let i = this._steps.length - 1; i >= 0; i--) {
|
||||
const createFlowStep: (value) => FlowStep<any> = this._steps[i];
|
||||
const isConfirm = i == this._steps.length - 1;
|
||||
nextConstr[i] = (value, backButton) => {
|
||||
console.log("Creating flowSTep ", this._stepNames[i])
|
||||
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 {
|
||||
|
||||
public isActive: UIEventSource<boolean>
|
||||
|
||||
constructor(
|
||||
initial: (FlowStep<T>),
|
||||
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
||||
backbutton?: BaseUIElement
|
||||
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
||||
backbutton?: BaseUIElement,
|
||||
isConfirm = false
|
||||
) {
|
||||
const t = Translations.t.general;
|
||||
|
||||
|
||||
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(() => {
|
||||
currentStepActive.setData(true)
|
||||
})
|
||||
|
||||
let elements : (BaseUIElement | string)[] = []
|
||||
if(initial !== undefined){
|
||||
|
||||
let elements: (BaseUIElement | string)[] = []
|
||||
if (initial !== undefined) {
|
||||
// Startup the flow
|
||||
elements = [
|
||||
initial,
|
||||
new Combine([
|
||||
backbutton,
|
||||
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;
|
||||
nextStep.setData(constructNextstep(v, backButtonForNextStep))
|
||||
currentStepActive.setData(false)
|
||||
|
@ -88,18 +115,18 @@ export class FlowPanel<T> extends Toggle {
|
|||
initial.IsValid
|
||||
)
|
||||
]).SetClass("flex w-full justify-end space-x-2")
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
super(
|
||||
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
|
||||
new VariableUiElement(nextStep),
|
||||
currentStepActive
|
||||
);
|
||||
this.isActive = currentStepActive
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen";
|
|||
import MinimapImplementation from "../Base/MinimapImplementation";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
import {FlowPanel, FlowPanelFactory} from "./FlowStep";
|
||||
import {FlowPanelFactory} from "./FlowStep";
|
||||
import {RequestFile} from "./RequestFile";
|
||||
import {DataPanel} from "./DataPanel";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
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 {
|
||||
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
|
||||
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[] = [
|
||||
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"),
|
||||
].map(el => el?.SetClass("pl-4"))
|
||||
|
||||
const leftBar = new Combine([
|
||||
new Combine(leftContents).SetClass("sticky top-4 m-4")
|
||||
]).SetClass("block w-full md:w-2/6 lg:w-1/6")
|
||||
new Combine(leftContents).SetClass("sticky top-4 m-4"),
|
||||
]).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(
|
||||
new Toggle(
|
||||
new Combine([
|
||||
leftBar,
|
||||
mainPanel.SetClass("m-8 w-full mb-24")
|
||||
flow.SetClass("m-8 w-full mb-24")
|
||||
]).SetClass("h-full block md:flex")
|
||||
|
||||
,
|
||||
|
|
28
UI/ImportFlow/ImportUtils.ts
Normal file
28
UI/ImportFlow/ImportUtils.ts
Normal 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]);
|
||||
}
|
||||
}
|
|
@ -504,7 +504,8 @@ export default class ValidatedTextField {
|
|||
mapBackgroundLayer?: UIEventSource<any>,
|
||||
unit?: Unit,
|
||||
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
||||
feature?: any
|
||||
feature?: any,
|
||||
inputStyle?: string
|
||||
}): InputElement<string> {
|
||||
options = options ?? {};
|
||||
options.placeholder = options.placeholder ?? type;
|
||||
|
|
|
@ -19,6 +19,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
|
||||
constructor(
|
||||
state: {
|
||||
featureSwitchIsTesting: UIEventSource<boolean>;
|
||||
osmConnection: OsmConnection,
|
||||
featurePipeline: FeaturePipeline,
|
||||
backgroundLayer?: UIEventSource<BaseLayer>
|
||||
|
@ -167,8 +168,11 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
).onClick(cancel)
|
||||
|
||||
super([
|
||||
state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
||||
new Toggle(
|
||||
Translations.t.general.testing.SetClass("alert"),
|
||||
undefined,
|
||||
state.featureSwitchIsTesting
|
||||
),
|
||||
disableFiltersOrConfirm,
|
||||
cancelButton,
|
||||
preset.description,
|
||||
|
|
|
@ -141,7 +141,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){
|
||||
// This is probably a key
|
||||
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);
|
||||
|
@ -201,7 +201,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){
|
||||
// This might be a property to expand...
|
||||
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)
|
||||
}else{
|
||||
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
|
||||
|
|
|
@ -55,6 +55,45 @@ export interface SpecialVisualization {
|
|||
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 {
|
||||
|
||||
public static specialVisualizations = SpecialVisualizations.init()
|
||||
|
@ -99,37 +138,7 @@ export default class SpecialVisualizations {
|
|||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: ((state, tags: UIEventSource<any>) => {
|
||||
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")
|
||||
})
|
||||
constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state))
|
||||
},
|
||||
{
|
||||
funcName: "image_carousel",
|
||||
|
@ -339,7 +348,7 @@ export default class SpecialVisualizations {
|
|||
const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat),
|
||||
encodeURIComponent(subject),
|
||||
state.mangroveIdentity,
|
||||
state.osmConnection._dryRun
|
||||
state.featureSwitchIsTesting.data
|
||||
);
|
||||
const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection);
|
||||
return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form);
|
||||
|
@ -743,10 +752,6 @@ export default class SpecialVisualizations {
|
|||
return t.addCommentAndClose
|
||||
}))).onClick(() => {
|
||||
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(_ => {
|
||||
tags.data["closed_at"] = new Date().toISOString();
|
||||
tags.ping()
|
||||
|
@ -760,10 +765,6 @@ export default class SpecialVisualizations {
|
|||
return t.reopenNoteAndComment
|
||||
}))).onClick(() => {
|
||||
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(_ => {
|
||||
tags.data["closed_at"] = undefined;
|
||||
tags.ping()
|
||||
|
|
|
@ -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)",
|
||||
"source": {
|
||||
"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,
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
|
@ -29,7 +29,8 @@
|
|||
"_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined",
|
||||
"_first_user:=feat.get('comments')[0].user",
|
||||
"_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": [
|
||||
{
|
||||
|
@ -201,6 +202,17 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "no_imports",
|
||||
"options": [
|
||||
{
|
||||
"osmTags": "_is_import_note=",
|
||||
"question": {
|
||||
"en": "Hide import notes"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1493,6 +1493,10 @@ video {
|
|||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.pb-12 {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
|
|
@ -465,7 +465,9 @@
|
|||
"importLayer": {
|
||||
"layerName": "Possible {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": {
|
||||
"title": "Import helper",
|
||||
|
@ -477,5 +479,5 @@
|
|||
"selectLayer": "Select a layer...",
|
||||
"selectFileTitle": "Select file",
|
||||
"validateDataTitle": "Validate data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3354,6 +3354,13 @@
|
|||
"question": "Only show open notes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Hide import notes"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "OpenStreetMap notes",
|
||||
|
|
|
@ -314,5 +314,12 @@
|
|||
},
|
||||
"multi_apply": {
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@ import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
|||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import Constants from "../Models/Constants";
|
||||
import {
|
||||
DesugaringContext,
|
||||
PrepareLayer,
|
||||
PrepareTheme,
|
||||
ValidateLayer,
|
||||
ValidateThemeAndLayers
|
||||
} 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 icons from "../assets/tagRenderings/icons.json";
|
||||
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.
|
||||
// It spits out an overview of those to be used to load them
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import T from "./TestHelper";
|
||||
import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
||||
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 {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer";
|
||||
|
||||
export default class CreateNoteImportLayerSpec extends T {
|
||||
|
||||
|
@ -17,7 +18,7 @@ export default class CreateNoteImportLayerSpec extends T {
|
|||
|
||||
}
|
||||
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 generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert")
|
||||
// fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, " "), "utf8")
|
||||
|
|
|
@ -112,6 +112,12 @@ export default class TagSpec extends T {
|
|||
equal(compare.matchesProperties({"key": "5"}), true);
|
||||
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", (() => {
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ import * as assert from "assert";
|
|||
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
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 {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import Constants from "../Models/Constants";
|
||||
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||
|
||||
export default class ThemeSpec extends T {
|
||||
constructor() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue