MapComplete/src/Models/ThemeViewState/WithSpecialLayers.ts

231 lines
9.2 KiB
TypeScript

import ThemeConfig from "../ThemeConfig/ThemeConfig"
import { WithChangesState } from "./WithChangesState"
import FavouritesFeatureSource from "../../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import Constants from "../Constants"
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { Feature } from "geojson"
import { BBox } from "../../Logic/BBox"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import MetaTagging from "../../Logic/MetaTagging"
import FilteredLayer from "../FilteredLayer"
import LayerConfig from "../ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson"
import last_click_layerconfig from "../../assets/generated/layers/last_click.json"
import { GeoOperations } from "../../Logic/GeoOperations"
import summaryLayer from "../../assets/generated/layers/summary.json"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
import {
SummaryTileSource,
SummaryTileSourceRewriter
} from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
export class WithSpecialLayers extends WithChangesState {
readonly favourites: FavouritesFeatureSource
/**
* When hovering (in the popup) an image, the location of the image will be revealed on the main map.
* This store contains those images that should be shown, probably only the currently hovered image
*/
readonly geocodedImages: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
/**
* Contains a few (<10) >features that are near the center of the map.
*/
readonly closestFeatures: NearbyFeatureSource
readonly featureSummary: SummaryTileSourceRewriter
/**
* When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set.
* This is the 'viewport' which 'closestFeatures' uses to filter wilt
*/
readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.favourites = new FavouritesFeatureSource(this)
this.closestFeatures = new NearbyFeatureSource(
this.mapProperties.location,
this.perLayerFiltered,
{
currentZoom: this.mapProperties.zoom,
layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds.map(
(bounds) => bounds ?? this.mapProperties.bounds?.data,
[this.mapProperties.bounds]
),
}
)
this.closestFeatures.registerSource(this.favourites, "favourite")
this.featureSummary = this.setupSummaryLayer()
this.initActorsSpecialLayers()
this.drawSelectedElement()
this.drawSpecialLayers()
this.drawLastClick()
// Note: the lock-range is handled by UserMapFeatureSwitchState
{
// Activate metatagging for the 'current_view' layer
const currentViewLayer = this.layerState.filteredLayers.get("current_view")?.layerDef
if (currentViewLayer?.tagRenderings?.length > 0) {
const params = MetaTagging.createExtraFuncParams(this)
this.currentView.features.addCallbackAndRunD((features) => {
MetaTagging.addMetatags(
features,
params,
currentViewLayer,
this.theme,
this.osmObjectDownloader,
this.featureProperties
)
})
}
}
}
private setupSummaryLayer(): SummaryTileSourceRewriter | undefined {
/**
* MaxZoom for the summary layer
*/
const normalLayers = this.theme.layers.filter((l) => l.isNormal())
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
const layers = this.theme.layers.filter(
(l) =>
!Constants.isPriviliged(l) &&
l.source.geojsonSource === undefined &&
l.doCount
)
if (!Constants.SummaryServer || layers.length === 0) {
return undefined
}
const summaryTileSource = new SummaryTileSource(
Constants.SummaryServer,
layers.map((l) => l.id),
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
this.mapProperties,
{
isActive: this.mapProperties.zoom.map((z) => z < maxzoom),
}
)
const source = new SummaryTileSourceRewriter(
summaryTileSource,
this.layerState.filteredLayers
)
new ShowDataLayer(this.map, {
features: source,
layer: new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer"),
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
selectedElement: this.selectedElement,
})
return source
}
protected registerSpecialLayer(flayer: FilteredLayer, source: FeatureSource) {
if (!source?.features) {
return
}
this.featureProperties.trackFeatureSource(source)
const options: ShowDataLayerOptions & { layer: LayerConfig } = {
features: source,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
}
new ShowDataLayer(this.map, options)
}
private drawLastClick() {
const source = this.lastClickObject
const lastClickLayerConfig = new LayerConfig(
<LayerConfigJson>last_click_layerconfig,
"last_click"
)
const lastClickFiltered =
lastClickLayerConfig.isShown === undefined
? source
: source.features.mapD((fs) =>
fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties
)
console.debug("LastClick ", f, "matches", matches)
return matches
})
)
// show last click = new point/note marker
const features = new StaticFeatureSource(lastClickFiltered)
this.featureProperties.trackFeatureSource(features)
new ShowDataLayer(this.map, {
features,
layer: lastClickLayerConfig,
onClick: (feature) => {
if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) {
this.selectedElement.setData(feature)
return
}
this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint,
center: GeoOperations.centerpointCoordinates(feature),
})
},
})
}
private drawSelectedElement() {
const src = new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? [] : [f]))
)
ShowDataLayer.showMultipleLayers(this.map, src, this.theme.layers)
}
private drawSpecialLayers() {
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
type LayersToAdd =
| "current_view"
| Exclude<
AddedByDefaultTypes,
| "search" // Handled by WithSearchState
| "last_click" // handled by this.drawLastClick()
| "summary" // handled by setupSummaryLayer
| "range" // handled by UserMapFeatureSwitchState
| "selected_element" // handled by this.drawSelectedElement
>
const empty = []
/**
* A listing which maps the layerId onto the featureSource
*/
const specialLayers: Record<LayersToAdd, FeatureSource> = {
home_location: this.userRelatedState.homeLocation,
gps_location: this.geolocation.currentUserLocation,
gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack,
current_view: this.currentView,
favourite: this.favourites,
geocoded_image: new StaticFeatureSource(this.geocodedImages),
}
// enumerate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
this.layerState.filteredLayers.forEach((flayer) => {
this.registerSpecialLayer(flayer, specialLayers[flayer.layerDef.id])
})
}
private initActorsSpecialLayers() {
this.selectedElement.addCallback((selected) => {
if (selected === undefined) {
this.focusOnMap()
this.geocodedImages.set([])
} else {
this.lastClickObject.clear()
}
})
}
}