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:
pietervdvn 2021-08-07 21:19:01 +02:00
parent bf2d634208
commit 0a01561d37
15 changed files with 460 additions and 143 deletions

View file

@ -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(
`<img src="${sourcePart}" style="${style}" />`
);
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(
`<img src="${sourcePart}" style="${style}" />`
);
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],

View file

@ -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
}
}[],

View file

@ -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});

View file

@ -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
}
}

View file

@ -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[],
} | {

View file

@ -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
}
}
]
}

View file

@ -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,

View file

@ -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 = {

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)
}

View file

@ -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
}
}
],

View file

@ -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
}
}
],

View file

@ -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": [

66
test.ts
View file

@ -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<LayoutConfig>(AllKnownLayouts.allKnownLayouts.get("bookcases"))
const layout = new UIEventSource<LayoutConfig>(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<Loc>({
zoom: 19,
lat: 51.21547967875836,
lon: 3.220340609550476
})
const li = new LocationInput(
{
mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("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")