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 {LocalStorageSource} from "./Web/LocalStorageSource";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import * as personal from "../assets/themes/personal/personal.json";
|
import * as personal from "../assets/themes/personal/personal.json";
|
||||||
import {FixLegacyTheme, PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
||||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||||
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
|
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
|
||||||
import * as known_layers from "../assets/generated/known_layers.json"
|
import * as known_layers from "../assets/generated/known_layers.json"
|
||||||
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||||
|
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||||
|
|
||||||
export default class DetermineLayout {
|
export default class DetermineLayout {
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,11 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
||||||
},
|
},
|
||||||
tileIndex,
|
tileIndex,
|
||||||
upstream: FeatureSourceForLayer,
|
upstream: FeatureSourceForLayer,
|
||||||
metataggingUpdated: UIEventSource<any>
|
metataggingUpdated?: UIEventSource<any>
|
||||||
) {
|
) {
|
||||||
this.name = "FilteringFeatureSource(" + upstream.name + ")"
|
this.name = "FilteringFeatureSource(" + upstream.name + ")"
|
||||||
this.tileIndex = tileIndex
|
this.tileIndex = tileIndex
|
||||||
this.bbox = BBox.fromTileIndex(tileIndex)
|
this.bbox = tileIndex === undefined ? undefined : BBox.fromTileIndex(tileIndex)
|
||||||
this.upstream = upstream
|
this.upstream = upstream
|
||||||
this.state = state
|
this.state = state
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
metataggingUpdated.addCallback(_ => {
|
metataggingUpdated?.addCallback(_ => {
|
||||||
self._is_dirty.setData(true)
|
self._is_dirty.setData(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
console.log("FIltering", this.upstream.name)
|
||||||
const self = this;
|
const self = this;
|
||||||
const layer = this.upstream.layer;
|
const layer = this.upstream.layer;
|
||||||
const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []);
|
const features: { feature: any; freshness: Date }[] = (this.upstream.features.data ?? []);
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
private readonly featureIdBlacklist?: UIEventSource<Set<string>>
|
private readonly featureIdBlacklist?: UIEventSource<Set<string>>
|
||||||
|
|
||||||
public constructor(flayer: FilteredLayer,
|
public constructor(flayer: FilteredLayer,
|
||||||
zxy?: [number, number, number],
|
zxy?: [number, number, number] | BBox,
|
||||||
options?: {
|
options?: {
|
||||||
featureIdBlacklist?: UIEventSource<Set<string>>
|
featureIdBlacklist?: UIEventSource<Set<string>>
|
||||||
}) {
|
}) {
|
||||||
|
@ -41,23 +41,32 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
this.featureIdBlacklist = options?.featureIdBlacklist
|
this.featureIdBlacklist = options?.featureIdBlacklist
|
||||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||||
if (zxy !== undefined) {
|
if (zxy !== undefined) {
|
||||||
const [z, x, y] = zxy;
|
let tile_bbox: BBox;
|
||||||
let tile_bbox = BBox.fromTile(z, x, y)
|
if (zxy instanceof BBox) {
|
||||||
|
tile_bbox = zxy;
|
||||||
|
} else {
|
||||||
|
const [z, x, y] = zxy;
|
||||||
|
tile_bbox = BBox.fromTile(z, x, y);
|
||||||
|
|
||||||
|
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||||
|
this.bbox = BBox.fromTile(z, x, y)
|
||||||
|
url = url
|
||||||
|
.replace('{z}', "" + z)
|
||||||
|
.replace('{x}', "" + x)
|
||||||
|
.replace('{y}', "" + y)
|
||||||
|
}
|
||||||
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
||||||
if (this.layer.layerDef.source.mercatorCrs) {
|
if (this.layer.layerDef.source.mercatorCrs) {
|
||||||
bounds = tile_bbox.toMercator()
|
bounds = tile_bbox.toMercator()
|
||||||
}
|
}
|
||||||
|
|
||||||
url = url
|
url = url
|
||||||
.replace('{z}', "" + z)
|
|
||||||
.replace('{x}', "" + x)
|
|
||||||
.replace('{y}', "" + y)
|
|
||||||
.replace('{y_min}', "" + bounds.minLat)
|
.replace('{y_min}', "" + bounds.minLat)
|
||||||
.replace('{y_max}', "" + bounds.maxLat)
|
.replace('{y_max}', "" + bounds.maxLat)
|
||||||
.replace('{x_min}', "" + bounds.minLon)
|
.replace('{x_min}', "" + bounds.minLon)
|
||||||
.replace('{x_max}', "" + bounds.maxLon)
|
.replace('{x_max}', "" + bounds.maxLon)
|
||||||
|
|
||||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
|
||||||
this.bbox = BBox.fromTile(z, x, y)
|
|
||||||
} else {
|
} else {
|
||||||
this.tileIndex = Tiles.tile_index(0, 0, 0)
|
this.tileIndex = Tiles.tile_index(0, 0, 0)
|
||||||
this.bbox = BBox.global;
|
this.bbox = BBox.global;
|
||||||
|
@ -83,7 +92,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||||
if (self.layer.layerDef.source.mercatorCrs) {
|
if (self.layer.layerDef.source.mercatorCrs) {
|
||||||
json = GeoOperations.GeoJsonToWGS84(json)
|
json = GeoOperations.GeoJsonToWGS84(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = new Date();
|
const time = new Date();
|
||||||
const newFeatures: { feature: any, freshness: Date } [] = []
|
const newFeatures: { feature: any, freshness: Date } [] = []
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
|
@ -683,6 +683,8 @@ export class GeoOperations {
|
||||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,13 @@ export class ChangesetHandler {
|
||||||
private readonly allElements: ElementStorage;
|
private readonly allElements: ElementStorage;
|
||||||
private osmConnection: OsmConnection;
|
private osmConnection: OsmConnection;
|
||||||
private readonly changes: Changes;
|
private readonly changes: Changes;
|
||||||
private readonly _dryRun: boolean;
|
private readonly _dryRun: UIEventSource<boolean>;
|
||||||
private readonly userDetails: UIEventSource<UserDetails>;
|
private readonly userDetails: UIEventSource<UserDetails>;
|
||||||
private readonly auth: any;
|
private readonly auth: any;
|
||||||
private readonly backend: string;
|
private readonly backend: string;
|
||||||
|
|
||||||
constructor(layoutName: string,
|
constructor(layoutName: string,
|
||||||
dryRun: boolean,
|
dryRun: UIEventSource<boolean>,
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
changes: Changes,
|
changes: Changes,
|
||||||
|
@ -67,7 +67,7 @@ export class ChangesetHandler {
|
||||||
this.userDetails.data.csCount = 1;
|
this.userDetails.data.csCount = 1;
|
||||||
this.userDetails.ping();
|
this.userDetails.ping();
|
||||||
}
|
}
|
||||||
if (this._dryRun) {
|
if (this._dryRun.data) {
|
||||||
const changesetXML = generateChangeXML(123456);
|
const changesetXML = generateChangeXML(123456);
|
||||||
console.log("Metatags are", extraMetaTags)
|
console.log("Metatags are", extraMetaTags)
|
||||||
console.log(changesetXML);
|
console.log(changesetXML);
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default class UserDetails {
|
||||||
public img: string;
|
public img: string;
|
||||||
public unreadMessages = 0;
|
public unreadMessages = 0;
|
||||||
public totalMessages = 0;
|
public totalMessages = 0;
|
||||||
public dryRun: boolean;
|
|
||||||
home: { lon: number; lat: number };
|
home: { lon: number; lat: number };
|
||||||
public backend: string;
|
public backend: string;
|
||||||
|
|
||||||
|
@ -47,7 +46,6 @@ export class OsmConnection {
|
||||||
public auth;
|
public auth;
|
||||||
public userDetails: UIEventSource<UserDetails>;
|
public userDetails: UIEventSource<UserDetails>;
|
||||||
public isLoggedIn: UIEventSource<boolean>
|
public isLoggedIn: UIEventSource<boolean>
|
||||||
_dryRun: boolean;
|
|
||||||
public preferencesHandler: OsmPreferences;
|
public preferencesHandler: OsmPreferences;
|
||||||
public changesetHandler: ChangesetHandler;
|
public changesetHandler: ChangesetHandler;
|
||||||
public readonly _oauth_config: {
|
public readonly _oauth_config: {
|
||||||
|
@ -55,6 +53,7 @@ export class OsmConnection {
|
||||||
oauth_secret: string,
|
oauth_secret: string,
|
||||||
url: string
|
url: string
|
||||||
};
|
};
|
||||||
|
private readonly _dryRun: UIEventSource<boolean>;
|
||||||
private fakeUser: boolean;
|
private fakeUser: boolean;
|
||||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||||
private readonly _iframeMode: Boolean | boolean;
|
private readonly _iframeMode: Boolean | boolean;
|
||||||
|
@ -62,7 +61,7 @@ export class OsmConnection {
|
||||||
private isChecking = false;
|
private isChecking = false;
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
dryRun?: false | boolean,
|
dryRun?: UIEventSource<boolean>,
|
||||||
fakeUser?: false | boolean,
|
fakeUser?: false | boolean,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
changes: Changes,
|
changes: Changes,
|
||||||
|
@ -82,7 +81,6 @@ export class OsmConnection {
|
||||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||||
|
|
||||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||||
this.userDetails.data.dryRun = (options.dryRun ?? false) || (options.fakeUser ?? false);
|
|
||||||
if (options.fakeUser) {
|
if (options.fakeUser) {
|
||||||
const ud = this.userDetails.data;
|
const ud = this.userDetails.data;
|
||||||
ud.csCount = 5678
|
ud.csCount = 5678
|
||||||
|
@ -99,13 +97,13 @@ export class OsmConnection {
|
||||||
self.AttemptLogin()
|
self.AttemptLogin()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._dryRun = options.dryRun;
|
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
|
||||||
|
|
||||||
this.updateAuthObject();
|
this.updateAuthObject();
|
||||||
|
|
||||||
this.preferencesHandler = new OsmPreferences(this.auth, this);
|
this.preferencesHandler = new OsmPreferences(this.auth, this);
|
||||||
|
|
||||||
this.changesetHandler = new ChangesetHandler(options.layoutName, options.dryRun, this, options.allElements, options.changes, this.auth);
|
this.changesetHandler = new ChangesetHandler(options.layoutName, this._dryRun, this, options.allElements, options.changes, this.auth);
|
||||||
if (options.oauth_token?.data !== undefined) {
|
if (options.oauth_token?.data !== undefined) {
|
||||||
console.log(options.oauth_token.data)
|
console.log(options.oauth_token.data)
|
||||||
const self = this;
|
const self = this;
|
||||||
|
@ -223,7 +221,7 @@ export class OsmConnection {
|
||||||
if ((text ?? "") !== "") {
|
if ((text ?? "") !== "") {
|
||||||
textSuffix = "?text=" + encodeURIComponent(text)
|
textSuffix = "?text=" + encodeURIComponent(text)
|
||||||
}
|
}
|
||||||
if (this._dryRun) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
ok()
|
ok()
|
||||||
|
@ -246,7 +244,7 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public reopenNote(id: number | string, text?: string): Promise<any> {
|
public reopenNote(id: number | string, text?: string): Promise<any> {
|
||||||
if (this._dryRun) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
ok()
|
ok()
|
||||||
|
@ -273,10 +271,10 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
|
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
|
||||||
if (this._dryRun) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually opening note with text ", text)
|
console.warn("Dryrun enabled - not actually opening note with text ", text)
|
||||||
return new Promise((ok, error) => {
|
return new Promise<{ id: number }>((ok, error) => {
|
||||||
ok()
|
window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const auth = this.auth;
|
const auth = this.auth;
|
||||||
|
@ -285,15 +283,18 @@ export class OsmConnection {
|
||||||
auth.xhr({
|
auth.xhr({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `/api/0.6/notes.json`,
|
path: `/api/0.6/notes.json`,
|
||||||
options: {header:
|
options: {
|
||||||
{'Content-Type': 'application/json'}},
|
header:
|
||||||
|
{'Content-Type': 'application/json'}
|
||||||
|
},
|
||||||
content: JSON.stringify(content)
|
content: JSON.stringify(content)
|
||||||
|
|
||||||
}, function (err, response) {
|
}, function (err, response) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
const id = Number(response.children[0].children[0].children.item("id").innerHTML)
|
|
||||||
|
const id = response.properties.id
|
||||||
console.log("OPENED NOTE", id)
|
console.log("OPENED NOTE", id)
|
||||||
ok({id})
|
ok({id})
|
||||||
}
|
}
|
||||||
|
@ -304,7 +305,7 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public addCommentToNode(id: number | string, text: string): Promise<any> {
|
public addCommentToNode(id: number | string, text: string): Promise<any> {
|
||||||
if (this._dryRun) {
|
if (this._dryRun.data) {
|
||||||
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
|
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
ok()
|
ok()
|
||||||
|
@ -317,7 +318,7 @@ export class OsmConnection {
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
||||||
path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}`
|
path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}`
|
||||||
}, function (err, response) {
|
}, function (err, response) {
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default class UserRelatedState extends ElementsState {
|
||||||
|
|
||||||
this.osmConnection = new OsmConnection({
|
this.osmConnection = new OsmConnection({
|
||||||
changes: this.changes,
|
changes: this.changes,
|
||||||
dryRun: this.featureSwitchIsTesting.data,
|
dryRun: this.featureSwitchIsTesting,
|
||||||
fakeUser: this.featureSwitchFakeUser.data,
|
fakeUser: this.featureSwitchFakeUser.data,
|
||||||
allElements: this.allElements,
|
allElements: this.allElements,
|
||||||
oauth_token: QueryParameters.GetQueryParameter(
|
oauth_token: QueryParameters.GetQueryParameter(
|
||||||
|
|
|
@ -10,6 +10,13 @@ export class RegexTag extends TagsFilter {
|
||||||
constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
|
constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
|
||||||
super();
|
super();
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value.indexOf("^") < 0 && value.indexOf("$") < 0) {
|
||||||
|
value = "^" + value + "$"
|
||||||
|
}
|
||||||
|
value = new RegExp(value)
|
||||||
|
}
|
||||||
|
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.invert = invert;
|
this.invert = invert;
|
||||||
this.matchesEmpty = RegexTag.doesMatch("", this.value);
|
this.matchesEmpty = RegexTag.doesMatch("", this.value);
|
||||||
|
@ -109,7 +116,7 @@ export class RegexTag extends TagsFilter {
|
||||||
console.error("Cannot export regex tag to asChange; ", this.key, this.value)
|
console.error("Cannot export regex tag to asChange; ", this.key, this.value)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
AsJson() {
|
AsJson() {
|
||||||
return this.asHumanString()
|
return this.asHumanString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,16 +192,16 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
const f = (value: string | undefined) => {
|
const f = (value: string | undefined) => {
|
||||||
if(value === undefined){
|
if (value === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let b = Number(value?.trim() )
|
let b = Number(value?.trim())
|
||||||
if (isNaN(b)) {
|
if (isNaN(b)) {
|
||||||
if(value.endsWith(" UTC")) {
|
if (value.endsWith(" UTC")) {
|
||||||
value = value.replace(" UTC", "+00")
|
value = value.replace(" UTC", "+00")
|
||||||
}
|
}
|
||||||
b = new Date(value).getTime()
|
b = new Date(value).getTime()
|
||||||
if(isNaN(b)){
|
if (isNaN(b)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
split[0],
|
split[0],
|
||||||
new RegExp("^" + split[1] + "$"),
|
split[1],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -228,8 +228,8 @@ export class TagUtils {
|
||||||
split[1] = "..*"
|
split[1] = "..*"
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
new RegExp("^" + split[0] + "$"),
|
split[0],
|
||||||
new RegExp("^" + split[1] + "$")
|
split[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tag.indexOf("!:=") >= 0) {
|
if (tag.indexOf("!:=") >= 0) {
|
||||||
|
@ -248,7 +248,7 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
split[0],
|
split[0],
|
||||||
new RegExp("^" + split[1] + "$"),
|
new RegExp("^" + split[1] + "$"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
split[0],
|
split[0],
|
||||||
new RegExp("^" + split[1] + "$"),
|
split[1],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
split[0],
|
split[0],
|
||||||
new RegExp("^" + split[1] + "$")
|
split[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tag.indexOf("=") >= 0) {
|
if (tag.indexOf("=") >= 0) {
|
||||||
|
|
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 LayerConfig from "../LayerConfig";
|
||||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||||
import Translations from "../../../UI/i18n/Translations";
|
import Translations from "../../../UI/i18n/Translations";
|
||||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
|
||||||
import {And} from "../../../Logic/Tags/And";
|
|
||||||
|
|
||||||
export default class CreateNoteImportLayer extends Conversion<LayerConfig, LayerConfigJson> {
|
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
|
||||||
|
/**
|
||||||
|
* A closed note is included if it is less then 'n'-days closed
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private readonly _includeClosedNotesDays: number;
|
||||||
|
|
||||||
constructor() {
|
constructor(includeClosedNotesDays= 0) {
|
||||||
super([
|
super([
|
||||||
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
|
"Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').",
|
||||||
"The import buttons and matches will be based on the presets of the given theme",
|
"The import buttons and matches will be based on the presets of the given theme",
|
||||||
].join("\n\n"), [])
|
].join("\n\n"), [])
|
||||||
|
this._includeClosedNotesDays = includeClosedNotesDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
convert(state: DesugaringContext, layer: LayerConfig, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
convert(state: DesugaringContext, layerJson: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
||||||
const errors = []
|
const errors = []
|
||||||
const warnings = []
|
const warnings = []
|
||||||
const t = Translations.t.importLayer;
|
const t = Translations.t.importLayer;
|
||||||
|
|
||||||
const possibleTags: TagsFilter[] = layer.presets.map(p => new And(p.tags))
|
/**
|
||||||
|
* The note itself will contain `tags=k=v;k=v;k=v;...
|
||||||
|
* This must be matched with a regex.
|
||||||
|
* This is a simple JSON-object as how it'll be put into the layerConfigJson directly
|
||||||
|
*/
|
||||||
|
const isShownIfAny : any[] = []
|
||||||
|
const layer = new LayerConfig(layerJson, "while constructing a note-import layer")
|
||||||
|
for (const preset of layer.presets) {
|
||||||
|
const mustMatchAll = []
|
||||||
|
for (const tag of preset.tags) {
|
||||||
|
const key = tag.key
|
||||||
|
const value = tag.value
|
||||||
|
const condition = "_tags~(^|.*;)"+key+"\="+value+"($|;.*)"
|
||||||
|
mustMatchAll.push(condition)
|
||||||
|
}
|
||||||
|
isShownIfAny.push({and:mustMatchAll})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointRenderings = (layerJson.mapRendering??[]).filter(r => r!== null && r["location"] !== undefined);
|
||||||
|
const firstRender = <PointRenderingConfigJson>(pointRenderings [0])
|
||||||
|
const icon = firstRender.icon
|
||||||
|
const iconBadges = []
|
||||||
|
if(icon !== undefined){
|
||||||
|
iconBadges.push({
|
||||||
|
if: {and:[]},
|
||||||
|
then:icon
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const importButton = {}
|
||||||
|
{
|
||||||
|
const translations = t.importButton.Subs({layerId: layer.id, title: layer.presets[0].title}).translations
|
||||||
|
for (const key in translations) {
|
||||||
|
importButton[key] = "{"+translations[key]+"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result : LayerConfigJson = {
|
const result : LayerConfigJson = {
|
||||||
"id": "note_import_"+layer.id,
|
"id": "note_import_"+layer.id,
|
||||||
"name": t.layerName.Subs({title: layer.title.render}).translations,
|
// By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
|
||||||
"description": t.description.Subs({title: layer.title.render}).translations,
|
"description": t.description.Subs({title: layer.title.render}).translations,
|
||||||
"source": {
|
"source": {
|
||||||
"osmTags": {
|
"osmTags": {
|
||||||
|
@ -31,27 +71,32 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
||||||
"id~*"
|
"id~*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=0&bbox={x_min},{y_min},{x_max},{y_max}",
|
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed="+this._includeClosedNotesDays+"&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||||
"geoJsonZoomLevel": 12,
|
"geoJsonZoomLevel": 10,
|
||||||
"maxCacheAge": 0
|
"maxCacheAge": 0
|
||||||
},
|
},
|
||||||
"minzoom": 10,
|
"minzoom": 12,
|
||||||
"title": {
|
"title": {
|
||||||
"render": t.popupTitle.Subs({title: layer.presets[0].title}).translations
|
"render": t.popupTitle.Subs({title: layer.presets[0].title}).translations
|
||||||
},
|
},
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_first_comment:=feat.get('comments')[0].text.toLowerCase()",
|
"_first_comment=feat.get('comments')[0].text.toLowerCase()",
|
||||||
"_trigger_index:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
|
"_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
|
||||||
"_intro:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.map(l => l == '' ? '<br/>' : l).join('');})()",
|
"_comments_count=feat.get('comments').length",
|
||||||
"_tags:=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
|
"_intro=(() => {const lines = feat.properties['_first_comment'].split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
|
||||||
|
"_tags=(() => {let lines = feat.properties['_first_comment'].split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()"
|
||||||
],
|
],
|
||||||
"isShown": {
|
"isShown": {
|
||||||
"render": "no",
|
"render": "no",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "comments!~.*https://mapcomplete.osm.be.*",
|
||||||
|
"then":"no"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"if": {and:
|
"if": {and:
|
||||||
["_trigger_index~*",
|
["_trigger_index~*",
|
||||||
{or: possibleTags.map(tf => tf.AsJson())}
|
{or: isShownIfAny}
|
||||||
]},
|
]},
|
||||||
"then": "yes"
|
"then": "yes"
|
||||||
}
|
}
|
||||||
|
@ -63,25 +108,34 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
|
||||||
"id": "conversation",
|
|
||||||
"render": "{visualize_note_comments(comments,1)}"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "Intro",
|
"id": "Intro",
|
||||||
"render": "{_intro}"
|
"render": "{_intro}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "conversation",
|
||||||
|
"render": "{visualize_note_comments(comments,1)}",
|
||||||
|
condition: "_comments_count>1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "import",
|
"id": "import",
|
||||||
"render": "{import_button(public_bookcase, _tags, There might be a public bookcase here,./assets/svg/addSmall.svg,,,id)}"
|
"render": importButton,
|
||||||
|
condition: "closed_at="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "close_note_",
|
"id": "close_note_",
|
||||||
"render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}"
|
"render": "{close_note(Does not exist<br/>, ./assets/svg/close.svg, id, This feature does not exist)}",
|
||||||
|
condition: "closed_at="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "close_note_mapped",
|
"id": "close_note_mapped",
|
||||||
"render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}"
|
"render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}",
|
||||||
|
condition: "closed_at="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "handled",
|
||||||
|
"render": t.importHandled.translations,
|
||||||
|
condition: "closed_at~*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "comment",
|
"id": "comment",
|
||||||
|
@ -90,6 +144,10 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
||||||
{
|
{
|
||||||
"id": "add_image",
|
"id": "add_image",
|
||||||
"render": "{add_image_to_note()}"
|
"render": "{add_image_to_note()}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id:"alltags",
|
||||||
|
render:"{all_tags()}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
|
@ -99,9 +157,14 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfig, Layer
|
||||||
"centroid"
|
"centroid"
|
||||||
],
|
],
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "teardrop:#3333cc"
|
"render": "circle:white;help:black",
|
||||||
|
mappings:[{
|
||||||
|
if: {or:["closed_at~*","_imported=yes"]},
|
||||||
|
then:"circle:white;checkmark:black"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
"iconSize": "40,40,bottom"
|
iconBadges,
|
||||||
|
"iconSize": "40,40,center"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,439 +1,12 @@
|
||||||
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
|
||||||
import DependencyCalculator from "../DependencyCalculator";
|
|
||||||
import LayerConfig from "../LayerConfig";
|
import LayerConfig from "../LayerConfig";
|
||||||
import {Translation} from "../../../UI/i18n/Translation";
|
import {Translation} from "../../../UI/i18n/Translation";
|
||||||
import LayoutConfig from "../LayoutConfig";
|
import LayoutConfig from "../LayoutConfig";
|
||||||
import {Utils} from "../../../Utils";
|
import {Utils} from "../../../Utils";
|
||||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
|
||||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
|
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
|
||||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||||
import Constants from "../../Constants";
|
import Constants from "../../Constants";
|
||||||
import {AllKnownLayouts} from "../../../Customizations/AllKnownLayouts";
|
import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion";
|
||||||
import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation";
|
|
||||||
|
|
||||||
export interface DesugaringContext {
|
|
||||||
tagRenderings: Map<string, TagRenderingConfigJson>
|
|
||||||
sharedLayers: Map<string, LayerConfigJson>
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class Conversion<TIn, TOut> {
|
|
||||||
public readonly modifiedAttributes: string[];
|
|
||||||
protected readonly doc: string;
|
|
||||||
|
|
||||||
constructor(doc: string, modifiedAttributes: string[] = []) {
|
|
||||||
this.modifiedAttributes = modifiedAttributes;
|
|
||||||
this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
|
|
||||||
if (fixed?.errors?.length > 0) {
|
|
||||||
throw fixed.errors.join("\n");
|
|
||||||
}
|
|
||||||
fixed.warnings?.forEach(w => console.warn(w))
|
|
||||||
return fixed.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public convertStrict(state: DesugaringContext, json: TIn, context: string): TOut {
|
|
||||||
const fixed = this.convert(state, json, context)
|
|
||||||
return DesugaringStep.strict(fixed)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract convert(state: DesugaringContext, json: TIn, context: string): { result: TOut, errors: string[], warnings: string[] }
|
|
||||||
|
|
||||||
public convertAll(state: DesugaringContext, jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } {
|
|
||||||
const result = []
|
|
||||||
const errors = []
|
|
||||||
const warnings = []
|
|
||||||
for (let i = 0; i < jsons.length; i++) {
|
|
||||||
const json = jsons[i];
|
|
||||||
const r = this.convert(state, json, context + "[" + i + "]")
|
|
||||||
result.push(r.result)
|
|
||||||
errors.push(...r.errors)
|
|
||||||
warnings.push(...r.warnings)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class DesugaringStep<T> extends Conversion<T, T> {
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnEvery<X, T> extends DesugaringStep<T> {
|
|
||||||
private readonly key: string;
|
|
||||||
private readonly step: DesugaringStep<X>;
|
|
||||||
|
|
||||||
constructor(key: string, step: DesugaringStep<X>) {
|
|
||||||
super("Applies " + step.constructor.name + " onto every object of the list `key`", [key]);
|
|
||||||
this.step = step;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
|
||||||
json = {...json}
|
|
||||||
const step = this.step
|
|
||||||
const key = this.key;
|
|
||||||
const r = step.convertAll(state, (<X[]>json[key]), context + "." + key)
|
|
||||||
json[key] = r.result
|
|
||||||
return {
|
|
||||||
result: json,
|
|
||||||
errors: r.errors,
|
|
||||||
warnings: r.warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnEveryConcat<X, T> extends DesugaringStep<T> {
|
|
||||||
private readonly key: string;
|
|
||||||
private readonly step: Conversion<X, X[]>;
|
|
||||||
|
|
||||||
constructor(key: string, step: Conversion<X, X[]>) {
|
|
||||||
super(`Applies ${step.constructor.name} onto every object of the list \`${key}\``, [key]);
|
|
||||||
this.step = step;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
|
||||||
json = {...json}
|
|
||||||
const step = this.step
|
|
||||||
const key = this.key;
|
|
||||||
const values = json[key]
|
|
||||||
if (values === undefined) {
|
|
||||||
// Move on - nothing to see here!
|
|
||||||
return {
|
|
||||||
result: json,
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const r = step.convertAll(state, (<X[]>values), context + "." + key)
|
|
||||||
const vals: X[][] = r.result
|
|
||||||
json[key] = [].concat(...vals)
|
|
||||||
return {
|
|
||||||
result: json,
|
|
||||||
errors: r.errors,
|
|
||||||
warnings: r.warnings
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Fuse<T> extends DesugaringStep<T> {
|
|
||||||
private readonly steps: DesugaringStep<T>[];
|
|
||||||
|
|
||||||
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
|
|
||||||
super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.constructor.name).join(", "),
|
|
||||||
Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes)))
|
|
||||||
);
|
|
||||||
this.steps = steps;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
|
||||||
const errors = []
|
|
||||||
const warnings = []
|
|
||||||
for (let i = 0; i < this.steps.length; i++) {
|
|
||||||
const step = this.steps[i];
|
|
||||||
let r = step.convert(state, json, context + "(fusion " + this.constructor.name + "." + i + ")")
|
|
||||||
errors.push(...r.errors)
|
|
||||||
warnings.push(...r.warnings)
|
|
||||||
json = r.result
|
|
||||||
if (errors.length > 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
result: json,
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
|
||||||
constructor() {
|
|
||||||
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this tag rendering has a minimap in some language.
|
|
||||||
* Note: this minimap can be hidden by conditions
|
|
||||||
*/
|
|
||||||
private static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean {
|
|
||||||
const translations: Translation[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]);
|
|
||||||
for (const translation of translations) {
|
|
||||||
for (const key in translation.translations) {
|
|
||||||
if (!translation.translations.hasOwnProperty(key)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const template = translation.translations[key]
|
|
||||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
|
||||||
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
|
||||||
if (hasMiniMap) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(<TagRenderingConfigJson> tr)) ?? true
|
|
||||||
if (!hasMinimap) {
|
|
||||||
layerConfig = {...layerConfig}
|
|
||||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
|
||||||
layerConfig.tagRenderings.push(state.tagRenderings.get("minimap"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
errors:[],
|
|
||||||
warnings: [],
|
|
||||||
result: layerConfig
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
|
|
||||||
constructor() {
|
|
||||||
super("Converts a tagRenderingSpec into the full tagRendering", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
|
||||||
const errors = []
|
|
||||||
const warnings = []
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: this.convertUntilStable(state, json, warnings, errors, context),
|
|
||||||
errors, warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private lookup(state: DesugaringContext, name: string): TagRenderingConfigJson[] {
|
|
||||||
if (state.tagRenderings.has(name)) {
|
|
||||||
return [state.tagRenderings.get(name)]
|
|
||||||
}
|
|
||||||
if (name.indexOf(".") >= 0) {
|
|
||||||
const spl = name.split(".");
|
|
||||||
const layer = state.sharedLayers.get(spl[0])
|
|
||||||
if (spl.length === 2 && layer !== undefined) {
|
|
||||||
const id = spl[1];
|
|
||||||
|
|
||||||
const layerTrs = <TagRenderingConfigJson[]>layer.tagRenderings.filter(tr => tr["id"] !== undefined)
|
|
||||||
let matchingTrs: TagRenderingConfigJson[]
|
|
||||||
if (id === "*") {
|
|
||||||
matchingTrs = layerTrs
|
|
||||||
} else if (id.startsWith("*")) {
|
|
||||||
const id_ = id.substring(1)
|
|
||||||
matchingTrs = layerTrs.filter(tr => tr.group === id_)
|
|
||||||
} else {
|
|
||||||
matchingTrs = layerTrs.filter(tr => tr.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < matchingTrs.length; i++) {
|
|
||||||
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
|
|
||||||
const found = Utils.Clone(matchingTrs[i]);
|
|
||||||
if (found.condition === undefined) {
|
|
||||||
found.condition = layer.source.osmTags
|
|
||||||
} else {
|
|
||||||
found.condition = {and: [found.condition, layer.source.osmTags]}
|
|
||||||
}
|
|
||||||
matchingTrs[i] = found
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingTrs.length !== 0) {
|
|
||||||
return matchingTrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private convertOnce(state: DesugaringContext, tr: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
|
||||||
if (tr === "questions") {
|
|
||||||
return [{
|
|
||||||
id: "questions"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (typeof tr === "string") {
|
|
||||||
const lookup = this.lookup(state, tr);
|
|
||||||
if (lookup !== undefined) {
|
|
||||||
return lookup
|
|
||||||
}
|
|
||||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
|
||||||
return [{
|
|
||||||
render: tr,
|
|
||||||
id: tr.replace(/![a-zA-Z0-9]/g, "")
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tr["builtin"] !== undefined) {
|
|
||||||
let names = tr["builtin"]
|
|
||||||
if (typeof names === "string") {
|
|
||||||
names = [names]
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of Object.keys(tr)) {
|
|
||||||
if (key === "builtin" || key === "override" || key === "id" || key.startsWith("#")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errors.push("At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON.stringify(tr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const trs: TagRenderingConfigJson[] = []
|
|
||||||
for (const name of names) {
|
|
||||||
const lookup = this.lookup(state, name)
|
|
||||||
if (lookup === undefined) {
|
|
||||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (let foundTr of lookup) {
|
|
||||||
foundTr = Utils.Clone<any>(foundTr)
|
|
||||||
Utils.Merge(tr["override"] ?? {}, foundTr)
|
|
||||||
trs.push(foundTr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [tr]
|
|
||||||
}
|
|
||||||
|
|
||||||
private convertUntilStable(state: DesugaringContext, spec: string | any, warnings: string[], errors: string[], ctx: string): TagRenderingConfigJson[] {
|
|
||||||
const trs = this.convertOnce(state, spec, warnings, errors, ctx);
|
|
||||||
|
|
||||||
const result = []
|
|
||||||
for (const tr of trs) {
|
|
||||||
if (tr["builtin"] !== undefined) {
|
|
||||||
const stable = this.convertUntilStable(state, tr, warnings, errors, ctx + "(RECURSIVE RESOLVE)")
|
|
||||||
result.push(...stable)
|
|
||||||
} else {
|
|
||||||
result.push(tr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpandGroupRewrite extends Conversion<{
|
|
||||||
rewrite: {
|
|
||||||
sourceString: string,
|
|
||||||
into: string[]
|
|
||||||
}[],
|
|
||||||
renderings: (string | { builtin: string, override: any } | TagRenderingConfigJson)[]
|
|
||||||
} | TagRenderingConfigJson, TagRenderingConfigJson[]> {
|
|
||||||
|
|
||||||
|
|
||||||
private static expandSubTagRenderings = new ExpandTagRendering()
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
"Converts a rewrite config for tagRenderings into the expanded form"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json:
|
|
||||||
{
|
|
||||||
rewrite:
|
|
||||||
{ sourceString: string; into: string[] }[]; renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
|
||||||
} | TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
|
|
||||||
|
|
||||||
if (json["rewrite"] === undefined) {
|
|
||||||
return {result: [<TagRenderingConfigJson>json], errors: [], warnings: []}
|
|
||||||
}
|
|
||||||
let config = <{
|
|
||||||
rewrite:
|
|
||||||
{ sourceString: string; into: string[] }[];
|
|
||||||
renderings: (string | { builtin: string; override: any } | TagRenderingConfigJson)[]
|
|
||||||
}>json;
|
|
||||||
|
|
||||||
|
|
||||||
const subRenderingsRes = ExpandGroupRewrite.expandSubTagRenderings.convertAll(state, config.renderings, context);
|
|
||||||
const subRenderings: TagRenderingConfigJson[] = [].concat(subRenderingsRes.result);
|
|
||||||
const errors = subRenderingsRes.errors;
|
|
||||||
const warnings = subRenderingsRes.warnings;
|
|
||||||
|
|
||||||
|
|
||||||
const rewrittenPerGroup = new Map<string, TagRenderingConfigJson[]>()
|
|
||||||
|
|
||||||
// The actual rewriting
|
|
||||||
for (const rewrite of config.rewrite) {
|
|
||||||
const source = rewrite.sourceString;
|
|
||||||
for (const target of rewrite.into) {
|
|
||||||
const groupName = target;
|
|
||||||
const trs: TagRenderingConfigJson[] = []
|
|
||||||
|
|
||||||
for (const tr of subRenderings) {
|
|
||||||
trs.push(this.prepConfig(source, target, tr))
|
|
||||||
}
|
|
||||||
if (rewrittenPerGroup.has(groupName)) {
|
|
||||||
rewrittenPerGroup.get(groupName).push(...trs)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
rewrittenPerGroup.set(groupName, trs)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add questions box for this category
|
|
||||||
rewrittenPerGroup.forEach((group, groupName) => {
|
|
||||||
group.push(<TagRenderingConfigJson>{
|
|
||||||
id: "questions",
|
|
||||||
group: groupName
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
rewrittenPerGroup.forEach((group, _) => {
|
|
||||||
group.forEach(tr => {
|
|
||||||
if (tr.id === undefined || tr.id === "") {
|
|
||||||
errors.push("A tagrendering has an empty ID after expanding the tag")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: [].concat(...Array.from(rewrittenPerGroup.values())),
|
|
||||||
errors, warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Used for left|right group creation and replacement */
|
|
||||||
private prepConfig(keyToRewrite: string, target: string, tr: TagRenderingConfigJson) {
|
|
||||||
|
|
||||||
function replaceRecursive(transl: string | any) {
|
|
||||||
if (typeof transl === "string") {
|
|
||||||
return transl.replace(keyToRewrite, target)
|
|
||||||
}
|
|
||||||
if (transl.map !== undefined) {
|
|
||||||
return transl.map(o => replaceRecursive(o))
|
|
||||||
}
|
|
||||||
transl = {...transl}
|
|
||||||
for (const key in transl) {
|
|
||||||
transl[key] = replaceRecursive(transl[key])
|
|
||||||
}
|
|
||||||
return transl
|
|
||||||
}
|
|
||||||
|
|
||||||
const orig = tr;
|
|
||||||
tr = replaceRecursive(tr)
|
|
||||||
|
|
||||||
tr.id = target + "-" + orig.id
|
|
||||||
tr.group = target
|
|
||||||
return tr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
|
export class UpdateLegacyLayer extends DesugaringStep<LayerConfigJson | string | { builtin, override }> {
|
||||||
|
@ -822,235 +395,3 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
|
|
||||||
constructor() {
|
|
||||||
super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map<string, LayerConfigJson>, themeId: string): LayerConfigJson[] {
|
|
||||||
const dependenciesToAdd: LayerConfigJson[] = []
|
|
||||||
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map(l => l.id));
|
|
||||||
|
|
||||||
// Verify cross-dependencies
|
|
||||||
let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = []
|
|
||||||
do {
|
|
||||||
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
|
|
||||||
|
|
||||||
for (const layerConfig of alreadyLoaded) {
|
|
||||||
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
|
|
||||||
dependencies.push(...layerDeps)
|
|
||||||
}
|
|
||||||
|
|
||||||
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
|
|
||||||
// Their existance is checked elsewhere, so this is fine
|
|
||||||
unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer))
|
|
||||||
for (const unmetDependency of unmetDependencies) {
|
|
||||||
if (loadedLayerIds.has(unmetDependency.neededLayer)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const dep = allKnownLayers.get(unmetDependency.neededLayer)
|
|
||||||
if (dep === undefined) {
|
|
||||||
const message =
|
|
||||||
["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.",
|
|
||||||
"This layer is needed by " + unmetDependency.neededBy,
|
|
||||||
unmetDependency.reason + " (at " + unmetDependency.context + ")",
|
|
||||||
"Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",")
|
|
||||||
|
|
||||||
]
|
|
||||||
throw message.join("\n\t");
|
|
||||||
}
|
|
||||||
dependenciesToAdd.unshift(dep)
|
|
||||||
loadedLayerIds.add(dep.id);
|
|
||||||
unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (unmetDependencies.length > 0)
|
|
||||||
|
|
||||||
return dependenciesToAdd;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
|
||||||
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers;
|
|
||||||
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings;
|
|
||||||
const errors = [];
|
|
||||||
const warnings = [];
|
|
||||||
const layers: LayerConfigJson[] = <LayerConfigJson[]> theme.layers; // Layers should be expanded at this point
|
|
||||||
|
|
||||||
knownTagRenderings.forEach((value, key) => {
|
|
||||||
value.id = key;
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id);
|
|
||||||
if (dependencies.length > 0) {
|
|
||||||
|
|
||||||
warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed")
|
|
||||||
}
|
|
||||||
layers.unshift(...dependencies);
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: {
|
|
||||||
...theme,
|
|
||||||
layers: layers
|
|
||||||
},
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SetDefault<T> extends DesugaringStep<T> {
|
|
||||||
private readonly value: any;
|
|
||||||
private readonly key: string;
|
|
||||||
private readonly _overrideEmptyString: boolean;
|
|
||||||
|
|
||||||
constructor(key: string, value: any, overrideEmptyString = false) {
|
|
||||||
super("Sets " + key + " to a default value if undefined");
|
|
||||||
this.key = key;
|
|
||||||
this.value = value;
|
|
||||||
this._overrideEmptyString = overrideEmptyString;
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: T, context: string): { result: T; errors: string[]; warnings: string[] } {
|
|
||||||
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
|
|
||||||
json = {...json}
|
|
||||||
json[this.key] = this.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
errors: [], warnings: [],
|
|
||||||
result: json
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
"Fully prepares and expands a layer for the LayerConfig.",
|
|
||||||
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite()),
|
|
||||||
new OnEveryConcat("tagRenderings", new ExpandTagRendering()),
|
|
||||||
new SetDefault("titleIcons", ["defaults"]),
|
|
||||||
new OnEveryConcat("titleIcons", new ExpandTagRendering())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
|
|
||||||
constructor() {
|
|
||||||
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[]; warnings: string[] } {
|
|
||||||
const errors = []
|
|
||||||
const warnings = []
|
|
||||||
if (typeof json === "string") {
|
|
||||||
const found = state.sharedLayers.get(json)
|
|
||||||
if (found === undefined) {
|
|
||||||
return {
|
|
||||||
result: null,
|
|
||||||
errors: [context + ": The layer with name " + json + " was not found as a builtin layer"],
|
|
||||||
warnings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
result: [found],
|
|
||||||
errors, warnings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json["builtin"] !== undefined) {
|
|
||||||
let names = json["builtin"]
|
|
||||||
if (typeof names === "string") {
|
|
||||||
names = [names]
|
|
||||||
}
|
|
||||||
const layers = []
|
|
||||||
for (const name of names) {
|
|
||||||
const found = Utils.Clone(state.sharedLayers.get(name))
|
|
||||||
if (found === undefined) {
|
|
||||||
errors.push(context + ": The layer with name " + json + " was not found as a builtin layer")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) {
|
|
||||||
errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Utils.Merge(json["override"], found);
|
|
||||||
layers.push(found)
|
|
||||||
} catch (e) {
|
|
||||||
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
result: layers,
|
|
||||||
errors, warnings
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: [json],
|
|
||||||
errors, warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(state: DesugaringContext, json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
|
|
||||||
const errors = []
|
|
||||||
const warnings = []
|
|
||||||
json.layers = [...json.layers]
|
|
||||||
|
|
||||||
if (json.id === "personal") {
|
|
||||||
json.layers = []
|
|
||||||
for (const publicLayer of AllKnownLayouts.AllPublicLayers()) {
|
|
||||||
const id = publicLayer.id
|
|
||||||
const config = state.sharedLayers.get(id)
|
|
||||||
if(Constants.added_by_default.indexOf(id) >= 0){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(config === undefined){
|
|
||||||
// This is a layer which is coded within a public theme, not as separate .json
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
json.layers.push(config)
|
|
||||||
}
|
|
||||||
const publicIds = AllKnownLayouts.AllPublicLayers().map(l => l.id)
|
|
||||||
publicIds.map(id => state.sharedLayers.get(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const layerName of Constants.added_by_default) {
|
|
||||||
const v = state.sharedLayers.get(layerName)
|
|
||||||
if (v === undefined) {
|
|
||||||
errors.push("Default layer " + layerName + " not found")
|
|
||||||
}
|
|
||||||
json.layers.push(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: json,
|
|
||||||
errors,
|
|
||||||
warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PrepareTheme extends Fuse<LayoutConfigJson> {
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
"Fully prepares and expands a theme",
|
|
||||||
new OnEveryConcat("layers", new SubstituteLayer()),
|
|
||||||
new SetDefault("socialImage", "assets/SocialImage.png", true),
|
|
||||||
new AddDefaultLayers(),
|
|
||||||
new AddDependencyLayersToTheme(),
|
|
||||||
new OnEvery("layers", new PrepareLayer()),
|
|
||||||
new OnEvery("layers", new AddMiniMap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
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 Combine from "./Combine";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
|
||||||
export default class Loading extends Combine {
|
export default class Loading extends Combine {
|
||||||
constructor(msg?: Translation | string) {
|
constructor(msg?: BaseUIElement | string) {
|
||||||
const t = Translations.T(msg) ?? Translations.t.general.loading.Clone();
|
const t = Translations.W(msg) ?? Translations.t.general.loading;
|
||||||
t.SetClass("pl-2")
|
t.SetClass("pl-2")
|
||||||
super([
|
super([
|
||||||
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),
|
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default class Minimap {
|
||||||
/**
|
/**
|
||||||
* Construct a minimap
|
* Construct a minimap
|
||||||
*/
|
*/
|
||||||
public static createMiniMap: (options: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => {
|
public static createMiniMap: (options?: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => {
|
||||||
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
|
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
||||||
private readonly _addLayerControl: boolean;
|
private readonly _addLayerControl: boolean;
|
||||||
private readonly _options: MinimapOptions;
|
private readonly _options: MinimapOptions;
|
||||||
|
|
||||||
private constructor(options: MinimapOptions) {
|
private constructor(options?: MinimapOptions) {
|
||||||
super()
|
super()
|
||||||
options = options ?? {}
|
options = options ?? {}
|
||||||
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
|
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
|
||||||
|
@ -290,12 +290,6 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
||||||
map.setView([loc.lat, loc.lon], loc.zoom)
|
map.setView([loc.lat, loc.lon], loc.zoom)
|
||||||
})
|
})
|
||||||
|
|
||||||
location.map(loc => loc.zoom)
|
|
||||||
.addCallback(zoom => {
|
|
||||||
if (Math.abs(map.getZoom() - zoom) > 0.1) {
|
|
||||||
map.setZoom(zoom, {});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (self.bounds !== undefined) {
|
if (self.bounds !== undefined) {
|
||||||
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
|
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default class SimpleAddUI extends Toggle {
|
||||||
constructor(isShown: UIEventSource<boolean>,
|
constructor(isShown: UIEventSource<boolean>,
|
||||||
filterViewIsOpened: UIEventSource<boolean>,
|
filterViewIsOpened: UIEventSource<boolean>,
|
||||||
state: {
|
state: {
|
||||||
|
featureSwitchIsTesting: UIEventSource<boolean>,
|
||||||
layoutToUse: LayoutConfig,
|
layoutToUse: LayoutConfig,
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
changes: Changes,
|
changes: Changes,
|
||||||
|
@ -155,6 +156,7 @@ export default class SimpleAddUI extends Toggle {
|
||||||
|
|
||||||
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
|
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
|
||||||
state: {
|
state: {
|
||||||
|
featureSwitchIsTesting: UIEventSource<boolean>;
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||||
featureSwitchFilter: UIEventSource<boolean>,
|
featureSwitchFilter: UIEventSource<boolean>,
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
|
@ -162,10 +164,9 @@ export default class SimpleAddUI extends Toggle {
|
||||||
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
|
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
|
||||||
let intro: BaseUIElement = Translations.t.general.add.intro;
|
let intro: BaseUIElement = Translations.t.general.add.intro;
|
||||||
|
|
||||||
let testMode: BaseUIElement = undefined;
|
let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"),
|
||||||
if (state.osmConnection?.userDetails?.data?.dryRun) {
|
undefined,
|
||||||
testMode = Translations.t.general.testing.Clone().SetClass("alert")
|
state.featureSwitchIsTesting);
|
||||||
}
|
|
||||||
|
|
||||||
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
|
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,11 @@ export default class UserBadge extends Toggle {
|
||||||
).SetClass("alert")
|
).SetClass("alert")
|
||||||
}
|
}
|
||||||
|
|
||||||
let dryrun = new FixedUiElement("");
|
let dryrun = new Toggle(
|
||||||
if (user.dryRun) {
|
new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"),
|
||||||
dryrun = new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4");
|
undefined,
|
||||||
}
|
state.featureSwitchIsTesting
|
||||||
|
)
|
||||||
|
|
||||||
const settings =
|
const settings =
|
||||||
new Link(Svg.gear,
|
new Link(Svg.gear,
|
||||||
|
|
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 * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
|
||||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||||
|
import {ImportUtils} from "./ImportUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||||
*/
|
*/
|
||||||
export default class ConflationChecker extends Combine implements FlowStep<any> {
|
export default class ConflationChecker extends Combine implements FlowStep<{features: any[], layer: LayerConfig}> {
|
||||||
|
|
||||||
public readonly IsValid
|
public readonly IsValid
|
||||||
public readonly Value
|
public readonly Value
|
||||||
|
@ -44,19 +46,21 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
||||||
const layer = params.layer;
|
const layer = params.layer;
|
||||||
const toImport = params.geojson;
|
const toImport = params.geojson;
|
||||||
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle")
|
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle")
|
||||||
|
const cacheAge = new UIEventSource<number>(undefined);
|
||||||
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
|
||||||
whenLoaded: (v) => {
|
whenLoaded: (v) => {
|
||||||
if (v !== undefined) {
|
if (v !== undefined) {
|
||||||
console.log("Loaded from local storage:", v)
|
console.log("Loaded from local storage:", v)
|
||||||
const [geojson, date] = v;
|
const [geojson, date] = v;
|
||||||
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
const timeDiff = (new Date().getTime() - date.getTime()) / 1000;
|
||||||
console.log("The cache is ", timeDiff, "seconds old")
|
console.log("Loaded ", geojson.features.length," features; cache is ", timeDiff, "seconds old")
|
||||||
|
cacheAge.setData(timeDiff)
|
||||||
if (timeDiff < 24 * 60 * 60) {
|
if (timeDiff < 24 * 60 * 60) {
|
||||||
// Recently cached!
|
// Recently cached!
|
||||||
overpassStatus.setData("cached")
|
overpassStatus.setData("cached")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cacheAge.setData(-1)
|
||||||
}
|
}
|
||||||
// Load the data!
|
// Load the data!
|
||||||
const url = Constants.defaultOverpassUrls[1]
|
const url = Constants.defaultOverpassUrls[1]
|
||||||
|
@ -115,7 +119,7 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
||||||
layerToShow:new LayerConfig(currentview),
|
layerToShow:new LayerConfig(currentview),
|
||||||
state,
|
state,
|
||||||
leafletMap: osmLiveData.leafletMap,
|
leafletMap: osmLiveData.leafletMap,
|
||||||
enablePopups: undefined,
|
popup: undefined,
|
||||||
zoomToFeatures: true,
|
zoomToFeatures: true,
|
||||||
features: new StaticFeatureSource([
|
features: new StaticFeatureSource([
|
||||||
bbox.asGeoJson({})
|
bbox.asGeoJson({})
|
||||||
|
@ -161,17 +165,10 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
||||||
toImport.features.some(imp =>
|
toImport.features.some(imp =>
|
||||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
||||||
}, [nearbyCutoff.GetValue()]), false);
|
}, [nearbyCutoff.GetValue()]), false);
|
||||||
|
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
|
||||||
|
|
||||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||||
const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => {
|
const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els =>els?.hasNearby ?? []), false);
|
||||||
if(osmData?.features === undefined){
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const maxDist = Number(nearbyCutoff.GetValue().data)
|
|
||||||
return toImport.features.filter(imp =>
|
|
||||||
osmData.features.some(f =>
|
|
||||||
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) )
|
|
||||||
}, [nearbyCutoff.GetValue()]), false);
|
|
||||||
|
|
||||||
new ShowDataLayer({
|
new ShowDataLayer({
|
||||||
layerToShow:layer,
|
layerToShow:layer,
|
||||||
|
@ -192,6 +189,38 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const conflationMaps = new Combine([
|
||||||
|
new VariableUiElement(
|
||||||
|
geojson.map(geojson => {
|
||||||
|
if (geojson === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
|
||||||
|
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
|
||||||
|
mimetype: "application/json+geo"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})),
|
||||||
|
new VariableUiElement(cacheAge.map(age => {
|
||||||
|
if(age === undefined){
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if(age < 0){
|
||||||
|
return new FixedUiElement("Cache was expired")
|
||||||
|
}
|
||||||
|
return new FixedUiElement("Loaded data is from the cache and is "+Utils.toHumanTime(age)+" old")
|
||||||
|
})),
|
||||||
|
|
||||||
|
new Title("Live data on OSM"),
|
||||||
|
osmLiveData,
|
||||||
|
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
|
||||||
|
|
||||||
|
new Title("Nearby features"),
|
||||||
|
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
||||||
|
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
|
||||||
|
"Set the range to 0 or 1 if you want to import them all",
|
||||||
|
matchedFeaturesMap]).SetClass("flex flex-col")
|
||||||
|
|
||||||
super([
|
super([
|
||||||
new Title("Comparison with existing data"),
|
new Title("Comparison with existing data"),
|
||||||
new VariableUiElement(overpassStatus.map(d => {
|
new VariableUiElement(overpassStatus.map(d => {
|
||||||
|
@ -205,38 +234,19 @@ export default class ConflationChecker extends Combine implements FlowStep<any>
|
||||||
return new Loading("Querying overpass...")
|
return new Loading("Querying overpass...")
|
||||||
}
|
}
|
||||||
if(d === "cached"){
|
if(d === "cached"){
|
||||||
return new FixedUiElement("Fetched data from local storage")
|
return conflationMaps
|
||||||
}
|
}
|
||||||
if(d === "success"){
|
if(d === "success"){
|
||||||
return new FixedUiElement("Data loaded")
|
return conflationMaps
|
||||||
}
|
}
|
||||||
return new FixedUiElement("Unexpected state "+d).SetClass("alert")
|
return new FixedUiElement("Unexpected state "+d).SetClass("alert")
|
||||||
})),
|
}))
|
||||||
new VariableUiElement(
|
|
||||||
geojson.map(geojson => {
|
|
||||||
if (geojson === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
|
|
||||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
|
|
||||||
mimetype: "application/json+geo"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})),
|
|
||||||
|
|
||||||
new Title("Live data on OSM"),
|
|
||||||
osmLiveData,
|
|
||||||
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"),
|
|
||||||
|
|
||||||
new Title("Nearby features"),
|
|
||||||
new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
|
|
||||||
new FixedUiElement("The red elements on the following map will <b>not</b> be imported!").SetClass("alert"),
|
|
||||||
"Set the range to 0 or 1 if you want to import them all",
|
|
||||||
matchedFeaturesMap
|
|
||||||
])
|
])
|
||||||
|
|
||||||
this.IsValid = new UIEventSource(false)
|
this.Value = paritionedImport.map(feats => ({features: feats?.noNearby, layer: params.layer}))
|
||||||
this.Value = new UIEventSource(undefined)
|
this.Value.addCallbackAndRun(v => console.log("ConflationChecker-step value is ", v))
|
||||||
}
|
this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
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 {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import {FlowStep} from "./FlowStep";
|
import {FlowStep} from "./FlowStep";
|
||||||
import {Layer} from "leaflet";
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||||
|
import {AllTagsPanel} from "../SpecialVisualizations";
|
||||||
|
import Title from "../Base/Title";
|
||||||
|
|
||||||
|
class PreviewPanel extends ScrollableFullScreen {
|
||||||
|
|
||||||
|
constructor(tags, layer) {
|
||||||
|
super(
|
||||||
|
_ => new FixedUiElement("Element to import"),
|
||||||
|
_ => new Combine(["The tags are:",
|
||||||
|
new AllTagsPanel(tags)
|
||||||
|
]).SetClass("flex flex-col"),
|
||||||
|
"element"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||||
|
@ -36,7 +52,6 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
||||||
const t = Translations.t.importHelper;
|
const t = Translations.t.importHelper;
|
||||||
|
|
||||||
const propertyKeys = new Set<string>()
|
const propertyKeys = new Set<string>()
|
||||||
console.log("Datapanel input got ", geojson)
|
|
||||||
for (const f of geojson.features) {
|
for (const f of geojson.features) {
|
||||||
Object.keys(f.properties).forEach(key => propertyKeys.add(key))
|
Object.keys(f.properties).forEach(key => propertyKeys.add(key))
|
||||||
}
|
}
|
||||||
|
@ -56,6 +71,7 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
||||||
!layer.source.osmTags.matchesProperties(f.properties)
|
!layer.source.osmTags.matchesProperties(f.properties)
|
||||||
)
|
)
|
||||||
if (!mismatched) {
|
if (!mismatched) {
|
||||||
|
console.log("Autodected layer", layer.id)
|
||||||
layerPicker.GetValue().setData(layer);
|
layerPicker.GetValue().setData(layer);
|
||||||
layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
|
layerPicker.GetValue().addCallback(_ => autodetected.setData(false))
|
||||||
autodetected.setData(true)
|
autodetected.setData(true)
|
||||||
|
@ -96,25 +112,22 @@ export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer:
|
||||||
map.SetClass("w-full").SetStyle("height: 500px")
|
map.SetClass("w-full").SetStyle("height: 500px")
|
||||||
|
|
||||||
new ShowDataMultiLayer({
|
new ShowDataMultiLayer({
|
||||||
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers().map(l => ({
|
layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers()
|
||||||
|
.filter(l => l.source.geojsonSource === undefined)
|
||||||
|
.map(l => ({
|
||||||
layerDef: l,
|
layerDef: l,
|
||||||
isDisplayed: new UIEventSource<boolean>(true),
|
isDisplayed: new UIEventSource<boolean>(true),
|
||||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
|
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined)
|
||||||
}))),
|
}))),
|
||||||
zoomToFeatures: true,
|
zoomToFeatures: true,
|
||||||
features: new StaticFeatureSource(matching, false),
|
features: new StaticFeatureSource(matching, false),
|
||||||
state: {
|
|
||||||
...state,
|
|
||||||
filteredLayers: new UIEventSource<FilteredLayer[]>(undefined),
|
|
||||||
backgroundLayer: background
|
|
||||||
},
|
|
||||||
leafletMap: map.leafletMap,
|
leafletMap: map.leafletMap,
|
||||||
|
popup: (tag, layer) => new PreviewPanel(tag, layer).SetClass("font-lg")
|
||||||
})
|
})
|
||||||
var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates]))))
|
var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates]))))
|
||||||
|
|
||||||
super([
|
super([
|
||||||
"Has " + geojson.features.length + " features",
|
new Title(geojson.features.length + " features to import"),
|
||||||
layerPicker,
|
layerPicker,
|
||||||
new Toggle("Automatically detected layer", undefined, autodetected),
|
new Toggle("Automatically detected layer", undefined, autodetected),
|
||||||
new Table(["", "Key", "Values", "Unique values seen"],
|
new Table(["", "Key", "Values", "Unique values seen"],
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
|
|
||||||
export interface FlowStep<T> extends BaseUIElement{
|
export interface FlowStep<T> extends BaseUIElement {
|
||||||
readonly IsValid: UIEventSource<boolean>
|
readonly IsValid: UIEventSource<boolean>
|
||||||
readonly Value: UIEventSource<T>
|
readonly Value: UIEventSource<T>
|
||||||
}
|
}
|
||||||
|
@ -16,70 +16,97 @@ export interface FlowStep<T> extends BaseUIElement{
|
||||||
export class FlowPanelFactory<T> {
|
export class FlowPanelFactory<T> {
|
||||||
private _initial: FlowStep<any>;
|
private _initial: FlowStep<any>;
|
||||||
private _steps: ((x: any) => FlowStep<any>)[];
|
private _steps: ((x: any) => FlowStep<any>)[];
|
||||||
private _stepNames: string[];
|
private _stepNames: (string | BaseUIElement)[];
|
||||||
|
|
||||||
private constructor(initial: FlowStep<any>, steps: ((x:any) => FlowStep<any>)[], stepNames: string[]) {
|
private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) {
|
||||||
this._initial = initial;
|
this._initial = initial;
|
||||||
this._steps = steps;
|
this._steps = steps;
|
||||||
this._stepNames = stepNames;
|
this._stepNames = stepNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static start<TOut> (step: FlowStep<TOut>): FlowPanelFactory<TOut>{
|
public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
|
||||||
return new FlowPanelFactory(step, [], [])
|
return new FlowPanelFactory(step, [], [name])
|
||||||
}
|
}
|
||||||
|
|
||||||
public then<TOut>(name: string, construct: ((t:T) => FlowStep<TOut>)): FlowPanelFactory<TOut>{
|
public then<TOut>(name: string | BaseUIElement, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
|
||||||
return new FlowPanelFactory<TOut>(
|
return new FlowPanelFactory<TOut>(
|
||||||
this._initial,
|
this._initial,
|
||||||
this._steps.concat([construct]),
|
this._steps.concat([construct]),
|
||||||
this._stepNames.concat([name])
|
this._stepNames.concat([name])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public finish(construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)) : BaseUIElement {
|
public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): {
|
||||||
|
flow: BaseUIElement,
|
||||||
|
furthestStep: UIEventSource<number>,
|
||||||
|
titles: (string | BaseUIElement)[]
|
||||||
|
} {
|
||||||
|
const furthestStep = new UIEventSource(0)
|
||||||
// Construct all the flowpanels step by step (in reverse order)
|
// Construct all the flowpanels step by step (in reverse order)
|
||||||
const nextConstr : ((t:any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
|
const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined)
|
||||||
nextConstr.push(construct)
|
nextConstr.push(construct)
|
||||||
|
for (let i = this._steps.length - 1; i >= 0; i--) {
|
||||||
for (let i = this._steps.length - 1; i >= 0; i--){
|
const createFlowStep: (value) => FlowStep<any> = this._steps[i];
|
||||||
const createFlowStep : (value) => FlowStep<any> = this._steps[i];
|
const isConfirm = i == this._steps.length - 1;
|
||||||
nextConstr[i] = (value, backButton) => {
|
nextConstr[i] = (value, backButton) => {
|
||||||
console.log("Creating flowSTep ", this._stepNames[i])
|
|
||||||
const flowStep = createFlowStep(value)
|
const flowStep = createFlowStep(value)
|
||||||
return new FlowPanel(flowStep, nextConstr[i + 1], backButton);
|
furthestStep.setData(i + 1);
|
||||||
|
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm);
|
||||||
|
panel.isActive.addCallbackAndRun(active => {
|
||||||
|
if (active) {
|
||||||
|
furthestStep.setData(i + 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return panel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FlowPanel(this._initial, nextConstr[0],undefined)
|
const flow = new FlowPanel(this._initial, nextConstr[0])
|
||||||
|
flow.isActive.addCallbackAndRun(active => {
|
||||||
|
if (active) {
|
||||||
|
furthestStep.setData(0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
flow,
|
||||||
|
furthestStep,
|
||||||
|
titles: this._stepNames
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlowPanel<T> extends Toggle {
|
export class FlowPanel<T> extends Toggle {
|
||||||
|
public isActive: UIEventSource<boolean>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
initial: (FlowStep<T>),
|
initial: (FlowStep<T>),
|
||||||
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement),
|
||||||
backbutton?: BaseUIElement
|
backbutton?: BaseUIElement,
|
||||||
|
isConfirm = false
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.general;
|
const t = Translations.t.general;
|
||||||
|
|
||||||
const currentStepActive = new UIEventSource(true);
|
const currentStepActive = new UIEventSource(true);
|
||||||
|
|
||||||
let nextStep: UIEventSource<BaseUIElement>= new UIEventSource<BaseUIElement>(undefined)
|
let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined)
|
||||||
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
|
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
|
||||||
currentStepActive.setData(true)
|
currentStepActive.setData(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
let elements : (BaseUIElement | string)[] = []
|
let elements: (BaseUIElement | string)[] = []
|
||||||
if(initial !== undefined){
|
if (initial !== undefined) {
|
||||||
// Startup the flow
|
// Startup the flow
|
||||||
elements = [
|
elements = [
|
||||||
initial,
|
initial,
|
||||||
new Combine([
|
new Combine([
|
||||||
backbutton,
|
backbutton,
|
||||||
new Toggle(
|
new Toggle(
|
||||||
new SubtleButton(Svg.back_svg().SetStyle("transform: rotate(180deg);"), t.next).onClick(() => {
|
new SubtleButton(
|
||||||
|
isConfirm ? Svg.checkmark_svg() :
|
||||||
|
Svg.back_svg().SetStyle("transform: rotate(180deg);"),
|
||||||
|
isConfirm ? t.confirm : t.next
|
||||||
|
).onClick(() => {
|
||||||
const v = initial.Value.data;
|
const v = initial.Value.data;
|
||||||
nextStep.setData(constructNextstep(v, backButtonForNextStep))
|
nextStep.setData(constructNextstep(v, backButtonForNextStep))
|
||||||
currentStepActive.setData(false)
|
currentStepActive.setData(false)
|
||||||
|
@ -88,18 +115,18 @@ export class FlowPanel<T> extends Toggle {
|
||||||
initial.IsValid
|
initial.IsValid
|
||||||
)
|
)
|
||||||
]).SetClass("flex w-full justify-end space-x-2")
|
]).SetClass("flex w-full justify-end space-x-2")
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
super(
|
super(
|
||||||
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
|
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
|
||||||
new VariableUiElement(nextStep),
|
new VariableUiElement(nextStep),
|
||||||
currentStepActive
|
currentStepActive
|
||||||
);
|
);
|
||||||
|
this.isActive = currentStepActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,11 +9,18 @@ import MoreScreen from "../BigComponents/MoreScreen";
|
||||||
import MinimapImplementation from "../Base/MinimapImplementation";
|
import MinimapImplementation from "../Base/MinimapImplementation";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import {FlowPanel, FlowPanelFactory} from "./FlowStep";
|
import {FlowPanelFactory} from "./FlowStep";
|
||||||
import {RequestFile} from "./RequestFile";
|
import {RequestFile} from "./RequestFile";
|
||||||
import {DataPanel} from "./DataPanel";
|
import {DataPanel} from "./DataPanel";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
|
||||||
import ConflationChecker from "./ConflationChecker";
|
import ConflationChecker from "./ConflationChecker";
|
||||||
|
import {AskMetadata} from "./AskMetadata";
|
||||||
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
|
import {ConfirmProcess} from "./ConfirmProcess";
|
||||||
|
import {CreateNotes} from "./CreateNotes";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
import List from "../Base/List";
|
||||||
|
import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes";
|
||||||
|
|
||||||
export default class ImportHelperGui extends LoginToggle {
|
export default class ImportHelperGui extends LoginToggle {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -24,28 +31,51 @@ export default class ImportHelperGui extends LoginToggle {
|
||||||
// We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
|
// We disable the userbadge, as various 'showData'-layers will give a read-only view in this case
|
||||||
state.featureSwitchUserbadge.setData(false)
|
state.featureSwitchUserbadge.setData(false)
|
||||||
|
|
||||||
|
const {flow, furthestStep, titles} =
|
||||||
|
FlowPanelFactory
|
||||||
|
.start("Select file", new RequestFile())
|
||||||
|
.then("Inspect data", geojson => new DataPanel(state, geojson))
|
||||||
|
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
|
||||||
|
.then("Compare with existing data", v => new ConflationChecker(state, v))
|
||||||
|
.then("License and community check", v => new ConfirmProcess(v))
|
||||||
|
.then("Metadata", (v:{features:any[], layer: LayerConfig}) => new AskMetadata(v))
|
||||||
|
.finish("Note creation", v => new CreateNotes(state, v));
|
||||||
|
|
||||||
|
const toc = new List(
|
||||||
|
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
|
||||||
|
if(i > currentStep){
|
||||||
|
return new Combine([title]).SetClass("subtle");
|
||||||
|
}
|
||||||
|
if(i == currentStep){
|
||||||
|
return new Combine([title]).SetClass("font-bold");
|
||||||
|
}
|
||||||
|
if(i < currentStep){
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})))
|
||||||
|
, true)
|
||||||
|
|
||||||
const leftContents: BaseUIElement[] = [
|
const leftContents: BaseUIElement[] = [
|
||||||
new BackToIndex().SetClass("block pl-4"),
|
new BackToIndex().SetClass("block pl-4"),
|
||||||
|
toc,
|
||||||
|
new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting),
|
||||||
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
|
||||||
].map(el => el?.SetClass("pl-4"))
|
].map(el => el?.SetClass("pl-4"))
|
||||||
|
|
||||||
const leftBar = new Combine([
|
const leftBar = new Combine([
|
||||||
new Combine(leftContents).SetClass("sticky top-4 m-4")
|
new Combine(leftContents).SetClass("sticky top-4 m-4"),
|
||||||
]).SetClass("block w-full md:w-2/6 lg:w-1/6")
|
]).SetClass("block w-full md:w-2/6 lg:w-1/6")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const mainPanel =
|
|
||||||
FlowPanelFactory
|
|
||||||
.start(new RequestFile())
|
|
||||||
.then("datapanel", geojson => new DataPanel(state, geojson))
|
|
||||||
.then("conflation", v => new ConflationChecker(state, v))
|
|
||||||
.finish(_ => new FixedUiElement("All done!"))
|
|
||||||
|
|
||||||
super(
|
super(
|
||||||
new Toggle(
|
new Toggle(
|
||||||
new Combine([
|
new Combine([
|
||||||
leftBar,
|
leftBar,
|
||||||
mainPanel.SetClass("m-8 w-full mb-24")
|
flow.SetClass("m-8 w-full mb-24")
|
||||||
]).SetClass("h-full block md:flex")
|
]).SetClass("h-full block md:flex")
|
||||||
|
|
||||||
,
|
,
|
||||||
|
|
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>,
|
mapBackgroundLayer?: UIEventSource<any>,
|
||||||
unit?: Unit,
|
unit?: Unit,
|
||||||
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
||||||
feature?: any
|
feature?: any,
|
||||||
|
inputStyle?: string
|
||||||
}): InputElement<string> {
|
}): InputElement<string> {
|
||||||
options = options ?? {};
|
options = options ?? {};
|
||||||
options.placeholder = options.placeholder ?? type;
|
options.placeholder = options.placeholder ?? type;
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: {
|
||||||
|
featureSwitchIsTesting: UIEventSource<boolean>;
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
featurePipeline: FeaturePipeline,
|
featurePipeline: FeaturePipeline,
|
||||||
backgroundLayer?: UIEventSource<BaseLayer>
|
backgroundLayer?: UIEventSource<BaseLayer>
|
||||||
|
@ -167,8 +168,11 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
).onClick(cancel)
|
).onClick(cancel)
|
||||||
|
|
||||||
super([
|
super([
|
||||||
state.osmConnection.userDetails.data.dryRun ?
|
new Toggle(
|
||||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
Translations.t.general.testing.SetClass("alert"),
|
||||||
|
undefined,
|
||||||
|
state.featureSwitchIsTesting
|
||||||
|
),
|
||||||
disableFiltersOrConfirm,
|
disableFiltersOrConfirm,
|
||||||
cancelButton,
|
cancelButton,
|
||||||
preset.description,
|
preset.description,
|
||||||
|
|
|
@ -141,7 +141,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){
|
if(tagSpec.indexOf(" ")< 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined){
|
||||||
// This is probably a key
|
// This is probably a key
|
||||||
tagSpec = tagSource.data[args.tags]
|
tagSpec = tagSource.data[args.tags]
|
||||||
console.warn("Using tagspec tagSource.data["+args.tags+"] which is ",tagSpec)
|
console.debug("The import button is using tags from properties["+args.tags+"] of this object, namely ",tagSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
const importClicked = new UIEventSource(false);
|
const importClicked = new UIEventSource(false);
|
||||||
|
@ -201,7 +201,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){
|
if(tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined){
|
||||||
// This might be a property to expand...
|
// This might be a property to expand...
|
||||||
const items : string = originalFeatureTags.data[tags]
|
const items : string = originalFeatureTags.data[tags]
|
||||||
console.warn("Using tagspec tagSource.data["+tags+"] which is ",items)
|
console.debug("The import button is using tags from properties["+tags+"] of this object, namely ",items)
|
||||||
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
|
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
|
||||||
}else{
|
}else{
|
||||||
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
|
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
|
||||||
|
|
|
@ -55,6 +55,45 @@ export interface SpecialVisualization {
|
||||||
getLayerDependencies?: (argument: string[]) => string[]
|
getLayerDependencies?: (argument: string[]) => string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AllTagsPanel extends VariableUiElement {
|
||||||
|
|
||||||
|
constructor(tags: UIEventSource<any>, state?) {
|
||||||
|
|
||||||
|
const calculatedTags = [].concat(
|
||||||
|
SimpleMetaTagger.lazyTags,
|
||||||
|
...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? []))
|
||||||
|
|
||||||
|
|
||||||
|
super(tags.map(tags => {
|
||||||
|
const parts = [];
|
||||||
|
for (const key in tags) {
|
||||||
|
if (!tags.hasOwnProperty(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let v = tags[key]
|
||||||
|
if (v === "") {
|
||||||
|
v = "<b>empty string</b>"
|
||||||
|
}
|
||||||
|
parts.push([key, v ?? "<b>undefined</b>"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of calculatedTags) {
|
||||||
|
const value = tags[key]
|
||||||
|
if (value === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts.push(["<i>" + key + "</i>", value])
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Table(
|
||||||
|
["key", "value"],
|
||||||
|
parts
|
||||||
|
)
|
||||||
|
.SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table")
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class SpecialVisualizations {
|
export default class SpecialVisualizations {
|
||||||
|
|
||||||
public static specialVisualizations = SpecialVisualizations.init()
|
public static specialVisualizations = SpecialVisualizations.init()
|
||||||
|
@ -99,37 +138,7 @@ export default class SpecialVisualizations {
|
||||||
funcName: "all_tags",
|
funcName: "all_tags",
|
||||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||||
args: [],
|
args: [],
|
||||||
constr: ((state, tags: UIEventSource<any>) => {
|
constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state))
|
||||||
const calculatedTags = [].concat(
|
|
||||||
SimpleMetaTagger.lazyTags,
|
|
||||||
...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? []))
|
|
||||||
return new VariableUiElement(tags.map(tags => {
|
|
||||||
const parts = [];
|
|
||||||
for (const key in tags) {
|
|
||||||
if (!tags.hasOwnProperty(key)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let v = tags[key]
|
|
||||||
if (v === "") {
|
|
||||||
v = "<b>empty string</b>"
|
|
||||||
}
|
|
||||||
parts.push([key, v ?? "<b>undefined</b>"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of calculatedTags) {
|
|
||||||
const value = tags[key]
|
|
||||||
if (value === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts.push(["<i>" + key + "</i>", value])
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Table(
|
|
||||||
["key", "value"],
|
|
||||||
parts
|
|
||||||
)
|
|
||||||
})).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table")
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
funcName: "image_carousel",
|
funcName: "image_carousel",
|
||||||
|
@ -339,7 +348,7 @@ export default class SpecialVisualizations {
|
||||||
const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat),
|
const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat),
|
||||||
encodeURIComponent(subject),
|
encodeURIComponent(subject),
|
||||||
state.mangroveIdentity,
|
state.mangroveIdentity,
|
||||||
state.osmConnection._dryRun
|
state.featureSwitchIsTesting.data
|
||||||
);
|
);
|
||||||
const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection);
|
const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection);
|
||||||
return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form);
|
return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form);
|
||||||
|
@ -743,10 +752,6 @@ export default class SpecialVisualizations {
|
||||||
return t.addCommentAndClose
|
return t.addCommentAndClose
|
||||||
}))).onClick(() => {
|
}))).onClick(() => {
|
||||||
const id = tags.data[args[1] ?? "id"]
|
const id = tags.data[args[1] ?? "id"]
|
||||||
if (state.featureSwitchIsTesting.data) {
|
|
||||||
console.log("Testmode: Not actually closing note...")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.osmConnection.closeNote(id, txt.data).then(_ => {
|
state.osmConnection.closeNote(id, txt.data).then(_ => {
|
||||||
tags.data["closed_at"] = new Date().toISOString();
|
tags.data["closed_at"] = new Date().toISOString();
|
||||||
tags.ping()
|
tags.ping()
|
||||||
|
@ -760,10 +765,6 @@ export default class SpecialVisualizations {
|
||||||
return t.reopenNoteAndComment
|
return t.reopenNoteAndComment
|
||||||
}))).onClick(() => {
|
}))).onClick(() => {
|
||||||
const id = tags.data[args[1] ?? "id"]
|
const id = tags.data[args[1] ?? "id"]
|
||||||
if (state.featureSwitchIsTesting.data) {
|
|
||||||
console.log("Testmode: Not actually reopening note...")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.osmConnection.reopenNote(id, txt.data).then(_ => {
|
state.osmConnection.reopenNote(id, txt.data).then(_ => {
|
||||||
tags.data["closed_at"] = undefined;
|
tags.data["closed_at"] = undefined;
|
||||||
tags.ping()
|
tags.ping()
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)",
|
"description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)",
|
||||||
"source": {
|
"source": {
|
||||||
"osmTags": "id~*",
|
"osmTags": "id~*",
|
||||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}",
|
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=7&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||||
"geoJsonZoomLevel": 12,
|
"geoJsonZoomLevel": 12,
|
||||||
"maxCacheAge": 0
|
"maxCacheAge": 0
|
||||||
},
|
},
|
||||||
|
@ -29,7 +29,8 @@
|
||||||
"_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined",
|
"_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined",
|
||||||
"_first_user:=feat.get('comments')[0].user",
|
"_first_user:=feat.get('comments')[0].user",
|
||||||
"_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()",
|
"_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()",
|
||||||
"_first_user_id:=feat.get('comments')[0].uid"
|
"_first_user_id:=feat.get('comments')[0].uid",
|
||||||
|
"_is_import_note:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()"
|
||||||
],
|
],
|
||||||
"titleIcons": [
|
"titleIcons": [
|
||||||
{
|
{
|
||||||
|
@ -201,6 +202,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "no_imports",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"osmTags": "_is_import_note=",
|
||||||
|
"question": {
|
||||||
|
"en": "Hide import notes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1493,6 +1493,10 @@ video {
|
||||||
padding: 0.125rem;
|
padding: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-8 {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pb-12 {
|
.pb-12 {
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -465,7 +465,9 @@
|
||||||
"importLayer": {
|
"importLayer": {
|
||||||
"layerName": "Possible {title}",
|
"layerName": "Possible {title}",
|
||||||
"description": "A layer which imports entries for {title}",
|
"description": "A layer which imports entries for {title}",
|
||||||
"popupTitle": "Possible {title}"
|
"popupTitle": "Possible {title}",
|
||||||
|
"importButton": "import_button({layerId}, _tags, There might be a {title} here,./assets/svg/addSmall.svg,,,id)",
|
||||||
|
"importHandled": "<div class='thanks'>This feature has been handled! Thanks for your effort</div>"
|
||||||
},
|
},
|
||||||
"importHelper": {
|
"importHelper": {
|
||||||
"title": "Import helper",
|
"title": "Import helper",
|
||||||
|
@ -477,5 +479,5 @@
|
||||||
"selectLayer": "Select a layer...",
|
"selectLayer": "Select a layer...",
|
||||||
"selectFileTitle": "Select file",
|
"selectFileTitle": "Select file",
|
||||||
"validateDataTitle": "Validate data"
|
"validateDataTitle": "Validate data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3354,6 +3354,13 @@
|
||||||
"question": "Only show open notes"
|
"question": "Only show open notes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Hide import notes"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "OpenStreetMap notes",
|
"name": "OpenStreetMap notes",
|
||||||
|
|
|
@ -314,5 +314,12 @@
|
||||||
},
|
},
|
||||||
"multi_apply": {
|
"multi_apply": {
|
||||||
"autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten."
|
"autoApply": "Wijzigingen aan eigenschappen {attr_names} zullen ook worden uitgevoerd op {count} andere objecten."
|
||||||
|
},
|
||||||
|
"importLayer": {
|
||||||
|
"layerName": "Hier is misschien een {title}",
|
||||||
|
"description": "Deze laag toont kaart-nota's die wijzen op een {title}",
|
||||||
|
"popupTitle": "Mogelijkse {title}",
|
||||||
|
"importButton": "import_button({layerId}, _tags, Hier is een {title}, voeg toe...,./assets/svg/addSmall.svg,,,id)",
|
||||||
|
"importHandled": "<div class='thanks'>Dit punt is afgehandeld. Bedankt om mee te helpen!</div>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,6 @@ import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||||
import Constants from "../Models/Constants";
|
import Constants from "../Models/Constants";
|
||||||
import {
|
import {
|
||||||
DesugaringContext,
|
|
||||||
PrepareLayer,
|
|
||||||
PrepareTheme,
|
|
||||||
ValidateLayer,
|
ValidateLayer,
|
||||||
ValidateThemeAndLayers
|
ValidateThemeAndLayers
|
||||||
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
||||||
|
@ -16,6 +13,9 @@ import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingCon
|
||||||
import * as questions from "../assets/tagRenderings/questions.json";
|
import * as questions from "../assets/tagRenderings/questions.json";
|
||||||
import * as icons from "../assets/tagRenderings/icons.json";
|
import * as icons from "../assets/tagRenderings/icons.json";
|
||||||
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson";
|
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson";
|
||||||
|
import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer";
|
||||||
|
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||||
|
import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion";
|
||||||
|
|
||||||
// This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
|
// This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files.
|
||||||
// It spits out an overview of those to be used to load them
|
// It spits out an overview of those to be used to load them
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import T from "./TestHelper";
|
import T from "./TestHelper";
|
||||||
import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
import CreateNoteImportLayer from "../Models/ThemeConfig/Conversion/CreateNoteImportLayer";
|
||||||
import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json"
|
import * as bookcases from "../assets/layers/public_bookcase/public_bookcase.json"
|
||||||
import {DesugaringContext, PrepareLayer} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion";
|
||||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||||
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||||
|
import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer";
|
||||||
|
|
||||||
export default class CreateNoteImportLayerSpec extends T {
|
export default class CreateNoteImportLayerSpec extends T {
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ export default class CreateNoteImportLayerSpec extends T {
|
||||||
|
|
||||||
}
|
}
|
||||||
const layerPrepare = new PrepareLayer()
|
const layerPrepare = new PrepareLayer()
|
||||||
const layer = new LayerConfig(layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases"), "ImportLayerGeneratorTest: init bookcases-layer")
|
const layer =layerPrepare.convertStrict(desugaringState, bookcases, "ImportLayerGeneratorTest:Parse bookcases")
|
||||||
const generator = new CreateNoteImportLayer()
|
const generator = new CreateNoteImportLayer()
|
||||||
const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert")
|
const generatedLayer = generator.convertStrict(desugaringState, layer, "ImportLayerGeneratorTest: convert")
|
||||||
// fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, " "), "utf8")
|
// fs.writeFileSync("bookcases-import-layer.generated.json", JSON.stringify(generatedLayer, null, " "), "utf8")
|
||||||
|
|
|
@ -112,6 +112,12 @@ export default class TagSpec extends T {
|
||||||
equal(compare.matchesProperties({"key": "5"}), true);
|
equal(compare.matchesProperties({"key": "5"}), true);
|
||||||
equal(compare.matchesProperties({"key": "4.2"}), false);
|
equal(compare.matchesProperties({"key": "4.2"}), false);
|
||||||
|
|
||||||
|
const importMatch = TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)")
|
||||||
|
equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}), true)
|
||||||
|
equal(importMatch.matchesProperties({"tags": "amenity=public_bookcase"}), true)
|
||||||
|
equal(importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}), true)
|
||||||
|
equal(importMatch.matchesProperties({"tags": "amenity=bench"}), false)
|
||||||
|
|
||||||
})],
|
})],
|
||||||
["Is equivalent test", (() => {
|
["Is equivalent test", (() => {
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ import * as assert from "assert";
|
||||||
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||||
import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json"
|
import * as bookcaseLayer from "../assets/generated/layers/public_bookcase.json"
|
||||||
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert";
|
|
||||||
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||||
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
||||||
import Constants from "../Models/Constants";
|
import Constants from "../Models/Constants";
|
||||||
|
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||||
|
|
||||||
export default class ThemeSpec extends T {
|
export default class ThemeSpec extends T {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue