Merge develop

This commit is contained in:
Pieter Vander Vennet 2021-10-28 00:13:18 +02:00
commit 897c59f97a
35 changed files with 1792 additions and 1172 deletions

View file

@ -114,6 +114,7 @@ export default class SelectedFeatureHandler {
// Hash has been cleared - we clear the selected element
state.selectedElement.setData(undefined);
} else {
// we search the element to select
const feature = state.allElements.ContainingFeatures.get(h)
if (feature === undefined) {

View file

@ -1,5 +1,6 @@
import * as turf from "@turf/turf";
import {TileRange, Tiles} from "../Models/TileRange";
import {GeoOperations} from "./GeoOperations";
export class BBox {
@ -22,7 +23,7 @@ export class BBox {
this.minLon = Math.min(this.minLon, coordinate[0]);
this.minLat = Math.min(this.minLat, coordinate[1]);
}
this.maxLon = Math.min(this.maxLon, 180)
this.maxLat = Math.min(this.maxLat, 90)
this.minLon = Math.max(this.minLon, -180)
@ -117,12 +118,12 @@ export class BBox {
}
pad(factor: number, maxIncrease = 2): BBox {
const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
const lonDiff =Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
return new BBox([[
this.minLon - lonDiff,
this.minLat - latDiff
this.minLat - latDiff
], [this.maxLon + lonDiff,
this.maxLat + latDiff]])
}
@ -161,4 +162,16 @@ export class BBox {
const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y)
return new BBox([].concat(boundsul, boundslr))
}
toMercator(): { minLat: number, maxLat: number, minLon: number, maxLon: number } {
const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat])
const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat])
return {
minLon, maxLon,
minLat, maxLat
}
}
}

View file

@ -18,7 +18,6 @@ export default class DetermineLayout {
*/
public static async GetLayout(): Promise<[LayoutConfig, string]> {
const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme")
const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data);
@ -73,17 +72,13 @@ export default class DetermineLayout {
try {
const data = await Utils.downloadJson(link)
const parsed = await Utils.downloadJson(link)
console.log("Got ", parsed)
try {
let parsed = data;
if (typeof parsed == "string") {
parsed = JSON.parse(parsed);
}
// Overwrite the id to the url
parsed.id = link;
return new LayoutConfig(parsed, false).patchImages(link, data);
return new LayoutConfig(parsed, false).patchImages(link, JSON.stringify(parsed));
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid:`,
new FixedUiElement(e)
@ -92,6 +87,7 @@ export default class DetermineLayout {
}
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
new FixedUiElement(e)
@ -107,7 +103,7 @@ export default class DetermineLayout {
try {
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
"user-layout-" + userLayoutParam.data.replace(" ", "_")
"user-layout-" + userLayoutParam.data?.replace(" ", "_")
);
if (dedicatedHashFromLocalStorage.data?.length < 10) {
dedicatedHashFromLocalStorage.setData(undefined);
@ -134,6 +130,7 @@ export default class DetermineLayout {
try {
json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
return null;
}
@ -143,6 +140,7 @@ export default class DetermineLayout {
userLayoutParam.setData(layoutToUse.id);
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
} catch (e) {
console.error(e)
if (hash === undefined || hash.length < 10) {
DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"))
}

View file

@ -222,7 +222,6 @@ export class ExtraFunction {
const maxFeatures = options?.maxFeatures ?? 1
const maxDistance = options?.maxDistance ?? 500
const uniqueTag: string | undefined = options?.uniqueTag
console.log("Requested closestN")
if (typeof features === "string") {
const name = features
const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
@ -238,7 +237,7 @@ export class ExtraFunction {
let closestFeatures: { feat: any, distance: number }[] = [];
for (const featureList of features) {
for (const otherFeature of featureList) {
if (otherFeature === feature || otherFeature.id === feature.id) {
if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) {
continue; // We ignore self
}
const distance = GeoOperations.distanceBetween(
@ -249,6 +248,11 @@ export class ExtraFunction {
console.error("Could not calculate the distance between", feature, "and", otherFeature)
throw "Undefined distance!"
}
if(distance === 0){
console.trace("Got a suspiciously zero distance between", otherFeature, "and self-feature",feature)
}
if (distance > maxDistance) {
continue
}

View file

@ -98,7 +98,7 @@ export default class FeaturePipeline {
this.osmSourceZoomLevel = state.osmApiTileSize.data;
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
this.relationTracker = new RelationsTracker()
state.changes.allChanges.addCallbackAndRun(allChanges => {
allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined)
.map(ch => ch.changes)
@ -203,7 +203,9 @@ export default class FeaturePipeline {
neededTiles: neededTilesFromOsm,
handleTile: tile => {
new RegisteringAllFromFeatureSourceActor(tile)
new SaveTileToLocalStorageActor(tile, tile.tileIndex)
if (tile.layer.layerDef.maxAgeOfCache > 0) {
new SaveTileToLocalStorageActor(tile, tile.tileIndex)
}
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
@ -211,7 +213,9 @@ export default class FeaturePipeline {
state: state,
markTileVisited: (tileId) =>
state.filteredLayers.data.forEach(flayer => {
SaveTileToLocalStorageActor.MarkVisited(flayer.layerDef.id, tileId, new Date())
if (flayer.layerDef.maxAgeOfCache > 0) {
SaveTileToLocalStorageActor.MarkVisited(flayer.layerDef.id, tileId, new Date())
}
self.freshnesses.get(flayer.layerDef.id).addTileLoad(tileId, new Date())
})
})
@ -260,7 +264,7 @@ export default class FeaturePipeline {
// Whenever fresh data comes in, we need to update the metatagging
self.newDataLoadedSignal.stabilized(1000).addCallback(_ => {
self.newDataLoadedSignal.stabilized(250).addCallback(src => {
self.updateAllMetaTagging()
})
@ -385,7 +389,7 @@ export default class FeaturePipeline {
window.setTimeout(
() => {
const layerDef = src.layer.layerDef;
MetaTagging.addMetatags(
const somethingChanged = MetaTagging.addMetatags(
src.features.data,
{
memberships: this.relationTracker,
@ -406,9 +410,10 @@ export default class FeaturePipeline {
private updateAllMetaTagging() {
const self = this;
console.debug("Updating the meta tagging of all tiles as new data got loaded")
this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(src => {
self.applyMetaTags(src)
hierarchy.loadedTiles.forEach(tile => {
self.applyMetaTags(tile)
})
})

View file

@ -1,5 +1,4 @@
import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import Hash from "../../Web/Hash";
@ -12,6 +11,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
public readonly layer: FilteredLayer;
public readonly tileIndex: number
public readonly bbox: BBox
private readonly upstream: FeatureSourceForLayer;
private readonly state: { locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any> };
constructor(
state: {
@ -21,70 +22,63 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
tileIndex,
upstream: FeatureSourceForLayer
) {
const self = this;
this.name = "FilteringFeatureSource(" + upstream.name + ")"
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
this.upstream = upstream
this.state = state
this.layer = upstream.layer;
const layer = upstream.layer;
function update() {
const features: { feature: any; freshness: Date }[] = upstream.features.data;
const newFeatures = features.filter((f) => {
if (
state.selectedElement.data?.id === f.feature.id ||
f.feature.id === Hash.hash.data) {
// This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
return true;
}
const isShown = layer.layerDef.isShown;
const tags = f.feature.properties;
if (isShown.IsKnown(tags)) {
const result = layer.layerDef.isShown.GetRenderValue(
f.feature.properties
).txt;
if (result !== "yes") {
return false;
}
}
const tagsFilter = layer.appliedFilters.data;
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter wat
return false;
}
}
return true;
});
self.features.setData(newFeatures);
}
upstream.features.addCallback(() => {
update();
this. update();
});
layer.appliedFilters.addCallback(_ => {
update()
this.update()
})
update();
this.update();
}
public update() {
const layer = this.upstream.layer;
const features: { feature: any; freshness: Date }[] = this.upstream.features.data;
const newFeatures = features.filter((f) => {
if (
this.state.selectedElement.data?.id === f.feature.id ||
f.feature.id === Hash.hash.data) {
// This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
return true;
}
const isShown = layer.layerDef.isShown;
const tags = f.feature.properties;
if (isShown.IsKnown(tags)) {
const result = layer.layerDef.isShown.GetRenderValue(
f.feature.properties
).txt;
if (result !== "yes") {
return false;
}
}
const tagsFilter = layer.appliedFilters.data;
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter wat
return false;
}
}
return true;
});
this.features.setData(newFeatures);
}
private static showLayer(
layer: {
isDisplayed: UIEventSource<boolean>;
layerDef: LayerConfig;
}) {
return layer.isDisplayed.data;
}
}

View file

@ -7,6 +7,7 @@ import {Utils} from "../../../Utils";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox";
import {GeoOperations} from "../../GeoOperations";
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
@ -14,7 +15,6 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name;
public readonly isOsmCache: boolean
private onFail: ((errorMsg: any, url: string) => void) = undefined;
private readonly seenids: Set<string> = new Set<string>()
public readonly layer: FilteredLayer;
@ -44,10 +44,20 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
if (zxy !== undefined) {
const [z, x, y] = zxy;
let tile_bbox = BBox.fromTile(z, x, y)
let bounds : { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
if(this.layer.layerDef.source.mercatorCrs){
bounds = tile_bbox.toMercator()
}
url = url
.replace('{z}', "" + z)
.replace('{x}', "" + x)
.replace('{y}', "" + y)
.replace('{y_min}',""+bounds.minLat)
.replace('{y_max}',""+bounds.maxLat)
.replace('{x_min}',""+bounds.minLon)
.replace('{x_max}',""+bounds.maxLon)
this.tileIndex = Tiles.tile_index(z, x, y)
this.bbox = BBox.fromTile(z, x, y)
} else {
@ -71,6 +81,10 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
if(json.features === undefined || json.features === null){
return;
}
if(self.layer.layerDef.source.mercatorCrs){
json = GeoOperations.GeoJsonToWGS84(json)
}
const time = new Date();
const newFeatures: { feature: any, freshness: Date } [] = []

View file

@ -31,7 +31,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
// Already handled
!seenChanges.has(ch)))
.addCallbackAndRunD(changes => {
if (changes.length === 0) {
return;
}

View file

@ -20,24 +20,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
if (source.geojsonSource === undefined) {
throw "Invalid layer: geojsonSource expected"
}
const whitelistUrl = source.geojsonSource
.replace("{z}", ""+source.geojsonZoomLevel)
.replace("{x}_{y}.geojson", "overview.json")
.replace("{layer}",layer.layerDef.id)
let whitelist = undefined
Utils.downloadJson(whitelistUrl).then(
json => {
const data = new Map<number, Set<number>>();
for (const x in json) {
data.set(Number(x), new Set(json[x]))
if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) {
const whitelistUrl = source.geojsonSource
.replace("{z}", "" + source.geojsonZoomLevel)
.replace("{x}_{y}.geojson", "overview.json")
.replace("{layer}", layer.layerDef.id)
Utils.downloadJson(whitelistUrl).then(
json => {
const data = new Map<number, Set<number>>();
for (const x in json) {
data.set(Number(x), new Set(json[x]))
}
console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl)
whitelist = data
}
whitelist = data
}
).catch(err => {
console.warn("No whitelist found for ", layer.layerDef.id, err)
})
).catch(err => {
console.warn("No whitelist found for ", layer.layerDef.id, err)
})
}
const seenIds = new Set<string>();
const blackList = new UIEventSource(seenIds)
@ -45,14 +49,14 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
layer,
source.geojsonZoomLevel,
(zxy) => {
if(whitelist !== undefined){
if (whitelist !== undefined) {
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
if(!isWhiteListed){
if (!isWhiteListed) {
console.log("Not downloading tile", ...zxy, "as it is not on the whitelist")
return undefined;
}
}
const src = new GeoJsonSource(
layer,
zxy,

View file

@ -226,7 +226,7 @@ export class GeoOperations {
/**
* Generates the closest point on a way from a given point
*
*
* The properties object will contain three values:
// - `index`: closest point was found on nth line part,
// - `dist`: distance between pt and the closest point (in kilometer),
@ -283,6 +283,34 @@ export class GeoOperations {
return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n")
}
private static readonly _earthRadius = 6378137;
private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2;
//Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913
public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] {
const lon = lonLat[0];
const lat = lonLat[1];
const x = lon * GeoOperations._originShift / 180;
let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
y = y * GeoOperations._originShift / 180;
return [x, y];
}
//Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum
public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] {
const lon = lonLat[0]
const lat = lonLat[1]
const x = 180 * lon / GeoOperations._originShift;
let y = 180 * lat / GeoOperations._originShift;
y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);
return [x, y];
}
public static GeoJsonToWGS84(geojson){
return turf.toWgs84(geojson)
}
/**
* Calculates the intersection between two features.
* Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons

View file

@ -18,6 +18,8 @@ export default class MetaTagging {
/**
* This method (re)calculates all metatags and calculated tags on every given object.
* The given features should be part of the given layer
*
* Returns true if at least one feature has changed properties
*/
public static addMetatags(features: { feature: any; freshness: Date }[],
params: ExtraFuncParams,
@ -25,7 +27,7 @@ export default class MetaTagging {
options?: {
includeDates?: true | boolean,
includeNonDates?: true | boolean
}) {
}): boolean {
if (features === undefined || features.length === 0) {
return;
@ -48,6 +50,7 @@ export default class MetaTagging {
// The calculated functions - per layer - which add the new keys
const layerFuncs = this.createRetaggingFunc(layer)
let atLeastOneFeatureChanged = false;
for (let i = 0; i < features.length; i++) {
const ff = features[i];
@ -95,8 +98,10 @@ export default class MetaTagging {
if (somethingChanged) {
State.state?.allElements?.getEventSourceById(feature.properties.id)?.ping()
atLeastOneFeatureChanged = true
}
}
return atLeastOneFeatureChanged
}