Merge master

This commit is contained in:
Pieter Vander Vennet 2022-06-22 20:06:41 +02:00
commit 98c6113cbe
87 changed files with 3860 additions and 412 deletions

View file

@ -126,7 +126,7 @@ export default class DetermineLayout {
.AttachTo("centermessage");
}
private static prepCustomTheme(json: any, sourceUrl?: string): LayoutConfig {
private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig {
if(json.layers === undefined && json.tagRenderings !== undefined){
const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined)
@ -161,6 +161,7 @@ export default class DetermineLayout {
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
console.log("The layoutconfig is ", json)
json.id = forceId ?? json.id
return new LayoutConfig(json, false, {
definitionRaw: JSON.stringify(raw, null, " "),
@ -178,9 +179,13 @@ export default class DetermineLayout {
let parsed = await Utils.downloadJson(link)
try {
parsed.id = link;
let forcedId = parsed.id
const url = new URL(link)
if(!(url.hostname === "localhost" || url.hostname === "127.0.0.1")){
forcedId = link;
}
console.log("Loaded remote link:", link)
return DetermineLayout.prepCustomTheme(parsed, link)
return DetermineLayout.prepCustomTheme(parsed, link, forcedId);
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(

View file

@ -5,6 +5,7 @@ import BaseUIElement from "../UI/BaseUIElement";
import List from "../UI/Base/List";
import Title from "../UI/Base/Title";
import {BBox} from "./BBox";
import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf";
export interface ExtraFuncParams {
/**
@ -12,9 +13,9 @@ export interface ExtraFuncParams {
* Note that more features then requested can be given back.
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
*/
getFeaturesWithin: (layerId: string, bbox: BBox) => any[][],
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, {id: string}>[][],
memberships: RelationsTracker
getFeatureById: (id: string) => any
getFeatureById: (id: string) => Feature<Geometry, {id: string}>
}
/**
@ -24,21 +25,65 @@ interface ExtraFunction {
readonly _name: string;
readonly _args: string[];
readonly _doc: string;
readonly _f: (params: ExtraFuncParams, feat: any) => any;
readonly _f: (params: ExtraFuncParams, feat: Feature<Geometry, any>) => any;
}
class EnclosingFunc implements ExtraFunction {
_name = "enclosingFeatures"
_doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)","",
"The result is a list of features: `{feat: Polygon}[]`",
"This function will never return the feature itself."].join("\n")
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
_f(params: ExtraFuncParams, feat: Feature<Geometry, any>) {
return (...layerIds: string[]) => {
const result: { feat: any }[] = []
const bbox = BBox.get(feat)
const seenIds = new Set<string>()
seenIds.add(feat.properties.id)
for (const layerId of layerIds) {
const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
if (otherFeaturess === undefined) {
continue;
}
if (otherFeaturess.length === 0) {
continue;
}
for (const otherFeatures of otherFeaturess) {
for (const otherFeature of otherFeatures) {
if(seenIds.has(otherFeature.properties.id)){
continue
}
seenIds.add(otherFeature.properties.id)
if(otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon"){
continue;
}
if(GeoOperations.completelyWithin(feat, <Feature<Polygon | MultiPolygon, any>> otherFeature)){
result.push({feat: otherFeature})
}
}
}
}
return result;
}
}
}
class OverlapFunc implements ExtraFunction {
_name = "overlapWith";
_doc = "Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well." +
"If the current feature is a point, all features that this point is embeded in are given.\n\n" +
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" +
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list\n" +
"\n" +
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`"
_doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.",
"If the current feature is a point, all features that this point is embeded in are given." ,
"",
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point." ,
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list." ,
"",
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
"",
"Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature"
].join("\n")
_args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
_f(params, feat) {
@ -46,15 +91,15 @@ class OverlapFunc implements ExtraFunction {
const result: { feat: any, overlap: number }[] = []
const bbox = BBox.get(feat)
for (const layerId of layerIds) {
const otherLayers = params.getFeaturesWithin(layerId, bbox)
if (otherLayers === undefined) {
const otherFeaturess = params.getFeaturesWithin(layerId, bbox)
if (otherFeaturess === undefined) {
continue;
}
if (otherLayers.length === 0) {
if (otherFeaturess.length === 0) {
continue;
}
for (const otherLayer of otherLayers) {
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
for (const otherFeatures of otherFeaturess) {
result.push(...GeoOperations.calculateOverlap(feat, otherFeatures));
}
}
@ -392,6 +437,7 @@ export class ExtraFunctions {
private static readonly allFuncs: ExtraFunction[] = [
new DistanceToFunc(),
new OverlapFunc(),
new EnclosingFunc(),
new IntersectionFunc(),
new ClosestObjectFunc(),
new ClosestNObjectFunc(),

View file

@ -79,6 +79,9 @@ export default class SaveTileToLocalStorageActor {
}
loadedTiles.add(key)
this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => {
if(features === undefined){
return;
}
console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk")
const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features))
registerTile(src)

View file

@ -23,6 +23,7 @@ import TileFreshnessCalculator from "./TileFreshnessCalculator";
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
import MapState from "../State/MapState";
import {ElementStorage} from "../ElementStorage";
import {Feature, Geometry} from "@turf/turf";
/**
@ -337,7 +338,7 @@ export default class FeaturePipeline {
}
public GetAllFeaturesWithin(bbox: BBox): any[][] {
public GetAllFeaturesWithin(bbox: BBox): Feature<Geometry, {id: string}>[][] {
const self = this
const tiles = []
Array.from(this.perLayerHierarchy.keys())

View file

@ -1,13 +1,10 @@
/**
* Merges features from different featureSources for a single layer
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
*/
import {UIEventSource} from "../../UIEventSource";
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox";
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
@ -17,7 +14,10 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
public readonly bbox: BBox;
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
private readonly _sources: UIEventSource<FeatureSource[]>;
/**
* Merges features from different featureSources for a single layer
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
*/
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
this.tileIndex = tileIndex;
this.bbox = bbox;

View file

@ -1,28 +1,94 @@
/**
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
*/
import {Store, UIEventSource} from "../../UIEventSource";
import {Store} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
import FeatureSource from "../FeatureSource";
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig";
export default class RenderingMultiPlexerFeatureSource {
public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[];
private centroidRenderings: { rendering: PointRenderingConfig; index: number }[];
private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[];
private startRenderings: { rendering: PointRenderingConfig; index: number }[];
private endRenderings: { rendering: PointRenderingConfig; index: number }[];
private hasCentroid: boolean;
private lineRenderObjects: LineRenderingConfig[];
private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){
if (feat.geometry.type === "Point") {
for (const rendering of this.pointRenderings) {
withIndex.push({
...feat,
pointRenderingIndex: rendering.index
})
}
} else {
// This is a a line: add the centroids
let centerpoint: [number, number] = undefined;
let projectedCenterPoint : [number, number] = undefined
if(this.hasCentroid){
centerpoint = GeoOperations.centerpointCoordinates(feat)
if(this.projectedCentroidRenderings.length > 0){
projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
}
}
for (const rendering of this.centroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
if (feat.geometry.type === "LineString") {
for (const rendering of this.projectedCentroidRenderings) {
addAsPoint(feat, rendering, projectedCenterPoint)
}
// Add start- and endpoints
const coordinates = feat.geometry.coordinates
for (const rendering of this.startRenderings) {
addAsPoint(feat, rendering, coordinates[0])
}
for (const rendering of this.endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
}else{
for (const rendering of this.projectedCentroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
}
// AT last, add it 'as is' to what we should render
for (let i = 0; i < this.lineRenderObjects.length; i++) {
withIndex.push({
...feat,
lineRenderingIndex: i
})
}
}
}
constructor(upstream: FeatureSource, layer: LayerConfig) {
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
rendering: r,
index: i
}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
const hasCentroid = centroidRenderings.length > 0 || projectedCentroidRenderings.length > 0
const lineRenderObjects = layer.lineRendering
this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
this.lineRenderObjects = layer.lineRendering
this.features = upstream.features.map(
features => {
@ -31,8 +97,7 @@ export default class RenderingMultiPlexerFeatureSource {
}
const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined, multiLineStringIndex: number | undefined })[] = [];
const withIndex: any[] = [];
function addAsPoint(feat, rendering, coordinate) {
const patched = {
@ -45,68 +110,14 @@ export default class RenderingMultiPlexerFeatureSource {
}
withIndex.push(patched)
}
for (const f of features) {
const feat = f.feature;
if(feat === undefined){
continue
}
if(feat.geometry === undefined){
console.error("No geometry in ", feat,"provided by", upstream.features.tag, upstream.name)
}
if (feat.geometry.type === "Point") {
for (const rendering of pointRenderings) {
withIndex.push({
...feat,
pointRenderingIndex: rendering.index
})
}
continue
}
// This is a a line: add the centroids
let centerpoint: [number, number] = undefined;
let projectedCenterPoint: [number, number] = undefined
if (hasCentroid) {
centerpoint = GeoOperations.centerpointCoordinates(feat)
if (projectedCentroidRenderings.length > 0) {
projectedCenterPoint = <[number, number]>GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
}
}
for (const rendering of centroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
if (feat.geometry.type === "LineString") {
for (const rendering of projectedCentroidRenderings) {
addAsPoint(feat, rendering, projectedCenterPoint)
}
// Add start- and endpoints
const coordinates = feat.geometry.coordinates
for (const rendering of startRenderings) {
addAsPoint(feat, rendering, coordinates[0])
}
for (const rendering of endRenderings) {
const coordinate = coordinates[coordinates.length - 1]
addAsPoint(feat, rendering, coordinate)
}
} else {
for (const rendering of projectedCentroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
}
// AT last, add it 'as is' to what we should render
for (let i = 0; i < lineRenderObjects.length; i++) {
withIndex.push({
...feat,
lineRenderingIndex: i
})
}
this.inspectFeature(feat, addAsPoint, withIndex)
}

View file

@ -7,7 +7,6 @@ import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox";
import {OsmConnection} from "../../Osm/OsmConnection";
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import {Or} from "../../Tags/Or";
import {TagsFilter} from "../../Tags/TagsFilter";
@ -27,65 +26,71 @@ export default class OsmFeatureSource {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: Store<boolean>,
neededTiles: Store<number[]>,
state: {
readonly osmConnection: OsmConnection;
},
markTileVisited?: (tileId: number) => void
};
private readonly allowedTags: TagsFilter;
/**
*
* @param options: allowedFeatures is normally calculated from the layoutToUse
*/
constructor(options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: Store<boolean>,
neededTiles: Store<number[]>,
state: {
readonly filteredLayers: UIEventSource<FilteredLayer[]>;
readonly osmConnection: OsmConnection;
readonly layoutToUse: LayoutConfig
readonly osmConnection: {
Backend(): string
};
readonly layoutToUse?: LayoutConfig
},
readonly allowedFeatures?: TagsFilter,
markTileVisited?: (tileId: number) => void
}) {
this.options = options;
this._backend = options.state.osmConnection._oauth_config.url;
this._backend = options.state.osmConnection.Backend();
this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined))
this.handleTile = options.handleTile
this.isActive = options.isActive
const self = this
options.neededTiles.addCallbackAndRunD(neededTiles => {
if (options.isActive?.data === false) {
return;
}
neededTiles = neededTiles.filter(tile => !self.downloadedTiles.has(tile))
if (neededTiles.length == 0) {
return;
}
self.isRunning.setData(true)
try {
for (const neededTile of neededTiles) {
self.downloadedTiles.add(neededTile)
self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => {
console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded from OSM")
})
}
} catch (e) {
console.error(e)
} finally {
self.isRunning.setData(false)
}
self.Update(neededTiles)
})
const neededLayers = (options.state.layoutToUse?.layers ?? [])
.filter(layer => !layer.doNotDownload)
.filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer)
this.allowedTags = new Or(neededLayers.map(l => l.source.osmTags))
this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags))
}
private async LoadTile(z, x, y): Promise<void> {
private async Update(neededTiles: number[]) {
if (this.options.isActive?.data === false) {
return;
}
neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile))
if (neededTiles.length == 0) {
return;
}
this.isRunning.setData(true)
try {
for (const neededTile of neededTiles) {
this.downloadedTiles.add(neededTile)
this.LoadTile(...Tiles.tile_from_index(neededTile))
}
} catch (e) {
console.error(e)
} finally {
this.isRunning.setData(false)
}
}
private LoadTile(z, x, y): void {
if (z > 25) {
throw "This is an absurd high zoom level"
}
@ -96,11 +101,10 @@ export default class OsmFeatureSource {
const bbox = BBox.fromTile(z, x, y)
const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
try {
const osmJson = await Utils.downloadJson(url)
Utils.downloadJson(url).then(osmJson => {
try {
console.debug("Got tile", z, x, y, "from the osm api")
console.log("Got tile", z, x, y, "from the osm api")
this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
const geojson = OsmToGeoJson.default(osmJson,
// @ts-ignore
@ -130,17 +134,18 @@ export default class OsmFeatureSource {
} catch (e) {
console.error("Weird error: ", e)
}
} catch (e) {
console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds")
if (e === "rate limited") {
})
.catch(e => {
console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds")
if (e === "rate limited") {
return;
}
this.LoadTile(z + 1, x * 2, y * 2)
this.LoadTile(z + 1, 1 + x * 2, y * 2)
this.LoadTile(z + 1, x * 2, 1 + y * 2)
this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2)
return;
}
await this.LoadTile(z + 1, x * 2, y * 2)
await this.LoadTile(z + 1, 1 + x * 2, y * 2)
await this.LoadTile(z + 1, x * 2, 1 + y * 2)
await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2)
return;
}
})
}

View file

@ -3,7 +3,7 @@ import {BBox} from "./BBox";
import togpx from "togpx"
import Constants from "../Models/Constants";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {Coord} from "@turf/turf";
import {booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf";
export class GeoOperations {
@ -142,7 +142,10 @@ export class GeoOperations {
return result;
}
public static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) {
/**
* Helper function which does the heavy lifting for 'inside'
*/
private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) {
const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0])
if (!inside) {
return false;
@ -737,6 +740,38 @@ export class GeoOperations {
return turf.bearing(a, b)
}
/**
* Returns 'true' if one feature contains the other feature
*
* const pond: Feature<Polygon, any> = {
* "type": "Feature",
* "properties": {"natural":"water","water":"pond"},
* "geometry": {
* "type": "Polygon",
* "coordinates": [[
* [4.362924098968506,50.8435422298544 ],
* [4.363272786140442,50.8435219059949 ],
* [4.363213777542114,50.8437420806679 ],
* [4.362924098968506,50.8435422298544 ]
* ]]}}
* const park: Feature<Polygon, any> = {
* "type": "Feature",
* "properties": {"leisure":"park"},
* "geometry": {
* "type": "Polygon",
* "coordinates": [[
* [ 4.36073541641235,50.84323737103244 ],
* [ 4.36469435691833, 50.8423905305197 ],
* [ 4.36659336090087, 50.8458997374786 ],
* [ 4.36254858970642, 50.8468007074916 ],
* [ 4.36073541641235, 50.8432373710324 ]
* ]]}}
* GeoOperations.completelyWithin(pond, park) // => true
* GeoOperations.completelyWithin(park, pond) // => false
*/
static completelyWithin(feature: Feature<Geometry, any>, possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>) : boolean {
return booleanWithin(feature, possiblyEncloingFeature);
}
}

View file

@ -155,7 +155,6 @@ export default class MetaTagging {
// Lazy function
const f = (feature: any) => {
const oldValue = feature.properties[key]
delete feature.properties[key]
Object.defineProperty(feature.properties, key, {
configurable: true,

View file

@ -145,6 +145,10 @@ export class OsmConnection {
console.log("Logged out")
this.loadingStatus.setData("not-attempted")
}
public Backend(): string {
return this._oauth_config.url;
}
public AttemptLogin() {
this.loadingStatus.setData("loading")

View file

@ -1,10 +1,12 @@
import {UIEventSource} from "../UIEventSource";
import UserDetails, {OsmConnection} from "./OsmConnection";
import {Utils} from "../../Utils";
import {DomEvent} from "leaflet";
import preventDefault = DomEvent.preventDefault;
export class OsmPreferences {
public preferences = new UIEventSource<any>({}, "all-osm-preferences");
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences");
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private auth: any;
private userDetails: UIEventSource<UserDetails>;
@ -35,7 +37,7 @@ export class OsmPreferences {
const allStartWith = prefix + key + "-combined";
// Gives the number of combined preferences
const length = this.GetPreference(allStartWith + "-length", "");
const length = this.GetPreference(allStartWith + "-length", "", "");
if( (allStartWith + "-length").length > 255){
throw "This preference key is too long, it has "+key.length+" characters, but at most "+(255 - "-length".length - "-combined".length - prefix.length)+" characters are allowed"
@ -51,10 +53,10 @@ export class OsmPreferences {
let count = parseInt(length.data);
for (let i = 0; i < count; i++) {
// Delete all the preferences
self.GetPreference(allStartWith + "-" + i, "")
self.GetPreference(allStartWith + "-" + i, "", "")
.setData("");
}
self.GetPreference(allStartWith + "-length", "")
self.GetPreference(allStartWith + "-length", "", "")
.setData("");
return
}
@ -67,7 +69,7 @@ export class OsmPreferences {
if (i > 100) {
throw "This long preference is getting very long... "
}
self.GetPreference(allStartWith + "-" + i, "").setData(str.substr(0, 255));
self.GetPreference(allStartWith + "-" + i, "","").setData(str.substr(0, 255));
str = str.substr(255);
i++;
}
@ -107,6 +109,9 @@ export class OsmPreferences {
}
public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource<string> {
if(key.startsWith(prefix) && prefix !== ""){
console.trace("A preference was requested which has a duplicate prefix in its key. This is probably a bug")
}
key = prefix + key;
key = key.replace(/[:\\\/"' {}.%]/g, '')
if (key.length >= 255) {
@ -147,7 +152,7 @@ export class OsmPreferences {
const matches = prefixes.some(prefix => key.startsWith(prefix))
if (matches) {
console.log("Clearing ", key)
self.GetPreference(key, "").setData("")
self.GetPreference(key, "", "").setData("")
}
}

View file

@ -486,7 +486,7 @@ export default class SimpleMetaTaggers {
const subElements: (string | BaseUIElement)[] = [
new Combine([
"Metatags are extra tags available, in order to display more data or to give better questions.",
"The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.",
"They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.",
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object"
]).SetClass("flex-col")

View file

@ -12,6 +12,7 @@ import {Changes} from "../Osm/Changes";
import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Actors/PendingChangesUploader";
import * as translators from "../../assets/translators.json"
import {post} from "jquery";
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -39,6 +40,8 @@ export default class UserRelatedState extends ElementsState {
public readonly isTranslator : Store<boolean>;
public readonly installedUserThemes: UIEventSource<string[]>
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
super(layoutToUse);
@ -116,6 +119,7 @@ export default class UserRelatedState extends ElementsState {
this.InitializeLanguage();
new SelectedElementTagsUpdater(this)
this.installedUserThemes = this.InitInstalledUserThemes();
}
@ -144,5 +148,51 @@ export default class UserRelatedState extends ElementsState {
})
Locale.language.ping();
}
private InitInstalledUserThemes(): UIEventSource<string[]>{
const prefix = "mapcomplete-unofficial-theme-";
const postfix = "-combined-length"
return this.osmConnection.preferencesHandler.preferences.map(prefs =>
Object.keys(prefs)
.filter(k => k.startsWith(prefix) && k.endsWith(postfix))
.map(k => k.substring(prefix.length, k.length - postfix.length))
)
}
public GetUnofficialTheme(id: string): {
id: string
icon: string,
title: any,
shortDescription: any,
definition?: any,
isOfficial: boolean
} | undefined {
console.log("GETTING UNOFFICIAL THEME")
const pref = this.osmConnection.GetLongPreference("unofficial-theme-"+id)
const str = pref.data
if (str === undefined || str === "undefined" || str === "") {
pref.setData(null)
return undefined
}
try {
const value: {
id: string
icon: string,
title: any,
shortDescription: any,
definition?: any,
isOfficial: boolean
} = JSON.parse(str)
value.isOfficial = false
return value;
} catch (e) {
console.warn("Removing theme " + id + " as it could not be parsed from the preferences; the content is:", str)
pref.setData(null)
return undefined
}
}
}

View file

@ -10,6 +10,9 @@ import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
import {isRegExp} from "util";
import * as key_counts from "../../assets/key_totals.json"
type Tags = Record<string, string>
type OsmTags = Tags & {id: string}
export class TagUtils {
private static keyCounts: { keys: any, tags: any } = key_counts["default"] ?? key_counts
private static comparators
@ -56,11 +59,17 @@ export class TagUtils {
return true;
}
static SplitKeys(tagsFilters: TagsFilter[]): Record<string, string[]> {
return <any>this.SplitKeysRegex(tagsFilters, false);
}
/***
* Creates a hash {key --> [values : string | RegexTag ]}, with all the values present in the tagsfilter
*
* TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]}
*/
static SplitKeys(tagsFilters: TagsFilter[], allowRegex = false) {
const keyValues = {} // Map string -> (string | RegexTag)[]
static SplitKeysRegex(tagsFilters: TagsFilter[], allowRegex: boolean): Record<string, (string | RegexTag)[]> {
const keyValues: Record<string, (string | RegexTag)[]> = {}
tagsFilters = [...tagsFilters] // copy all, use as queue
while (tagsFilters.length > 0) {
const tagsFilter = tagsFilters.shift();
@ -78,7 +87,7 @@ export class TagUtils {
if (keyValues[tagsFilter.key] === undefined) {
keyValues[tagsFilter.key] = [];
}
keyValues[tagsFilter.key].push(...tagsFilter.value.split(";"));
keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map(s => s.trim()));
continue;
}
@ -130,19 +139,23 @@ export class TagUtils {
/**
* Returns true if the properties match the tagsFilter, interpreted as a multikey.
* Note that this might match a regex tag
* @param tag
* @param properties
* @constructor
*
* TagUtils.MatchesMultiAnswer(new Tag("isced:level","bachelor"), {"isced:level":"bachelor; master"}) // => true
* TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor;master"}) // => true
* TagUtils.MatchesMultiAnswer(new Tag("isced:level","doctorate"), {"isced:level":"bachelor; master"}) // => false
*
* // should match with a space too
* TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true
*/
static MatchesMultiAnswer(tag: TagsFilter, properties: any): boolean {
const splitted = TagUtils.SplitKeys([tag], true);
static MatchesMultiAnswer(tag: TagsFilter, properties: Tags): boolean {
const splitted = TagUtils.SplitKeysRegex([tag], true);
for (const splitKey in splitted) {
const neededValues = splitted[splitKey];
if (properties[splitKey] === undefined) {
return false;
}
const actualValue = properties[splitKey].split(";");
const actualValue = properties[splitKey].split(";").map(s => s.trim());
for (const neededValue of neededValues) {
if (neededValue instanceof RegexTag) {
@ -169,6 +182,7 @@ export class TagUtils {
/**
* Returns wether or not a keys is (probably) a valid key.
* See 'Tags_format.md' for an overview of what every tag does
*
* // should accept common keys
* TagUtils.isValidKey("name") // => true