First working version of snapping to already existing ways from the add-UI (still too slow though), partial fix of #436

This commit is contained in:
Pieter Vander Vennet 2021-08-07 21:19:01 +02:00
parent bf2d634208
commit 0a01561d37
15 changed files with 460 additions and 143 deletions

View file

@ -9,19 +9,16 @@ import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants";
import LayerConfig from "../../Customizations/JSON/LayerConfig";
import {Tag} from "../../Logic/Tags/Tag";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import UserDetails from "../../Logic/Osm/OsmConnection";
import {Translation} from "../i18n/Translation";
import LocationInput from "../Input/LocationInput";
import {InputElement} from "../Input/InputElement";
import Loc from "../../Models/Loc";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import Hash from "../../Logic/Web/Hash";
import PresetConfig from "../../Customizations/JSON/PresetConfig";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
/*
* The SimpleAddUI is a single panel, which can have multiple states:
@ -32,17 +29,12 @@ import Hash from "../../Logic/Web/Hash";
*/
/*private*/
interface PresetInfo {
description: string | Translation,
interface PresetInfo extends PresetConfig {
name: string | BaseUIElement,
icon: () => BaseUIElement,
tags: Tag[],
layerToAddTo: {
layerDef: LayerConfig,
isDisplayed: UIEventSource<boolean>
},
preciseInput?: {
preferredBackground?: string
}
}
@ -65,24 +57,43 @@ export default class SimpleAddUI extends Toggle {
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
console.trace("Creating a new point")
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {snapOnto: snapOntoWay})
State.state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
isShown.setData(false)
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(
selectedPreset.map(preset => {
if (preset === undefined) {
return presetsOverview
}
return SimpleAddUI.CreateConfirmButton(preset,
(tags, location) => {
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon)
State.state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
isShown.setData(false)
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
))
}, () => {
(tags, location, snapOntoWayId?: string) => {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
createNewPoint(tags, location,<OsmWay> way)
return true;
})
}
},
() => {
selectedPreset.setData(undefined)
})
}
@ -115,11 +126,11 @@ export default class SimpleAddUI extends Toggle {
private static CreateConfirmButton(preset: PresetInfo,
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void): BaseUIElement {
let location = State.state.LastClickLocation;
let preciseInput: InputElement<Loc> = undefined
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
const locationSrc = new UIEventSource({
lat: location.data.lat,
@ -132,9 +143,22 @@ export default class SimpleAddUI extends Toggle {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
let features: UIEventSource<{ feature: any }[]> = undefined
if (preset.preciseInput.snapToLayers) {
// We have to snap to certain layers.
// Lets fetch tehm
const asSet = new Set(preset.preciseInput.snapToLayers)
features = State.state.featurePipeline.features.map(f => f.filter(feat => asSet.has(feat.feature._matching_layer_id)))
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
console.log("Opening precise input ", preset.preciseInput, "with tags", tags)
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc
centerLocation: locationSrc,
snapTo: features,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance
})
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
@ -148,7 +172,7 @@ export default class SimpleAddUI extends Toggle {
]).SetClass("flex flex-col")
).SetClass("font-bold break-words")
.onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data, preciseInput?.snappedOnto?.data?.properties?.id);
});
if (preciseInput !== undefined) {
@ -242,8 +266,8 @@ export default class SimpleAddUI extends Toggle {
// The layer is not displayed and we cannot enable the layer control -> we skip
continue;
}
if(layer.layerDef.name === undefined){
if (layer.layerDef.name === undefined) {
// this is a parlty hidden layer
continue;
}
@ -258,6 +282,7 @@ export default class SimpleAddUI extends Toggle {
tags: preset.tags,
layerToAddTo: layer,
name: preset.title,
title: preset.title,
description: preset.description,
icon: icon,
preciseInput: preset.preciseInput

View file

@ -6,28 +6,114 @@ import BaseLayer from "../../Models/BaseLayer";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {GeoOperations} from "../../Logic/GeoOperations";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import ShowDataLayer from "../ShowDataLayer";
export default class LocationInput extends InputElement<Loc> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _centerLocation: UIEventSource<Loc>;
private readonly mapBackground : UIEventSource<BaseLayer>;
private readonly mapBackground: UIEventSource<BaseLayer>;
private readonly _snapTo: UIEventSource<{ feature: any }[]>
private readonly _value: UIEventSource<Loc>
private readonly _snappedPoint: UIEventSource<any>
private readonly _maxSnapDistance: number
private readonly _snappedPointTags: any;
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
constructor(options?: {
constructor(options: {
mapBackground?: UIEventSource<BaseLayer>,
centerLocation?: UIEventSource<Loc>,
snapTo?: UIEventSource<{ feature: any }[]>,
maxSnapDistance?: number,
snappedPointTags?: any,
requiresSnapping?: boolean,
centerLocation: UIEventSource<Loc>,
}) {
super();
options = options ?? {}
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point"))
this._maxSnapDistance = options.maxSnapDistance
this._centerLocation = options.centerLocation;
this._snappedPointTags = options.snappedPointTags
if (this._snapTo === undefined) {
this._value = this._centerLocation;
} else {
const self = this;
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
let matching_layer: UIEventSource<string>
if (self._snappedPointTags !== undefined) {
matching_layer = State.state.layoutToUse.map(layout => {
for (const layer of layout.layers) {
if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) {
return layer.id
}
}
console.error("No matching layer found for tags ", self._snappedPointTags)
return "matchpoint"
})
} else {
matching_layer = new UIEventSource<string>("matchpoint")
}
this._snappedPoint = options.centerLocation.map(loc => {
if (loc === undefined) {
return undefined;
}
// We reproject the location onto every 'snap-to-feature' and select the closest
let min = undefined;
let matchedWay = undefined;
for (const feature of self._snapTo.data) {
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
if (min === undefined) {
min = nearestPointOnLine
matchedWay = feature.feature;
continue;
}
if (min.properties.dist > nearestPointOnLine.properties.dist) {
min = nearestPointOnLine
matchedWay = feature.feature;
}
}
if (min.properties.dist * 1000 > self._maxSnapDistance) {
if (options.requiresSnapping) {
return undefined
} else {
return {
"type": "Feature",
"_matching_layer_id": matching_layer.data,
"properties": options.snappedPointTags ?? min.properties,
"geometry": {"type": "Point", "coordinates": [loc.lon, loc.lat]}
}
}
}
min._matching_layer_id = matching_layer?.data ?? "matchpoint"
min.properties = options.snappedPointTags ?? min.properties
self.snappedOnto.setData(matchedWay)
return min
}, [this._snapTo])
this._value = this._snappedPoint.map(f => {
const [lon, lat] = f.geometry.coordinates;
return {
lon: lon, lat: lat, zoom: undefined
}
})
}
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto)
this.SetClass("block h-full")
}
GetValue(): UIEventSource<Loc> {
return this._centerLocation;
return this._value;
}
IsValid(t: Loc): boolean {
@ -35,41 +121,88 @@ export default class LocationInput extends InputElement<Loc> {
}
protected InnerConstructElement(): HTMLElement {
const map = new Minimap(
{
location: this._centerLocation,
background: this.mapBackground
}
)
map.leafletMap.addCallbackAndRunD(leaflet => {
leaflet.setMaxBounds(
leaflet.getBounds().pad(0.15)
try {
const map = new Minimap(
{
location: this._centerLocation,
background: this.mapBackground
}
)
})
map.leafletMap.addCallbackAndRunD(leaflet => {
leaflet.setMaxBounds(
leaflet.getBounds().pad(0.15)
)
})
this.mapBackground.map(layer => {
if (this._snapTo !== undefined) {
new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false)
const leaflet = map.leafletMap.data
if (leaflet === undefined || layer === undefined) {
return;
const matchPoint = this._snappedPoint.map(loc => {
if (loc === undefined) {
return []
}
return [{feature: loc}];
})
if (this._snapTo) {
let layout = LocationInput.matchLayout
if (this._snappedPointTags !== undefined) {
layout = State.state.layoutToUse
}
new ShowDataLayer(
matchPoint,
map.leafletMap,
layout,
false, false
)
}
}
leaflet.setMaxZoom(layer.max_zoom)
leaflet.setMinZoom(layer.max_zoom - 3)
leaflet.setZoom(layer.max_zoom - 1)
this.mapBackground.map(layer => {
const leaflet = map.leafletMap.data
if (leaflet === undefined || layer === undefined) {
return;
}
}, [map.leafletMap])
return new Combine([
new Combine([
Svg.crosshair_empty_ui()
.SetClass("block relative")
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
]).SetClass("block w-0 h-0 z-10 relative")
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
map
.SetClass("z-0 relative block w-full h-full bg-gray-100")
leaflet.setMaxZoom(layer.max_zoom)
leaflet.setMinZoom(layer.max_zoom - 3)
leaflet.setZoom(layer.max_zoom - 1)
]).ConstructElement();
}, [map.leafletMap])
return new Combine([
new Combine([
Svg.crosshair_empty_ui()
.SetClass("block relative")
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
]).SetClass("block w-0 h-0 z-10 relative")
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
map
.SetClass("z-0 relative block w-full h-full bg-gray-100")
]).ConstructElement();
} catch (e) {
console.error("Could not generate LocationInputElement:", e)
return undefined;
}
}
private static readonly matchLayout = new UIEventSource(new LayoutConfig({
description: "Matchpoint style",
icon: "./assets/svg/crosshair-empty.svg",
id: "matchpoint",
language: ["en"],
layers: [{
id: "matchpoint", source: {
osmTags: {and: []}
},
icon: "./assets/svg/crosshair-empty.svg"
}],
maintainer: "MapComplete",
startLat: 0,
startLon: 0,
startZoom: 0,
title: "Location input",
version: "0"
}));
}

View file

@ -16,9 +16,9 @@ export default class ShowDataLayer {
private readonly _leafletMap: UIEventSource<L.Map>;
private _cleanCount = 0;
private readonly _enablePopups: boolean;
private readonly _features: UIEventSource<{ feature: any}[]>
private readonly _features: UIEventSource<{ feature: any }[]>
constructor(features: UIEventSource<{ feature: any}[]>,
constructor(features: UIEventSource<{ feature: any }[]>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>,
enablePopups = true,
@ -85,7 +85,9 @@ export default class ShowDataLayer {
console.error(e)
}
}
State.state.selectedElement.ping()
if (self._enablePopups) {
State.state.selectedElement.ping()
}
}
features.addCallback(() => update());
@ -106,13 +108,12 @@ export default class ShowDataLayer {
// We have to convert them to the appropriate icon
// Click handling is done in the next step
const tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
if (layer === undefined) {
return;
}
const tagSource = feature.properties.id === undefined ? new UIEventSource<any>(feature.properties) : State.state.allElements.getEventSourceById(feature.properties.id)
const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
const baseElement = style.icon.html;
if (!this._enablePopups) {
@ -146,8 +147,8 @@ export default class ShowDataLayer {
autoPan: true,
closeOnEscapeKey: true,
closeButton: false,
autoPanPaddingTopLeft: [15,15],
autoPanPaddingTopLeft: [15, 15],
}, leafletLayer);
leafletLayer.bindPopup(popup);
@ -191,7 +192,7 @@ export default class ShowDataLayer {
) {
leafletLayer.openPopup()
}
if(feature.id !== feature.properties.id){
if (feature.id !== feature.properties.id) {
console.trace("Not opening the popup for", feature)
}