More refactoring, move minimap behind facade

This commit is contained in:
Pieter Vander Vennet 2021-09-21 02:10:42 +02:00
parent c11ff652b8
commit d5c1ba4cd1
79 changed files with 1848 additions and 1118 deletions

View file

@ -10,6 +10,9 @@ export default class Img extends BaseUIElement {
fallbackImage?: string
}) {
super();
if(src === undefined || src === "undefined"){
throw "Undefined src for image"
}
this._src = src;
this._rawSvg = rawSvg;
this._options = options;

View file

@ -1,208 +1,30 @@
import BaseUIElement from "../BaseUIElement";
import * as L from "leaflet";
import {Map} from "leaflet";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {Utils} from "../../Utils";
import {BBox} from "../../Logic/GeoOperations";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class Minimap extends BaseUIElement {
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
bounds?: UIEventSource<BBox>,
allowMoving?: boolean,
leafletOptions?: any,
attribution?: BaseUIElement | boolean,
onFullyLoaded?: (leaflet: L.Map) => void,
leafletMap?: UIEventSource<any>,
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
}
private static _nextId = 0;
public readonly leafletMap: UIEventSource<Map>
private readonly _id: string;
private readonly _background: UIEventSource<BaseLayer>;
private readonly _location: UIEventSource<Loc>;
private _isInited = false;
private _allowMoving: boolean;
private readonly _leafletoptions: any;
private readonly _onFullyLoaded: (leaflet: L.Map) => void
private readonly _attribution: BaseUIElement | boolean;
private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
export default class Minimap {
/**
* A stub implementation. The actual implementation is injected later on, but only in the browser.
* importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it
*/
constructor(options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean,
leafletOptions?: any,
attribution?: BaseUIElement | boolean,
onFullyLoaded?: (leaflet: L.Map) => void,
leafletMap?: UIEventSource<Map>,
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
}
) {
super()
options = options ?? {}
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
this._id = "minimap" + Minimap._nextId;
this._allowMoving = options.allowMoving ?? true;
this._leafletoptions = options.leafletOptions ?? {}
this._onFullyLoaded = options.onFullyLoaded
this._attribution = options.attribution
this._lastClickLocation = options.lastClickLocation;
Minimap._nextId++
/**
* Construct a minimap
*/
public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> }
}
protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div")
div.id = this._id;
div.style.height = "100%"
div.style.width = "100%"
div.style.minWidth = "40px"
div.style.minHeight = "40px"
div.style.position = "relative"
const wrapper = document.createElement("div")
wrapper.appendChild(div)
const self = this;
// @ts-ignore
const resizeObserver = new ResizeObserver(_ => {
self.InitMap();
self.leafletMap?.data?.invalidateSize()
});
resizeObserver.observe(div);
return wrapper;
}
private InitMap() {
if (this._constructedHtmlElement === undefined) {
// This element isn't initialized yet
return;
}
if (document.getElementById(this._id) === null) {
// not yet attached, we probably got some other event
return;
}
if (this._isInited) {
return;
}
this._isInited = true;
const location = this._location;
const self = this;
let currentLayer = this._background.data.layer()
const options = {
center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0],
zoom: location.data?.zoom ?? 2,
layers: [currentLayer],
zoomControl: false,
attributionControl: this._attribution !== undefined,
dragging: this._allowMoving,
scrollWheelZoom: this._allowMoving,
doubleClickZoom: this._allowMoving,
keyboard: this._allowMoving,
touchZoom: this._allowMoving,
// Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
fadeAnimation: this._allowMoving,
}
Utils.Merge(this._leafletoptions, options)
const map = L.map(this._id, options);
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
// Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
// We give a bit of leeway for people on the edges
// Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
map.setMaxBounds(
[[-100, -200], [100, 200]]
);
if (this._attribution !== undefined) {
if (this._attribution === true) {
map.attributionControl.setPrefix(false)
} else {
map.attributionControl.setPrefix(
"<span id='leaflet-attribution'></span>");
}
}
this._background.addCallbackAndRun(layer => {
const newLayer = layer.layer()
if (currentLayer !== undefined) {
map.removeLayer(currentLayer);
}
currentLayer = newLayer;
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
map.addLayer(newLayer);
map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom())
if (self._attribution !== true && self._attribution !== false) {
self._attribution?.AttachTo('leaflet-attribution')
}
})
let isRecursing = false;
map.on("moveend", function () {
if (isRecursing) {
return
}
if (map.getZoom() === location.data.zoom &&
map.getCenter().lat === location.data.lat &&
map.getCenter().lng === location.data.lon) {
return;
}
location.data.zoom = map.getZoom();
location.data.lat = map.getCenter().lat;
location.data.lon = map.getCenter().lng;
isRecursing = true;
location.ping();
isRecursing = false; // This is ugly, I know
})
location.addCallback(loc => {
const mapLoc = map.getCenter()
const dlat = Math.abs(loc.lat - mapLoc[0])
const dlon = Math.abs(loc.lon - mapLoc[1])
if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
return;
}
map.setView([loc.lat, loc.lon], loc.zoom)
})
location.map(loc => loc.zoom)
.addCallback(zoom => {
if (Math.abs(map.getZoom() - zoom) > 0.1) {
map.setZoom(zoom, {});
}
})
if (this._lastClickLocation) {
const lastClickLocation = this._lastClickLocation
map.on("click", function (e) {
// @ts-ignore
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng})
});
map.on("contextmenu", function (e) {
// @ts-ignore
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
});
}
this.leafletMap.setData(map)
}
}

View file

@ -0,0 +1,215 @@
import {Utils} from "../../Utils";
import BaseUIElement from "../BaseUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {BBox} from "../../Logic/GeoOperations";
import * as L from "leaflet";
import {Map} from "leaflet";
import Minimap, {MinimapOptions} from "./Minimap";
export default class MinimapImplementation extends BaseUIElement {
private static _nextId = 0;
public readonly leafletMap: UIEventSource<Map>
private readonly _id: string;
private readonly _background: UIEventSource<BaseLayer>;
private readonly _location: UIEventSource<Loc>;
private _isInited = false;
private _allowMoving: boolean;
private readonly _leafletoptions: any;
private readonly _onFullyLoaded: (leaflet: L.Map) => void
private readonly _attribution: BaseUIElement | boolean;
private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>;
private readonly _bounds: UIEventSource<BBox> | undefined;
private constructor(options: MinimapOptions) {
super()
options = options ?? {}
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
this._bounds = options?.bounds;
this._id = "minimap" + MinimapImplementation._nextId;
this._allowMoving = options.allowMoving ?? true;
this._leafletoptions = options.leafletOptions ?? {}
this._onFullyLoaded = options.onFullyLoaded
this._attribution = options.attribution
this._lastClickLocation = options.lastClickLocation;
MinimapImplementation._nextId++
}
public static initialize() {
Minimap.createMiniMap = options => new MinimapImplementation(options)
}
protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div")
div.id = this._id;
div.style.height = "100%"
div.style.width = "100%"
div.style.minWidth = "40px"
div.style.minHeight = "40px"
div.style.position = "relative"
const wrapper = document.createElement("div")
wrapper.appendChild(div)
const self = this;
// @ts-ignore
const resizeObserver = new ResizeObserver(_ => {
self.InitMap();
self.leafletMap?.data?.invalidateSize()
});
resizeObserver.observe(div);
return wrapper;
}
private InitMap() {
if (this._constructedHtmlElement === undefined) {
// This element isn't initialized yet
return;
}
if (document.getElementById(this._id) === null) {
// not yet attached, we probably got some other event
return;
}
if (this._isInited) {
return;
}
this._isInited = true;
const location = this._location;
const self = this;
let currentLayer = this._background.data.layer()
const options = {
center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0],
zoom: location.data?.zoom ?? 2,
layers: [currentLayer],
zoomControl: false,
attributionControl: this._attribution !== undefined,
dragging: this._allowMoving,
scrollWheelZoom: this._allowMoving,
doubleClickZoom: this._allowMoving,
keyboard: this._allowMoving,
touchZoom: this._allowMoving,
// Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
fadeAnimation: this._allowMoving,
}
Utils.Merge(this._leafletoptions, options)
const map = L.map(this._id, options);
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
// Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
// We give a bit of leeway for people on the edges
// Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
map.setMaxBounds(
[[-100, -200], [100, 200]]
);
if (this._attribution !== undefined) {
if (this._attribution === true) {
map.attributionControl.setPrefix(false)
} else {
map.attributionControl.setPrefix(
"<span id='leaflet-attribution'></span>");
}
}
this._background.addCallbackAndRun(layer => {
const newLayer = layer.layer()
if (currentLayer !== undefined) {
map.removeLayer(currentLayer);
}
currentLayer = newLayer;
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
map.addLayer(newLayer);
map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom())
if (self._attribution !== true && self._attribution !== false) {
self._attribution?.AttachTo('leaflet-attribution')
}
})
let isRecursing = false;
map.on("moveend", function () {
if (isRecursing) {
return
}
if (map.getZoom() === location.data.zoom &&
map.getCenter().lat === location.data.lat &&
map.getCenter().lng === location.data.lon) {
return;
}
location.data.zoom = map.getZoom();
location.data.lat = map.getCenter().lat;
location.data.lon = map.getCenter().lng;
isRecursing = true;
location.ping();
if (self._bounds !== undefined) {
self._bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
}
isRecursing = false; // This is ugly, I know
})
location.addCallback(loc => {
const mapLoc = map.getCenter()
const dlat = Math.abs(loc.lat - mapLoc[0])
const dlon = Math.abs(loc.lon - mapLoc[1])
if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
return;
}
map.setView([loc.lat, loc.lon], loc.zoom)
})
location.map(loc => loc.zoom)
.addCallback(zoom => {
if (Math.abs(map.getZoom() - zoom) > 0.1) {
map.setZoom(zoom, {});
}
})
if (self._bounds !== undefined) {
self._bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
}
if (this._lastClickLocation) {
const lastClickLocation = this._lastClickLocation
map.on("click", function (e) {
// @ts-ignore
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng})
});
map.on("contextmenu", function (e) {
// @ts-ignore
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
});
}
this.leafletMap.setData(map)
}
}

View file

@ -32,7 +32,7 @@ export default class AllDownloads extends ScrollableFullScreen {
freeDivId: "belowmap",
background: State.state.backgroundLayer,
location: State.state.locationControl,
features: State.state.featurePipeline.features,
features: State.state.featurePipeline,
layout: State.state.layoutToUse,
}).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
}

View file

@ -5,19 +5,19 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import UserDetails from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants";
import Loc from "../../Models/Loc";
import * as L from "leaflet"
import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {BBox} from "../../Logic/GeoOperations";
/**
* The bottom right attribution panel in the leaflet map
*/
export default class Attribution extends Combine {
constructor(location: UIEventSource<Loc>,
constructor(location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: UIEventSource<LayoutConfig>,
leafletMap: UIEventSource<L.Map>) {
currentBounds: UIEventSource<BBox>) {
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
@ -43,7 +43,7 @@ export default class Attribution extends Combine {
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
return undefined;
}
const bounds: any = leafletMap?.data?.getBounds();
const bounds: any = currentBounds.data;
if (bounds === undefined) {
return undefined
}
@ -55,7 +55,7 @@ export default class Attribution extends Combine {
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
},
[location, leafletMap]
[location, currentBounds]
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);

View file

@ -26,10 +26,13 @@ export default class AttributionPanel extends Combine {
((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
layoutToUse.data.credits,
"<br/>",
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap),
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
"<br/>",
new VariableUiElement(contributions.map(contributions => {
if(contributions === undefined){
return ""
}
const sorted = Array.from(contributions, ([name, value]) => ({
name,
value

View file

@ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import State from "../../State";
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
import Combine from "../Base/Combine";
import CheckBoxes from "../Input/Checkboxes";
import {GeoOperations} from "../../Logic/GeoOperations";
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
import Toggle from "../Input/Toggle";
import Title from "../Base/Title";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {UIEventSource} from "../../Logic/UIEventSource";
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {meta} from "@turf/turf";
export class DownloadPanel extends Toggle {
constructor() {
const state: {
featurePipeline: FeaturePipeline,
layoutToUse: UIEventSource<LayoutConfig>,
currentBounds: UIEventSource<BBox>
} = State.state
const t = Translations.t.general.download
const somethingLoaded = State.state.featurePipeline.features.map(features => features.length > 0);
const name = State.state.layoutToUse.data.id;
const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson),
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
mimetype: "application/vnd.geo+json"
});
})
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadCSV.Clone().SetClass("font-bold"),
t.downloadCSVHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
const csv = GeoOperations.toCSV(geojson.features)
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
mimetype: "text/csv"
});
})
const downloadButtons = new Combine(
[new Title(t.title), buttonGeoJson, buttonCSV, includeMetaToggle, t.licenseInfo.Clone().SetClass("link-underline")])
[new Title(t.title),
buttonGeoJson,
buttonCSV,
includeMetaToggle,
t.licenseInfo.Clone().SetClass("link-underline")])
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
super(
downloadButtons,
t.noDataLoaded.Clone(),
somethingLoaded)
state.featurePipeline.somethingLoaded)
}
private static getCleanGeoJson(state: {
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>
}, includeMetaData: boolean) {
const resultFeatures = []
const featureList = state.featurePipeline.GetAllFeaturesWithin(state.currentBounds.data);
for (const tile of featureList) {
for (const feature of tile) {
const cleaned = {
type: feature.type,
geometry: feature.geometry,
properties: {...feature.properties}
}
if (!includeMetaData) {
for (const key in cleaned.properties) {
if (key === "_lon" || key === "_lat") {
continue;
}
if (key.startsWith("_")) {
delete feature.properties[key]
}
}
}
const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys))
for (const key of datedKeys) {
delete feature.properties[key]
}
resultFeatures.push(feature)
}
}
return {
type:"FeatureCollection",
features: featureList
}
}
}

View file

@ -15,6 +15,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
@ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
const now = new Date()
const date = now.getFullYear()+"-"+Utils.TwoDigits(now.getMonth()+1)+"-"+Utils.TwoDigits(now.getDate())
const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
tabsWithAboutMc.push({
header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber])
content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
.Subs({"osmcha_link": osmcha_link}), "<br/>Version " + Constants.vNumber])
.SetClass("link-underline")
}
);

View file

@ -52,7 +52,7 @@ export default class ImportButton extends Toggle {
const withLoadingCheck = new Toggle(
t.stillLoading,
new Combine([button, appliedTags]).SetClass("flex flex-col"),
State.state.layerUpdater.runningQuery
State.state.featurePipeline.runningQuery
)
super(t.hasBeenImported, withLoadingCheck, isImported)
}

View file

@ -9,18 +9,21 @@ import MapControlButton from "../MapControlButton";
import Svg from "../../Svg";
import AllDownloads from "./AllDownloads";
import FilterView from "./FilterView";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {BBox} from "../../Logic/GeoOperations";
import Loc from "../../Models/Loc";
export default class LeftControls extends Combine {
constructor(featureSource: FeatureSource) {
constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>}) {
const toggledCopyright = new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle.Clone(),
() =>
new AttributionPanel(
State.state.layoutToUse,
new ContributorCount(featureSource).Contributors
new ContributorCount(state).Contributors
),
undefined
);

View file

@ -65,10 +65,6 @@ export default class SimpleAddUI extends Toggle {
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
}
const addUi = new VariableUiElement(
@ -104,7 +100,7 @@ export default class SimpleAddUI extends Toggle {
new Toggle(
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
addUi,
State.state.layerUpdater.runningQuery
State.state.featurePipeline.runningQuery
),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
@ -150,7 +146,6 @@ export default class SimpleAddUI extends Toggle {
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
console.log("Opening precise input ", preset.preciseInput, "with tags", tags)
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
@ -215,10 +210,7 @@ export default class SimpleAddUI extends Toggle {
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
console.log("Current filters are ", filters)
return filters === undefined || filters.normalize().and.length === 0;
})
preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0)
)

View file

@ -6,7 +6,7 @@ export default class CenterMessageBox extends VariableUiElement {
constructor() {
const state = State.state;
const updater = State.state.layerUpdater;
const updater = State.state.featurePipeline;
const t = Translations.t.centerMessage;
const message = updater.runningQuery.map(
isRunning => {

View file

@ -1,3 +1,19 @@
import jsPDF from "jspdf";
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
import {UIEventSource} from "../Logic/UIEventSource";
import Minimap from "./Base/Minimap";
import Loc from "../Models/Loc";
import {BBox} from "../Logic/GeoOperations";
import BaseLayer from "../Models/BaseLayer";
import {FixedUiElement} from "./Base/FixedUiElement";
import Translations from "./i18n/Translations";
import State from "../State";
import Constants from "../Models/Constants";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
/**
* Creates screenshoter to take png screenshot
* Creates jspdf and downloads it
@ -8,21 +24,6 @@
* - add new layout in "PDFLayout"
* -> in there are more instructions
*/
import jsPDF from "jspdf";
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
import {UIEventSource} from "../Logic/UIEventSource";
import Minimap from "./Base/Minimap";
import Loc from "../Models/Loc";
import {BBox} from "../Logic/GeoOperations";
import ShowDataLayer from "./ShowDataLayer";
import BaseLayer from "../Models/BaseLayer";
import {FixedUiElement} from "./Base/FixedUiElement";
import Translations from "./i18n/Translations";
import State from "../State";
import Constants from "../Models/Constants";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
export default class ExportPDF {
// dimensions of the map in milimeter
public isRunning = new UIEventSource(true)
@ -39,7 +40,7 @@ export default class ExportPDF {
freeDivId: string,
location: UIEventSource<Loc>,
background?: UIEventSource<BaseLayer>
features: UIEventSource<{ feature: any }[]>,
features: FeaturePipeline,
layout: UIEventSource<LayoutConfig>
}
) {
@ -57,7 +58,7 @@ export default class ExportPDF {
zoom: l.zoom + 1
}
const minimap = new Minimap({
const minimap = Minimap.createMiniMap({
location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
background: options.background,
allowMoving: false,
@ -83,24 +84,21 @@ export default class ExportPDF {
minimap.AttachTo(options.freeDivId)
// Next: we prepare the features. Only fully contained features are shown
const bounded = options.features.map(feats => {
const leaflet = minimap.leafletMap.data;
if (leaflet === undefined) {
return feats
}
minimap.leafletMap .addCallbackAndRunD(leaflet => {
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
return feats.filter(f => BBox.get(f.feature).isContainedIn(bounds))
}, [minimap.leafletMap])
// Add the features to the minimap
new ShowDataLayer(
bounded,
minimap.leafletMap,
options.layout,
false
)
options.features.GetTilesPerLayerWithin(bounds, tile => {
console.log("REndering", tile.name)
new ShowDataLayer(
{
features: tile,
leafletMap: minimap.leafletMap,
layerToShow: tile.layer.layerDef,
enablePopups: false
}
)
})
})
}

View file

@ -4,6 +4,7 @@ import Img from "../Base/Img";
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
export class AttributedImage extends Combine {
@ -16,8 +17,13 @@ export class AttributedImage extends Combine {
img = new Img(urlSource);
attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
} else {
img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'})))
attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
img = new VariableUiElement(preparedUrl.map(url => {
if(url === undefined){
return new Loading()
}
return new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'});
}))
attr = new VariableUiElement(preparedUrl.map(_ => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
}

View file

@ -6,13 +6,13 @@ import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
import Minimap from "../Base/Minimap";
/**
* Selects a direction in degrees
*/
export default class DirectionInput extends InputElement<string> {
public static constructMinimap: ((any) => BaseUIElement);
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _location: UIEventSource<Loc>;
private readonly value: UIEventSource<string>;
@ -40,7 +40,7 @@ export default class DirectionInput extends InputElement<string> {
let map: BaseUIElement = new FixedUiElement("")
if (!Utils.runningFromConsole) {
map = DirectionInput.constructMinimap({
map = Minimap.createMiniMap({
background: this.background,
allowMoving: false,
location: this._location

View file

@ -5,7 +5,7 @@ import Svg from "../../Svg";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../../Logic/GeoOperations";
import DirectionInput from "./DirectionInput";
import Minimap from "../Base/Minimap";
/**
@ -41,7 +41,7 @@ export default class LengthInput extends InputElement<string> {
// @ts-ignore
let map = undefined
if (!Utils.runningFromConsole) {
map = DirectionInput.constructMinimap({
map = Minimap.createMiniMap({
background: this.background,
allowMoving: false,
location: this._location,

View file

@ -8,35 +8,31 @@ import Svg from "../../Svg";
import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {GeoOperations} from "../../Logic/GeoOperations";
import ShowDataLayer from "../ShowDataLayer";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
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";
export default class LocationInput extends InputElement<Loc> {
private static readonly matchLayout = new UIEventSource(new LayoutConfig({
description: "Matchpoint style",
icon: "./assets/svg/crosshair-empty.svg",
id: "matchpoint",
language: ["en"],
layers: [{
private static readonly matchLayer = new LayerConfig(
{
id: "matchpoint", source: {
osmTags: {and: []}
},
icon: "./assets/svg/crosshair-empty.svg"
}],
maintainer: "MapComplete",
startLat: 0,
startLon: 0,
startZoom: 0,
title: "Location input",
version: "0"
}));
}, "matchpoint icon", true
)
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
private _centerLocation: UIEventSource<Loc>;
private readonly mapBackground: UIEventSource<BaseLayer>;
/**
* The features to which the input should be snapped
* @private
*/
private readonly _snapTo: UIEventSource<{ feature: any }[]>
private readonly _value: UIEventSource<Loc>
private readonly _snappedPoint: UIEventSource<any>
@ -143,7 +139,7 @@ export default class LocationInput extends InputElement<Loc> {
protected InnerConstructElement(): HTMLElement {
try {
const clickLocation = new UIEventSource<Loc>(undefined);
const map = new Minimap(
const map = Minimap.createMiniMap(
{
location: this._centerLocation,
background: this.mapBackground,
@ -198,7 +194,6 @@ export default class LocationInput extends InputElement<Loc> {
})
if (this._snapTo !== undefined) {
new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false)
const matchPoint = this._snappedPoint.map(loc => {
if (loc === undefined) {
@ -207,16 +202,25 @@ export default class LocationInput extends InputElement<Loc> {
return [{feature: loc}];
})
if (this._snapTo) {
let layout = LocationInput.matchLayout
if (this._snappedPointTags !== undefined) {
layout = State.state.layoutToUse
if (this._snappedPointTags === undefined) {
// No special tags - we show a default crosshair
new ShowDataLayer({
features: new StaticFeatureSource(matchPoint),
enablePopups: false,
zoomToFeatures: false,
leafletMap: map.leafletMap,
layerToShow: LocationInput.matchLayer
})
}else{
new ShowDataMultiLayer({
features: new StaticFeatureSource(matchPoint),
enablePopups: false,
zoomToFeatures: false,
leafletMap: map.leafletMap,
layers: State.state.filteredLayers
}
)
}
new ShowDataLayer(
matchPoint,
map.leafletMap,
layout,
false, false
)
}
}

View file

@ -4,7 +4,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Minimap from "../Base/Minimap";
import State from "../../State";
import ShowDataLayer from "../ShowDataLayer";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import {GeoOperations} from "../../Logic/GeoOperations";
import {LeafletMouseEvent} from "leaflet";
import Combine from "../Base/Combine";
@ -13,10 +13,16 @@ import Translations from "../i18n/Translations";
import SplitAction from "../../Logic/Osm/Actions/SplitAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import Title from "../Base/Title";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
export default class SplitRoadWizard extends Toggle {
private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout())
private static splitLayerStyling = new LayerConfig({
id: "splitpositions",
source: {osmTags: "_cutposition=yes"},
icon: "./assets/svg/plus.svg"
}, "(BUILTIN) SplitRoadWizard.ts", true)
/**
* A UI Element used for splitting roads
@ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle {
const splitClicked = new UIEventSource<boolean>(false);
// Minimap on which you can select the points to be splitted
const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false});
const miniMap = Minimap.createMiniMap({background: State.state.backgroundLayer, allowMoving: false});
miniMap.SetStyle("width: 100%; height: 24rem;");
// Define how a cut is displayed on the map
@ -45,8 +51,20 @@ export default class SplitRoadWizard extends Toggle {
const roadElement = State.state.allElements.ContainingFeatures.get(id)
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
// Datalayer displaying the road and the cut points (if any)
new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true);
new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false)
new ShowDataMultiLayer({
features: new StaticFeatureSource(roadEventSource, true),
layers: State.state.filteredLayers,
leafletMap: miniMap.leafletMap,
enablePopups: false,
zoomToFeatures: true
})
new ShowDataLayer({
features: new StaticFeatureSource(splitPoints, true),
leafletMap: miniMap.leafletMap,
zoomToFeatures: false,
enablePopups: false,
layerToShow: SplitRoadWizard.splitLayerStyling
})
/**
* Handles a click on the overleaf map.
@ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle {
const confirm = new Toggle(mapView, splitToggle, splitClicked);
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
}
private static GetSplitLayout(): LayoutConfig {
return new LayoutConfig({
maintainer: "mapcomplete",
language: ["en"],
startLon: 0,
startLat: 0,
description: "Split points visualisations - built in at SplitRoadWizard.ts",
icon: "", startZoom: 0,
title: "Split locations",
version: "",
id: "splitpositions",
layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}]
}, true, "(BUILTIN) SplitRoadWizard.ts")
}
}

View file

@ -1,19 +1,11 @@
/**
* The data layer shows all the given geojson elements with the appropriate icon etc
*/
import {UIEventSource} from "../Logic/UIEventSource";
import * as L from "leaflet"
import State from "../State";
import FeatureInfoBox from "./Popup/FeatureInfoBox";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import FeatureSource from "../Logic/FeatureSource/FeatureSource";
export interface ShowDataLayerOptions {
features: FeatureSource,
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures? : false | boolean,
}
import {UIEventSource} from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import State from "../../State";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
export default class ShowDataLayer {

View file

@ -0,0 +1,9 @@
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
export interface ShowDataLayerOptions {
features: FeatureSource,
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures?: false | boolean,
}

View file

@ -1,11 +1,12 @@
import {UIEventSource} from "../Logic/UIEventSource";
import FilteredLayer from "../Models/FilteredLayer";
import ShowDataLayer, {ShowDataLayerOptions} from "./ShowDataLayer/ShowDataLayer";
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
/**
* SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
*/
import {UIEventSource} from "../../Logic/UIEventSource";
import ShowDataLayer from "./ShowDataLayer";
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteredLayer from "../../Models/FilteredLayer";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
export default class ShowDataMultiLayer {
constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {

View file

@ -5,7 +5,6 @@ import {ImageCarousel} from "./Image/ImageCarousel";
import Combine from "./Base/Combine";
import {FixedUiElement} from "./Base/FixedUiElement";
import {ImageUploadFlow} from "./Image/ImageUploadFlow";
import ShareButton from "./BigComponents/ShareButton";
import Svg from "../Svg";
import ReviewElement from "./Reviews/ReviewElement";
@ -13,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews";
import Translations from "./i18n/Translations";
import ReviewForm from "./Reviews/ReviewForm";
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
import State from "../State";
import {ImageSearcher} from "../Logic/Actors/ImageSearcher";
import BaseUIElement from "./BaseUIElement";
@ -26,6 +24,9 @@ import BaseLayer from "../Models/BaseLayer";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import ImportButton from "./BigComponents/ImportButton";
import {Tag} from "../Logic/Tags/Tag";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
import Minimap from "./Base/Minimap";
export interface SpecialVisualization {
funcName: string,
@ -37,14 +38,6 @@ export interface SpecialVisualization {
export default class SpecialVisualizations {
static constructMiniMap: (options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean,
leafletOptions?: any
}) => BaseUIElement;
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
public static specialVisualizations: SpecialVisualization[] =
[
{
@ -153,7 +146,7 @@ export default class SpecialVisualizations {
lon: Number(properties._lon),
zoom: zoom
})
const minimap = SpecialVisualizations.constructMiniMap(
const minimap = Minimap.createMiniMap(
{
background: state.backgroundLayer,
location: locationSource,
@ -169,12 +162,14 @@ export default class SpecialVisualizations {
}
})
SpecialVisualizations.constructShowDataLayer(
featuresToShow,
minimap["leafletMap"],
State.state.layoutToUse,
false,
true
new ShowDataMultiLayer(
{
leafletMap: minimap["leafletMap"],
enablePopups : false,
zoomToFeatures: true,
layers: State.state.filteredLayers,
features: new StaticFeatureSource(featuresToShow, true)
}
)