Merge master
This commit is contained in:
commit
98c6113cbe
87 changed files with 3860 additions and 412 deletions
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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("")
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue