forked from MapComplete/MapComplete
Add initial clustering per tile, very broken
This commit is contained in:
parent
2b78c4b53f
commit
c5e9448720
88 changed files with 1080 additions and 651 deletions
|
@ -36,6 +36,8 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
|
||||
.SetClass("hidden md:block");
|
||||
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
||||
|
||||
|
||||
const self = this;
|
||||
isShown.addCallback(isShown => {
|
||||
if (isShown) {
|
||||
|
|
|
@ -2,22 +2,23 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export class VariableUiElement extends BaseUIElement {
|
||||
private _element: HTMLElement;
|
||||
private readonly _contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>;
|
||||
|
||||
constructor(
|
||||
contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>
|
||||
) {
|
||||
constructor(contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>) {
|
||||
super();
|
||||
this._contents = contents;
|
||||
|
||||
this._element = document.createElement("span");
|
||||
const el = this._element;
|
||||
contents.addCallbackAndRun((contents) => {
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("span");
|
||||
this._contents.addCallbackAndRun((contents) => {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.lastChild);
|
||||
}
|
||||
|
||||
if (contents === undefined) {
|
||||
return el;
|
||||
return;
|
||||
}
|
||||
if (typeof contents === "string") {
|
||||
el.innerHTML = contents;
|
||||
|
@ -35,9 +36,6 @@ export class VariableUiElement extends BaseUIElement {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._element;
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,8 +100,6 @@ export default abstract class BaseUIElement {
|
|||
throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name
|
||||
}
|
||||
try {
|
||||
|
||||
|
||||
const el = this.InnerConstructElement();
|
||||
|
||||
if (el === undefined) {
|
||||
|
|
|
@ -13,17 +13,16 @@ export default class Attribution extends VariableUiElement {
|
|||
}
|
||||
super(
|
||||
license.map((license: LicenseInfo) => {
|
||||
|
||||
if (license?.artist === undefined) {
|
||||
return undefined;
|
||||
if(license === undefined){
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
return new Combine([
|
||||
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
|
||||
|
||||
new Combine([
|
||||
Translations.W(license.artist).SetClass("block font-bold"),
|
||||
Translations.W((license.license ?? "") === "" ? "CC0" : (license.license ?? ""))
|
||||
Translations.W(license?.artist ?? ".").SetClass("block font-bold"),
|
||||
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
|
||||
]).SetClass("flex flex-col")
|
||||
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class DeleteImage extends Toggle {
|
|||
tags.map(tags => (tags[key] ?? "") !== "")
|
||||
),
|
||||
undefined /*Login (and thus editing) is disabled*/,
|
||||
State.state?.featureSwitchUserbadge ?? new UIEventSource<boolean>(true)
|
||||
State.state.osmConnection.isLoggedIn
|
||||
)
|
||||
this.SetClass("cursor-pointer")
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import State from "../../State";
|
|||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import * as L from "leaflet";
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
|
|
@ -31,8 +31,10 @@ export default class EditableTagRendering extends Toggle {
|
|||
|
||||
|
||||
const answerWithEditButton = new Combine([answer,
|
||||
new Toggle(editButton, undefined, State.state.osmConnection.isLoggedIn)])
|
||||
.SetClass("flex justify-between w-full")
|
||||
new Toggle(editButton,
|
||||
undefined,
|
||||
State.state.osmConnection.isLoggedIn)
|
||||
]).SetClass("flex justify-between w-full")
|
||||
|
||||
|
||||
const cancelbutton =
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class SplitRoadWizard extends Toggle {
|
|||
})
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
features: new StaticFeatureSource([roadElement]),
|
||||
features: new StaticFeatureSource([roadElement], false),
|
||||
layers: State.state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
enablePopups: false,
|
||||
|
|
156
UI/ShowDataLayer/PerTileCountAggregator.ts
Normal file
156
UI/ShowDataLayer/PerTileCountAggregator.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {BBox} from "../../Logic/GeoOperations";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
|
||||
|
||||
/**
|
||||
* A feature source containing meta features.
|
||||
* It will contain exactly one point for every tile of the specified (dynamic) zoom level
|
||||
*/
|
||||
export default class PerTileCountAggregator implements FeatureSource {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "PerTileCountAggregator"
|
||||
|
||||
private readonly perTile: Map<number, SingleTileCounter> = new Map<number, SingleTileCounter>()
|
||||
private readonly _requestedZoomLevel: UIEventSource<number>;
|
||||
|
||||
constructor(requestedZoomLevel: UIEventSource<number>) {
|
||||
this._requestedZoomLevel = requestedZoomLevel;
|
||||
const self = this;
|
||||
this._requestedZoomLevel.addCallbackAndRun(_ => self.update())
|
||||
}
|
||||
|
||||
private update() {
|
||||
const now = new Date()
|
||||
const allCountsAsFeatures : {feature: any, freshness: Date}[] = []
|
||||
const aggregate = this.calculatePerTileCount()
|
||||
aggregate.forEach((totalsPerLayer, tileIndex) => {
|
||||
const totals = {}
|
||||
let totalCount = 0
|
||||
totalsPerLayer.forEach((total, layerId) => {
|
||||
totals[layerId] = total
|
||||
totalCount += total
|
||||
})
|
||||
totals["tileId"] = tileIndex
|
||||
totals["count"] = totalCount
|
||||
const feature = {
|
||||
"type": "Feature",
|
||||
"properties": totals,
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": Tiles.centerPointOf(...Tiles.tile_from_index(tileIndex))
|
||||
}
|
||||
}
|
||||
allCountsAsFeatures.push({feature: feature, freshness: now})
|
||||
|
||||
const bbox= BBox.fromTileIndex(tileIndex)
|
||||
const box = {
|
||||
"type": "Feature",
|
||||
"properties":totals,
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[bbox.minLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.minLat]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
allCountsAsFeatures.push({feature:box, freshness: now})
|
||||
})
|
||||
this.features.setData(allCountsAsFeatures)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an aggregate count per tile and per subtile
|
||||
* @private
|
||||
*/
|
||||
private calculatePerTileCount() {
|
||||
const perTileCount = new Map<number, Map<string, number>>()
|
||||
const targetZoom = this._requestedZoomLevel.data;
|
||||
// We only search for tiles of the same zoomlevel or a higher zoomlevel, which is embedded
|
||||
for (const singleTileCounter of Array.from(this.perTile.values())) {
|
||||
|
||||
let tileZ = singleTileCounter.z
|
||||
let tileX = singleTileCounter.x
|
||||
let tileY = singleTileCounter.y
|
||||
if (tileZ < targetZoom) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (tileZ > targetZoom) {
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
tileZ--
|
||||
}
|
||||
const tileI = Tiles.tile_index(tileZ, tileX, tileY)
|
||||
let counts = perTileCount.get(tileI)
|
||||
if (counts === undefined) {
|
||||
counts = new Map<string, number>()
|
||||
perTileCount.set(tileI, counts)
|
||||
}
|
||||
singleTileCounter.countsPerLayer.data.forEach((count, layerId) => {
|
||||
if (counts.has(layerId)) {
|
||||
counts.set(layerId, count + counts.get(layerId))
|
||||
} else {
|
||||
counts.set(layerId, count)
|
||||
}
|
||||
})
|
||||
}
|
||||
return perTileCount;
|
||||
}
|
||||
|
||||
public addTile(tile: FeatureSourceForLayer & Tiled, shouldBeCounted: UIEventSource<boolean>) {
|
||||
let counter = this.perTile.get(tile.tileIndex)
|
||||
if (counter === undefined) {
|
||||
counter = new SingleTileCounter(tile.tileIndex)
|
||||
this.perTile.set(tile.tileIndex, counter)
|
||||
// We do **NOT** add a callback on the perTile index, even though we could! It'll update just fine without it
|
||||
}
|
||||
counter.addTileCount(tile, shouldBeCounted)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of a single tile
|
||||
*/
|
||||
class SingleTileCounter implements Tiled {
|
||||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
|
||||
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
|
||||
public readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
|
||||
constructor(tileIndex: number) {
|
||||
this.tileIndex = tileIndex
|
||||
this.bbox = BBox.fromTileIndex(tileIndex)
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
this.z = z;
|
||||
this.x = x;
|
||||
this.y = y
|
||||
}
|
||||
|
||||
public addTileCount(source: FeatureSourceForLayer, shouldBeCounted: UIEventSource<boolean>) {
|
||||
const layer = source.layer.layerDef
|
||||
this.registeredLayers.set(layer.id, layer)
|
||||
const self = this
|
||||
source.features.map(f => {
|
||||
/*if (!shouldBeCounted.data) {
|
||||
return;
|
||||
}*/
|
||||
self.countsPerLayer.data.set(layer.id, f.length)
|
||||
self.countsPerLayer.ping()
|
||||
}, [shouldBeCounted])
|
||||
}
|
||||
|
||||
}
|
|
@ -41,13 +41,14 @@ export default class ShowDataLayer {
|
|||
options.leafletMap.addCallback(_ => self.update(options));
|
||||
this.update(options);
|
||||
|
||||
|
||||
State.state.selectedElement.addCallbackAndRunD(selected => {
|
||||
if (self._leafletMap.data === undefined) {
|
||||
return;
|
||||
}
|
||||
const v = self.leafletLayersPerId.get(selected.properties.id)
|
||||
if(v === undefined){return;}
|
||||
if (v === undefined) {
|
||||
return;
|
||||
}
|
||||
const leafletLayer = v.leafletlayer
|
||||
const feature = v.feature
|
||||
if (leafletLayer.getPopup().isOpen()) {
|
||||
|
@ -66,6 +67,21 @@ export default class ShowDataLayer {
|
|||
|
||||
}
|
||||
})
|
||||
|
||||
options.doShowLayer?.addCallbackAndRun(doShow => {
|
||||
const mp = options.leafletMap.data;
|
||||
if (this.geoLayer == undefined || mp == undefined) {
|
||||
return;
|
||||
}
|
||||
if (doShow) {
|
||||
mp.addLayer(this.geoLayer)
|
||||
} else {
|
||||
mp.removeLayer(this.geoLayer)
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private update(options) {
|
||||
|
@ -83,21 +99,19 @@ export default class ShowDataLayer {
|
|||
mp.removeLayer(this.geoLayer);
|
||||
}
|
||||
|
||||
this.geoLayer= this.CreateGeojsonLayer()
|
||||
this.geoLayer = this.CreateGeojsonLayer()
|
||||
const allFeats = this._features.data;
|
||||
for (const feat of allFeats) {
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
try{
|
||||
try {
|
||||
this.geoLayer.addData(feat);
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
console.error("Could not add ", feat, "to the geojson layer in leaflet")
|
||||
}
|
||||
}
|
||||
|
||||
mp.addLayer(this.geoLayer)
|
||||
|
||||
if (options.zoomToFeatures ?? false) {
|
||||
try {
|
||||
mp.fitBounds(this.geoLayer.getBounds(), {animate: false})
|
||||
|
@ -105,6 +119,10 @@ export default class ShowDataLayer {
|
|||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.doShowLayer?.data ?? true) {
|
||||
mp.addLayer(this.geoLayer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -125,7 +143,8 @@ export default class ShowDataLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) :
|
||||
State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, clickable);
|
||||
const baseElement = style.icon.html;
|
||||
|
@ -193,8 +212,10 @@ export default class ShowDataLayer {
|
|||
infobox.Activate();
|
||||
});
|
||||
|
||||
|
||||
// Add the feature to the index to open the popup when needed
|
||||
this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer})
|
||||
|
||||
}
|
||||
|
||||
private CreateGeojsonLayer(): L.Layer {
|
||||
|
|
|
@ -6,4 +6,5 @@ export interface ShowDataLayerOptions {
|
|||
leafletMap: UIEventSource<L.Map>,
|
||||
enablePopups?: true | boolean,
|
||||
zoomToFeatures?: false | boolean,
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}
|
79
UI/ShowDataLayer/ShowTileInfo.ts
Normal file
79
UI/ShowDataLayer/ShowTileInfo.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ShowDataLayer from "./ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
|
||||
export default class ShowTileInfo {
|
||||
public static readonly styling = new LayerConfig({
|
||||
id: "tileinfo_styling",
|
||||
title: {
|
||||
render: "Tile {z}/{x}/{y}"
|
||||
},
|
||||
tagRenderings: [
|
||||
"all_tags"
|
||||
],
|
||||
source: {
|
||||
osmTags: "tileId~*"
|
||||
},
|
||||
color: {"render": "#3c3"},
|
||||
width: {
|
||||
"render": "1"
|
||||
},
|
||||
label: {
|
||||
render: "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{count}</div>"
|
||||
}
|
||||
}, "tileinfo", true)
|
||||
|
||||
constructor(options: {
|
||||
source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig,
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}) {
|
||||
|
||||
|
||||
const source = options.source
|
||||
const metaFeature: UIEventSource<any[]> =
|
||||
source.features.map(features => {
|
||||
const bbox = source.bbox
|
||||
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
|
||||
const box = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"z": z,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"tileIndex": source.tileIndex,
|
||||
"source": source.name,
|
||||
"count": features.length,
|
||||
tileId: source.name + "/" + source.tileIndex
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[bbox.minLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.minLat]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
const center = GeoOperations.centerpoint(box)
|
||||
return [box, center]
|
||||
})
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
features: new StaticFeatureSource(metaFeature, false),
|
||||
leafletMap: options.leafletMap,
|
||||
doShowLayer: options.doShowLayer
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue