diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts
index fa91e4d57d..04093daa7e 100644
--- a/Customizations/JSON/LayerConfig.ts
+++ b/Customizations/JSON/LayerConfig.ts
@@ -14,11 +14,11 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import SourceConfig from "./SourceConfig";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
-import {Tag} from "../../Logic/Tags/Tag";
import BaseUIElement from "../../UI/BaseUIElement";
import {Unit} from "./Denomination";
import DeleteConfig from "./DeleteConfig";
import FilterConfig from "./FilterConfig";
+import PresetConfig from "./PresetConfig";
export default class LayerConfig {
static WAYHANDLING_DEFAULT = 0;
@@ -35,7 +35,7 @@ export default class LayerConfig {
isShown: TagRenderingConfig;
minzoom: number;
minzoomVisible: number;
- maxzoom:number;
+ maxzoom: number;
title?: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
icon: TagRenderingConfig;
@@ -51,12 +51,7 @@ export default class LayerConfig {
public readonly deletion: DeleteConfig | null;
public readonly allowSplit: boolean
- presets: {
- title: Translation,
- tags: Tag[],
- description?: Translation,
- preciseInput?: { preferredBackground?: string }
- }[];
+ presets: PresetConfig[];
tagRenderings: TagRenderingConfig[];
filters: FilterConfig[];
@@ -149,17 +144,41 @@ export default class LayerConfig {
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
this.wayHandling = json.wayHandling ?? 0;
this.presets = (json.presets ?? []).map((pr, i) => {
- if (pr.preciseInput === true) {
- pr.preciseInput = {
- preferredBackground: undefined
+
+ let preciseInput = undefined;
+ if(pr.preciseInput !== undefined){
+ if (pr.preciseInput === true) {
+ pr.preciseInput = {
+ preferredBackground: undefined
+ }
+ }
+ let snapToLayers: string[];
+ if (typeof pr.preciseInput.snapToLayer === "string") {
+ snapToLayers = [pr.preciseInput.snapToLayer]
+ } else {
+ snapToLayers = pr.preciseInput.snapToLayer
+ }
+
+ let preferredBackground : string[]
+ if (typeof pr.preciseInput.preferredBackground === "string") {
+ preferredBackground = [pr.preciseInput.preferredBackground]
+ } else {
+ preferredBackground = pr.preciseInput.preferredBackground
+ }
+ preciseInput = {
+ preferredBackground: preferredBackground,
+ snapToLayers: snapToLayers,
+ maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
}
}
- return {
+
+ const config : PresetConfig= {
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
description: Translations.T(pr.description, `${context}.presets[${i}].description`),
- preciseInput: pr.preciseInput
+ preciseInput: preciseInput,
}
+ return config;
});
/** Given a key, gets the corresponding property from the json (or the default if not found
@@ -407,12 +426,15 @@ export default class LayerConfig {
}
function render(tr: TagRenderingConfig, deflt?: string) {
+ if(tags === undefined){
+ return deflt
+ }
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
}
const iconSize = render(this.iconSize, "40,40,center").split(",");
- const dashArray = render(this.dashArray).split(" ").map(Number);
+ const dashArray = render(this.dashArray)?.split(" ")?.map(Number);
let color = render(this.color, "#00f");
if (color.startsWith("--")) {
@@ -445,24 +467,26 @@ export default class LayerConfig {
const iconUrlStatic = render(this.icon);
const self = this;
- const mappedHtml = tags.map((tgs) => {
- function genHtmlFromString(sourcePart: string): BaseUIElement {
- const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
- let html: BaseUIElement = new FixedUiElement(
- `
`
- );
- const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
- if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
- html = new Combine([
- (Svg.All[match[1] + ".svg"] as string).replace(
- /#000000/g,
- match[2]
- ),
- ]).SetStyle(style);
- }
- return html;
- }
+ function genHtmlFromString(sourcePart: string, rotation: string): BaseUIElement {
+ const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
+ let html: BaseUIElement = new FixedUiElement(
+ `
`
+ );
+ const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
+ if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
+ html = new Combine([
+ (Svg.All[match[1] + ".svg"] as string).replace(
+ /#000000/g,
+ match[2]
+ ),
+ ]).SetStyle(style);
+ }
+ return html;
+ }
+
+
+ const mappedHtml = tags?.map((tgs) => {
// What do you mean, 'tgs' is never read?
// It is read implicitly in the 'render' method
const iconUrl = render(self.icon);
@@ -473,7 +497,7 @@ export default class LayerConfig {
iconUrl.split(";").filter((prt) => prt != "")
);
for (const sourcePart of sourceParts) {
- htmlParts.push(genHtmlFromString(sourcePart));
+ htmlParts.push(genHtmlFromString(sourcePart, rotation));
}
let badges = [];
@@ -489,7 +513,7 @@ export default class LayerConfig {
.filter((prt) => prt != "");
for (const badgePartStr of partDefs) {
- badgeParts.push(genHtmlFromString(badgePartStr));
+ badgeParts.push(genHtmlFromString(badgePartStr, "0"));
}
const badgeCompound = new Combine(badgeParts).SetStyle(
@@ -499,7 +523,7 @@ export default class LayerConfig {
badges.push(badgeCompound);
} else {
htmlParts.push(
- genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt)
+ genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt, "0")
);
}
}
@@ -533,7 +557,7 @@ export default class LayerConfig {
return {
icon: {
- html: new VariableUiElement(mappedHtml),
+ html: mappedHtml === undefined ? new FixedUiElement(self.icon.render.txt) : new VariableUiElement(mappedHtml),
iconSize: [iconW, iconH],
iconAnchor: [anchorW, anchorH],
popupAnchor: [0, 3 - anchorH],
diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts
index aa4bce1b78..49342b290a 100644
--- a/Customizations/JSON/LayerConfigJson.ts
+++ b/Customizations/JSON/LayerConfigJson.ts
@@ -226,7 +226,21 @@ export interface LayerConfigJson {
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
*/
preciseInput?: true | {
- preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string
+ /**
+ * The type of background picture
+ */
+ preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string [],
+ /**
+ * If specified, these layers will be shown to and the new point will be snapped towards it
+ */
+ snapToLayer?: string | string[],
+ /**
+ * If specified, a new point will only be snapped if it is within this range.
+ * Distance in meter
+ *
+ * Default: 10
+ */
+ maxSnapDistance?: number
}
}[],
diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts
index 8032701eaa..d29e93844e 100644
--- a/Customizations/JSON/LayoutConfig.ts
+++ b/Customizations/JSON/LayoutConfig.ts
@@ -99,7 +99,7 @@ export default class LayoutConfig {
this.defaultBackgroundId = json.defaultBackgroundId;
this.layers = LayoutConfig.ExtractLayers(json, this.units, official, context);
- // ALl the layers are constructed, let them share tags in now!
+ // ALl the layers are constructed, let them share tagRenderings now!
const roaming: { r, source: LayerConfig }[] = []
for (const layer of this.layers) {
roaming.push({r: layer.GetRoamingRenderings(), source: layer});
diff --git a/Customizations/JSON/PresetConfig.ts b/Customizations/JSON/PresetConfig.ts
new file mode 100644
index 0000000000..9f198289bb
--- /dev/null
+++ b/Customizations/JSON/PresetConfig.ts
@@ -0,0 +1,16 @@
+import {Translation} from "../../UI/i18n/Translation";
+import {Tag} from "../../Logic/Tags/Tag";
+
+export default interface PresetConfig {
+ title: Translation,
+ tags: Tag[],
+ description?: Translation,
+ /**
+ * If precise input is set, then an extra map is shown in which the user can drag the map to the precise location
+ */
+ preciseInput?: {
+ preferredBackground?: string[],
+ snapToLayers?: string[],
+ maxSnapDistance?: number
+ }
+}
\ No newline at end of file
diff --git a/Logic/Osm/Actions/ChangeDescription.ts b/Logic/Osm/Actions/ChangeDescription.ts
index aefab9c1cb..f91d794fa0 100644
--- a/Logic/Osm/Actions/ChangeDescription.ts
+++ b/Logic/Osm/Actions/ChangeDescription.ts
@@ -14,7 +14,7 @@ export interface ChangeDescription {
lat: number,
lon: number
} | {
- // Coordinates are only used for rendering
+ // Coordinates are only used for rendering. They should be lon, lat
locations: [number, number][]
nodes: number[],
} | {
diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts
index 692271a126..56f240758a 100644
--- a/Logic/Osm/Actions/CreateNewNodeAction.ts
+++ b/Logic/Osm/Actions/CreateNewNodeAction.ts
@@ -3,6 +3,8 @@ import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription";
import {And} from "../../Tags/And";
+import {OsmWay} from "../OsmObject";
+import {GeoOperations} from "../../GeoOperations";
export default class CreateNewNodeAction extends OsmChangeAction {
@@ -10,13 +12,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
private readonly _lat: number;
private readonly _lon: number;
- public newElementId : string = undefined
-
- constructor(basicTags: Tag[], lat: number, lon: number) {
+ public newElementId: string = undefined
+ private readonly _snapOnto: OsmWay;
+ private readonly _reusePointDistance: number;
+
+ constructor(basicTags: Tag[], lat: number, lon: number, options?: { snapOnto: OsmWay, reusePointWithinMeters?: number }) {
super()
this._basicTags = basicTags;
this._lat = lat;
this._lon = lon;
+ if(lat === undefined || lon === undefined){
+ throw "Lat or lon are undefined!"
+ }
+ this._snapOnto = options?.snapOnto;
+ this._reusePointDistance = options.reusePointWithinMeters ?? 1
}
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
@@ -24,7 +33,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
const properties = {
id: "node/" + id
}
- this.newElementId = "node/"+id
+ this.newElementId = "node/" + id
for (const kv of this._basicTags) {
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
@@ -32,16 +41,68 @@ export default class CreateNewNodeAction extends OsmChangeAction {
properties[kv.key] = kv.value;
}
- return [{
+ const newPointChange: ChangeDescription = {
tags: new And(this._basicTags).asChange(properties),
type: "node",
id: id,
- changes:{
+ changes: {
lat: this._lat,
lon: this._lon
}
- }]
+ }
+ if (this._snapOnto === undefined) {
+ return [newPointChange]
+ }
+
+ // Project the point onto the way
+
+ const geojson = this._snapOnto.asGeoJson()
+ const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat])
+ const index = projected.properties.index
+ // We check that it isn't close to an already existing point
+ let reusedPointId = undefined;
+ const prev = <[number, number]>geojson.geometry.coordinates[index]
+ if (GeoOperations.distanceBetween(prev, <[number, number]>projected.geometry.coordinates) * 1000 < this._reusePointDistance) {
+ // We reuse this point instead!
+ reusedPointId = this._snapOnto.nodes[index]
+ }
+ const next = <[number, number]>geojson.geometry.coordinates[index + 1]
+ if (GeoOperations.distanceBetween(next, <[number, number]>projected.geometry.coordinates) * 1000 < this._reusePointDistance) {
+ // We reuse this point instead!
+ reusedPointId = this._snapOnto.nodes[index + 1]
+ }
+ if (reusedPointId !== undefined) {
+ console.log("Reusing an existing point:", reusedPointId)
+ this.newElementId = "node/" + reusedPointId
+
+ return [{
+ tags: new And(this._basicTags).asChange(properties),
+ type: "node",
+ id: reusedPointId
+ }]
+ }
+
+ const locations = [...this._snapOnto.coordinates]
+ locations.forEach(coor => coor.reverse())
+ console.log("Locations are: ", locations)
+ const ids = [...this._snapOnto.nodes]
+
+ locations.splice(index + 1, 0, [this._lon, this._lat])
+ ids.splice(index + 1, 0, id)
+
+ // Allright, we have to insert a new point in the way
+ return [
+ newPointChange,
+ {
+ type:"way",
+ id: this._snapOnto.id,
+ changes: {
+ locations: locations,
+ nodes: ids
+ }
+ }
+ ]
}
diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts
index 6c2acd0f9c..5566407aa7 100644
--- a/Logic/Osm/Changes.ts
+++ b/Logic/Osm/Changes.ts
@@ -27,9 +27,6 @@ export class Changes {
private readonly previouslyCreated : OsmObject[] = []
constructor() {
- this.isUploading.addCallbackAndRun(uploading => {
- console.trace("Is uploading changed:", uploading)
- })
}
private static createChangesetFor(csId: string,
diff --git a/Models/Constants.ts b/Models/Constants.ts
index bc46f859ae..ca119915bd 100644
--- a/Models/Constants.ts
+++ b/Models/Constants.ts
@@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants {
- public static vNumber = "0.9.0-rc0";
+ public static vNumber = "0.9.0-rc2";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts
index 7c59503d74..a850841a91 100644
--- a/UI/BigComponents/SimpleAddUI.ts
+++ b/UI/BigComponents/SimpleAddUI.ts
@@ -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
- },
- 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, 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 = 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(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
diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts
index d54af67919..508df0245f 100644
--- a/UI/Input/LocationInput.ts
+++ b/UI/Input/LocationInput.ts
@@ -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 {
IsSelected: UIEventSource = new UIEventSource(false);
private _centerLocation: UIEventSource;
- private readonly mapBackground : UIEventSource;
+ private readonly mapBackground: UIEventSource;
+ private readonly _snapTo: UIEventSource<{ feature: any }[]>
+ private readonly _value: UIEventSource
+ private readonly _snappedPoint: UIEventSource
+ private readonly _maxSnapDistance: number
+ private readonly _snappedPointTags: any;
+ public readonly snappedOnto: UIEventSource = new UIEventSource(undefined)
- constructor(options?: {
+ constructor(options: {
mapBackground?: UIEventSource,
- centerLocation?: UIEventSource,
+ snapTo?: UIEventSource<{ feature: any }[]>,
+ maxSnapDistance?: number,
+ snappedPointTags?: any,
+ requiresSnapping?: boolean,
+ centerLocation: UIEventSource,
}) {
super();
- options = options ?? {}
- options.centerLocation = options.centerLocation ?? new UIEventSource({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
+
+ 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("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 {
- return this._centerLocation;
+ return this._value;
}
IsValid(t: Loc): boolean {
@@ -35,41 +121,88 @@ export default class LocationInput extends InputElement {
}
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"
+
+ }));
+
}
\ No newline at end of file
diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts
index 05a720b0ef..e6e318f009 100644
--- a/UI/ShowDataLayer.ts
+++ b/UI/ShowDataLayer.ts
@@ -16,9 +16,9 @@ export default class ShowDataLayer {
private readonly _leafletMap: UIEventSource;
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,
layoutToUse: UIEventSource,
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(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)
}
diff --git a/assets/layers/barrier/barrier.json b/assets/layers/barrier/barrier.json
index ccd5479e91..d18f411e5e 100644
--- a/assets/layers/barrier/barrier.json
+++ b/assets/layers/barrier/barrier.json
@@ -53,6 +53,11 @@
"description": {
"en": "A bollard in the road",
"nl": "Een paaltje in de weg"
+ },
+ "preciseInput": {
+ "preferredBackground": ["photo"],
+ "snapToLayer": "cycleways_and_roads",
+ "maxSnapDistance": 25
}
},
{
@@ -66,6 +71,11 @@
"description": {
"en": "Cycle barrier, slowing down cyclists",
"nl": "Fietshekjes, voor het afremmen van fietsers"
+ },
+ "preciseInput": {
+ "preferredBackground": ["photo"],
+ "snapToLayer": "cycleways_and_roads",
+ "maxSnapDistance": 25
}
}
],
diff --git a/assets/layers/crossings/crossings.json b/assets/layers/crossings/crossings.json
index bc3cf5f701..bbd132afc9 100644
--- a/assets/layers/crossings/crossings.json
+++ b/assets/layers/crossings/crossings.json
@@ -66,6 +66,11 @@
"description": {
"en": "Crossing for pedestrians and/or cyclists",
"nl": "Oversteekplaats voor voetgangers en/of fietsers"
+ },
+ "preciseInput": {
+ "preferredBackground": ["photo"],
+ "snapToLayer": "cycleways_and_roads",
+ "maxSnapDistance": 25
}
},
{
@@ -79,6 +84,11 @@
"description": {
"en": "Traffic signal on a road",
"nl": "Verkeerslicht op een weg"
+ },
+ "preciseInput": {
+ "preferredBackground": ["photo"],
+ "snapToLayer": "cycleways_and_roads",
+ "maxSnapDistance": 25
}
}
],
diff --git a/assets/themes/cycle_infra/cycle_infra.json b/assets/themes/cycle_infra/cycle_infra.json
index 9017703052..cb3da7bd84 100644
--- a/assets/themes/cycle_infra/cycle_infra.json
+++ b/assets/themes/cycle_infra/cycle_infra.json
@@ -16,14 +16,14 @@
"en",
"nl"
],
- "maintainer": "",
+ "maintainer": "MapComplete",
"defaultBackgroundId": "CartoDB.Voyager",
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
"version": "0",
"startLat": 51,
"startLon": 3.75,
"startZoom": 11,
- "widenFactor": 0,
+ "widenFactor": 0.05,
"socialImage": "./assets/themes/cycle_infra/cycle-infra.svg",
"enableDownload": true,
"layers": [
diff --git a/test.ts b/test.ts
index 609529cbd8..26d5dd637c 100644
--- a/test.ts
+++ b/test.ts
@@ -2,40 +2,49 @@ import {UIEventSource} from "./Logic/UIEventSource";
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import State from "./State";
+import LocationInput from "./UI/Input/LocationInput";
+import Loc from "./Models/Loc";
+import {VariableUiElement} from "./UI/Base/VariableUIElement";
+import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
-const layout = new UIEventSource(AllKnownLayouts.allKnownLayouts.get("bookcases"))
+const layout = new UIEventSource(AllKnownLayouts.allKnownLayouts.get("cycle_infra"))
State.state = new State(layout.data)
const features = new UIEventSource<{ feature: any }[]>([
{
feature: {
"type": "Feature",
- "properties": {"amenity": "public_bookcase", "id": "node/123"},
-
- id: "node/123",
- _matching_layer_id: "public_bookcase",
+ "properties": {},
"geometry": {
- "type": "Point",
+ "type": "LineString",
"coordinates": [
- 3.220506906509399,
- 51.215009243433094
+ [
+ 3.219616413116455,
+ 51.215315026941276
+ ],
+ [
+ 3.221080899238586,
+ 51.21564432998662
+ ]
]
}
}
- }, {
+ },
+ {
feature: {
"type": "Feature",
- "properties": {
- amenity: "public_bookcase",
- id: "node/456"
- },
- _matching_layer_id: "public_bookcase",
- id: "node/456",
+ "properties": {},
"geometry": {
- "type": "Point",
+ "type": "LineString",
"coordinates": [
- 3.4243011474609375,
- 51.138432319543924
+ [
+ 3.220340609550476,
+ 51.21547967875836
+ ],
+ [
+ 3.2198095321655273,
+ 51.216390293480515
+ ]
]
}
}
@@ -43,5 +52,22 @@ const features = new UIEventSource<{ feature: any }[]>([
])
features.data.map(f => State.state.allElements.addOrGetElement(f.feature))
-
-
+const loc = new UIEventSource({
+ zoom: 19,
+ lat: 51.21547967875836,
+ lon: 3.220340609550476
+})
+const li = new LocationInput(
+ {
+ mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource("map")),
+ snapTo: features,
+ snappedPointTags: {
+ "barrier": "cycle_barrier"
+ },
+ maxSnapDistance: 15,
+ requiresSnapping: false,
+ centerLocation: loc
+ }
+)
+li.SetStyle("height: 30rem").AttachTo("maindiv")
+new VariableUiElement(li.GetValue().map(JSON.stringify)).AttachTo("extradiv")