forked from MapComplete/MapComplete
Refactoring: add metatagging
This commit is contained in:
parent
adab786375
commit
3db87232f2
11 changed files with 101 additions and 191 deletions
|
@ -12,8 +12,8 @@ export interface ExtraFuncParams {
|
|||
* Note that more features then requested can be given back.
|
||||
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
|
||||
*/
|
||||
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, { id: string }>[][]
|
||||
getFeatureById: (id: string) => Feature<Geometry, { id: string }>
|
||||
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, Record<string, string>>[]
|
||||
getFeatureById: (id: string) => Feature<Geometry, Record<string, string>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,26 +52,24 @@ class EnclosingFunc implements ExtraFunction {
|
|||
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(
|
||||
<Feature>feat,
|
||||
<Feature<Polygon | MultiPolygon, any>>otherFeature
|
||||
)
|
||||
) {
|
||||
result.push({ feat: otherFeature })
|
||||
}
|
||||
for (const otherFeature of otherFeaturess) {
|
||||
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(
|
||||
<Feature>feat,
|
||||
<Feature<Polygon | MultiPolygon, any>>otherFeature
|
||||
)
|
||||
) {
|
||||
result.push({ feat: otherFeature })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,14 +152,12 @@ class IntersectionFunc implements ExtraFunction {
|
|||
if (otherLayers.length === 0) {
|
||||
continue
|
||||
}
|
||||
for (const tile of otherLayers) {
|
||||
for (const otherFeature of tile) {
|
||||
const intersections = GeoOperations.LineIntersections(feat, otherFeature)
|
||||
if (intersections.length === 0) {
|
||||
continue
|
||||
}
|
||||
result.push({ feat: otherFeature, intersections })
|
||||
for (const otherFeature of otherLayers) {
|
||||
const intersections = GeoOperations.LineIntersections(feat, otherFeature)
|
||||
if (intersections.length === 0) {
|
||||
continue
|
||||
}
|
||||
result.push({ feat: otherFeature, intersections })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +325,7 @@ class ClosestNObjectFunc implements ExtraFunction {
|
|||
const uniqueTagsMatch =
|
||||
otherFeature.properties[uniqueTag] !== undefined &&
|
||||
closestFeature.feat.properties[uniqueTag] ===
|
||||
otherFeature.properties[uniqueTag]
|
||||
otherFeature.properties[uniqueTag]
|
||||
if (uniqueTagsMatch) {
|
||||
targetIndex = -1
|
||||
if (closestFeature.distance > distance) {
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import MetaTagging from "../../MetaTagging"
|
||||
import { ExtraFuncParams } from "../../ExtraFunctions"
|
||||
import { BBox } from "../../BBox"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
/**
|
||||
* Concerned with the logic of updating the right layer at the right time
|
||||
*/
|
||||
class MetatagUpdater {
|
||||
public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>()
|
||||
private source: FeatureSourceForLayer & Tiled
|
||||
private readonly params: ExtraFuncParams
|
||||
private state: { allElements?: ElementStorage }
|
||||
|
||||
private readonly isDirty = new UIEventSource(false)
|
||||
|
||||
constructor(
|
||||
source: FeatureSourceForLayer & Tiled,
|
||||
state: { allElements?: ElementStorage },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this.state = state
|
||||
this.source = source
|
||||
const self = this
|
||||
this.params = {
|
||||
getFeatureById(id) {
|
||||
return <any>state.allElements.ContainingFeatures.get(id)
|
||||
},
|
||||
getFeaturesWithin(layerId, bbox) {
|
||||
// We keep track of the BBOX that this source needs
|
||||
let oldBbox: BBox = self.neededLayerBboxes.get(layerId)
|
||||
if (oldBbox === undefined) {
|
||||
self.neededLayerBboxes.set(layerId, bbox)
|
||||
} else if (!bbox.isContainedIn(oldBbox)) {
|
||||
self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox))
|
||||
}
|
||||
return featurePipeline.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
}
|
||||
this.isDirty.stabilized(100).addCallback((dirty) => {
|
||||
if (dirty) {
|
||||
self.updateMetaTags()
|
||||
}
|
||||
})
|
||||
this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true))
|
||||
}
|
||||
|
||||
public requestUpdate() {
|
||||
this.isDirty.setData(true)
|
||||
}
|
||||
|
||||
public updateMetaTags() {
|
||||
const features = this.source.features.data
|
||||
|
||||
if (features.length === 0) {
|
||||
this.isDirty.setData(false)
|
||||
return
|
||||
}
|
||||
MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state)
|
||||
this.isDirty.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetaTagRecalculator {
|
||||
private _state: {
|
||||
allElements?: ElementStorage
|
||||
}
|
||||
private _featurePipeline: FeaturePipeline
|
||||
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<
|
||||
FeatureSourceForLayer & Tiled
|
||||
>()
|
||||
private readonly _notifiers: MetatagUpdater[] = []
|
||||
|
||||
/**
|
||||
* The meta tag recalculator receives tiles of layers via the 'registerSource'-function.
|
||||
* It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded
|
||||
*/
|
||||
constructor(
|
||||
state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this._featurePipeline = featurePipeline
|
||||
this._state = state
|
||||
|
||||
if (state.currentView !== undefined) {
|
||||
const currentViewUpdater = new MetatagUpdater(
|
||||
state.currentView,
|
||||
this._state,
|
||||
this._featurePipeline
|
||||
)
|
||||
this._alreadyRegistered.add(state.currentView)
|
||||
this._notifiers.push(currentViewUpdater)
|
||||
state.currentView.features.addCallback((_) => {
|
||||
console.debug("Requesting an update for currentView")
|
||||
currentViewUpdater.updateMetaTags()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public registerSource(source: FeatureSourceForLayer & Tiled) {
|
||||
if (source === undefined) {
|
||||
return
|
||||
}
|
||||
if (this._alreadyRegistered.has(source)) {
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(source)
|
||||
this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline))
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((_) => {
|
||||
const layerName = source.layer.layerDef.id
|
||||
for (const updater of self._notifiers) {
|
||||
const neededBbox = updater.neededLayerBboxes.get(layerName)
|
||||
if (neededBbox == undefined) {
|
||||
continue
|
||||
}
|
||||
if (source.bbox === undefined || neededBbox.overlapsWith(source.bbox)) {
|
||||
updater.requestUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
|||
import { Feature } from "geojson"
|
||||
import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { GeoIndexedStoreForLayer } from "./FeatureSource/Actors/GeoIndexedStore"
|
||||
import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
|
||||
|
||||
/**
|
||||
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
||||
|
@ -15,6 +17,34 @@ export default class MetaTagging {
|
|||
private static readonly stopErrorOutputAt = 10
|
||||
private static retaggingFuncCache = new Map<string, ((feature: Feature) => void)[]>()
|
||||
|
||||
constructor(state: {
|
||||
layout: LayoutConfig
|
||||
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
featureProperties: FeaturePropertiesStore
|
||||
}) {
|
||||
const params: ExtraFuncParams = {
|
||||
getFeatureById: (id) => state.indexedFeatures.featuresById.data.get(id),
|
||||
getFeaturesWithin: (layerId, bbox) =>
|
||||
state.perLayer.get(layerId).GetFeaturesWithin(bbox),
|
||||
}
|
||||
for (const layer of state.layout.layers) {
|
||||
if (layer.source === null) {
|
||||
continue
|
||||
}
|
||||
const featureSource = state.perLayer.get(layer.id)
|
||||
featureSource.features?.addCallbackAndRunD((features) => {
|
||||
MetaTagging.addMetatags(
|
||||
features,
|
||||
params,
|
||||
layer,
|
||||
state.layout,
|
||||
state.featureProperties
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method (re)calculates all metatags and calculated tags on every given object.
|
||||
* The given features should be part of the given layer
|
||||
|
|
|
@ -237,7 +237,7 @@ export class OsmConnection {
|
|||
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0
|
||||
)
|
||||
data.tracesCount = Number.parseInt(
|
||||
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0
|
||||
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0
|
||||
)
|
||||
|
||||
data.img = undefined
|
||||
|
|
|
@ -95,6 +95,7 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
|
|||
if (!id.startsWith("node/")) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.trace("Downloading referencing ways for", feature.properties.id)
|
||||
OsmObject.DownloadReferencingWays(id).then((referencingWays) => {
|
||||
const currentTagsSource = state.allElements?.getEventSourceById(id) ?? []
|
||||
|
@ -111,7 +112,7 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
|
|||
}
|
||||
}
|
||||
|
||||
export class CountryTagger extends SimpleMetaTagger {
|
||||
class CountryTagger extends SimpleMetaTagger {
|
||||
private static readonly coder = new CountryCoder(
|
||||
Constants.countryCoderEndpoint,
|
||||
Utils.downloadJson
|
||||
|
@ -182,7 +183,7 @@ class InlineMetaTagger extends SimpleMetaTagger {
|
|||
}
|
||||
}
|
||||
|
||||
export class RewriteMetaInfoTags extends SimpleMetaTagger {
|
||||
class RewriteMetaInfoTags extends SimpleMetaTagger {
|
||||
constructor() {
|
||||
super({
|
||||
keys: [
|
||||
|
@ -267,8 +268,6 @@ export default class SimpleMetaTaggers {
|
|||
const lon = centerPoint.geometry.coordinates[0]
|
||||
feature.properties["_lat"] = "" + lat
|
||||
feature.properties["_lon"] = "" + lon
|
||||
feature._lon = lon // This is dirty, I know
|
||||
feature._lat = lat
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
|
|
@ -69,6 +69,7 @@ export default class LayerState {
|
|||
console.warn(
|
||||
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
|
||||
)
|
||||
filteredLayers.set(layer.id, toReuse)
|
||||
const copy = new FilteredLayer(layer, toReuse.appliedFilters, toReuse.isDisplayed)
|
||||
filteredLayers.set(layer.id, copy)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
|||
import usersettings from "../../assets/generated/layers/usersettings.json"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
||||
import FeatureSwitchState from "./FeatureSwitchState"
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
|
@ -57,7 +58,8 @@ export default class UserRelatedState {
|
|||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
availableLanguages?: string[],
|
||||
layout?: LayoutConfig
|
||||
layout?: LayoutConfig,
|
||||
featureSwitches?: FeatureSwitchState
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
{
|
||||
|
@ -97,7 +99,7 @@ export default class UserRelatedState {
|
|||
|
||||
this.homeLocation = this.initHomeLocation()
|
||||
|
||||
this.preferencesAsTags = this.initAmendedPrefs(layout)
|
||||
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
|
||||
}
|
||||
|
||||
public GetUnofficialTheme(id: string):
|
||||
|
@ -241,7 +243,10 @@ export default class UserRelatedState {
|
|||
* Initialize the 'amended preferences'.
|
||||
* This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd
|
||||
* */
|
||||
private initAmendedPrefs(layout?: LayoutConfig): UIEventSource<Record<string, string>> {
|
||||
private initAmendedPrefs(
|
||||
layout?: LayoutConfig,
|
||||
featureSwitches?: FeatureSwitchState
|
||||
): UIEventSource<Record<string, string>> {
|
||||
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
||||
_theme: layout?.id,
|
||||
_backend: this.osmConnection.Backend(),
|
||||
|
@ -357,6 +362,19 @@ export default class UserRelatedState {
|
|||
}
|
||||
})
|
||||
|
||||
for (const key in featureSwitches) {
|
||||
if (featureSwitches[key].addCallbackAndRun) {
|
||||
featureSwitches[key].addCallbackAndRun((v) => {
|
||||
const oldV = amendedPrefs.data["__" + key]
|
||||
if (oldV === v) {
|
||||
return
|
||||
}
|
||||
amendedPrefs.data["__" + key] = "" + v
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return amendedPrefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class FilteredLayer {
|
|||
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
|
||||
appliedFilters?: ReadonlyMap<string, UIEventSource<undefined | number | string>>,
|
||||
isDisplayed?: UIEventSource<boolean>
|
||||
) {
|
||||
this.layerDef = layer
|
||||
|
|
|
@ -76,6 +76,13 @@ export interface TagRenderingConfigJson {
|
|||
* */
|
||||
condition?: TagConfigJson
|
||||
|
||||
/**
|
||||
* If set, this tag will be evaluated agains the _usersettings/application state_ table.
|
||||
* Enable 'show debug info' in user settings to see available options.
|
||||
* Note that values with an underscore depicts _application state_ (including metainfo about the user) whereas values without an underscore depict _user settings_
|
||||
*/
|
||||
metacondition?: TagConfigJson
|
||||
|
||||
/**
|
||||
* Allow freeform text input from the user
|
||||
*/
|
||||
|
|
|
@ -41,6 +41,7 @@ import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexe
|
|||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { MenuState } from "./MenuState"
|
||||
import MetaTagging from "../Logic/MetaTagging"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -101,7 +102,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
),
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
|
||||
})
|
||||
this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language, layout)
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
layout?.language,
|
||||
layout,
|
||||
this.featureSwitches
|
||||
)
|
||||
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
|
||||
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
|
||||
this.geolocation = new GeoLocationHandler(
|
||||
|
@ -323,10 +329,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
/**
|
||||
* Setup various services for which no reference are needed
|
||||
* @private
|
||||
*/
|
||||
private initActors() {
|
||||
// Various actors that we don't need to reference
|
||||
new MetaTagging(this)
|
||||
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
|
||||
new ChangeToElementsActor(this.changes, this.featureProperties)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement)
|
||||
|
|
26
test.ts
26
test.ts
|
@ -1,24 +1,10 @@
|
|||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import ThemeViewGUI from "./UI/ThemeViewGUI.svelte"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
||||
import * as theme from "./assets/generated/themes/shops.json"
|
||||
import ThemeViewState from "./Models/ThemeViewState"
|
||||
import Combine from "./UI/Base/Combine"
|
||||
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||
import AddNewPoint from "./UI/Popup/AddNewPoint/AddNewPoint.svelte"
|
||||
import UserProfile from "./UI/BigComponents/UserProfile.svelte"
|
||||
|
||||
async function main() {
|
||||
new FixedUiElement("").AttachTo("extradiv")
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
|
||||
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
||||
main.AttachTo("maindiv")
|
||||
}
|
||||
|
||||
async function testspecial() {
|
||||
function testspecial() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
|
||||
|
@ -28,12 +14,4 @@ async function testspecial() {
|
|||
new Combine(all).AttachTo("maindiv")
|
||||
}
|
||||
|
||||
async function test() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
new SvelteUIElement(UserProfile, { osmConnection: state.osmConnection }).AttachTo("maindiv")
|
||||
}
|
||||
|
||||
/*
|
||||
test().then((_) => {}) /*/
|
||||
main().then((_) => {}) //*/
|
||||
testspecial()
|
||||
|
|
Loading…
Reference in a new issue