diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index d9fb6ede8..df7e3a585 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -57,13 +57,14 @@ export class ExtraFunction { doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. " + "If the current feature is a point, all features that embed the point are given. " + "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" + + "The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list" + "\n" + "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] }, (params, feat) => { return (...layerIds: string[]) => { - const result = [] + const result : {feat:any, overlap: number}[]= [] const bbox = BBox.get(feat) @@ -79,6 +80,9 @@ export class ExtraFunction { result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); } } + + result.sort((a, b) => b.overlap - a.overlap) + return result; } } diff --git a/Logic/Osm/Actions/ChangeDescription.ts b/Logic/Osm/Actions/ChangeDescription.ts index 44804bc32..c8a262d6f 100644 --- a/Logic/Osm/Actions/ChangeDescription.ts +++ b/Logic/Osm/Actions/ChangeDescription.ts @@ -16,7 +16,7 @@ export interface ChangeDescription { /** * The type of the change */ - changeType: "answer" | "create" | "split" | "delete" | "move" | string + changeType: "answer" | "create" | "split" | "delete" | "move" | "import" | string | null /** * THe motivation for the change, e.g. 'deleted because does not exist anymore' */ @@ -51,7 +51,8 @@ export interface ChangeDescription { lat: number, lon: number } | { - // Coordinates are only used for rendering. They should be LAT, LON + /* Coordinates are only used for rendering. They should be LON, LAT + * */ coordinates: [number, number][] nodes: number[], } | { diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts index e644eb2c6..b8e0f0171 100644 --- a/Logic/Osm/Actions/CreateNewNodeAction.ts +++ b/Logic/Osm/Actions/CreateNewNodeAction.ts @@ -8,20 +8,29 @@ import {GeoOperations} from "../../GeoOperations"; export default class CreateNewNodeAction extends OsmChangeAction { + /** + * Maps previously created points onto their assigned ID, to reuse the point if uplaoded + * "lat,lon" --> id + */ + private static readonly previouslyCreatedPoints = new Map() public newElementId: string = undefined + public newElementIdNumber: number = undefined private readonly _basicTags: Tag[]; private readonly _lat: number; private readonly _lon: number; private readonly _snapOnto: OsmWay; private readonly _reusePointDistance: number; private meta: { changeType: "create" | "import"; theme: string }; + private readonly _reusePreviouslyCreatedPoint: boolean; constructor(basicTags: Tag[], lat: number, lon: number, options: { - snapOnto?: OsmWay, - reusePointWithinMeters?: number, - theme: string, changeType: "create" | "import" }) { + allowReuseOfPreviouslyCreatedPoints?: boolean, + snapOnto?: OsmWay, + reusePointWithinMeters?: number, + theme: string, changeType: "create" | "import" | null + }) { super() this._basicTags = basicTags; this._lat = lat; @@ -31,18 +40,47 @@ export default class CreateNewNodeAction extends OsmChangeAction { } this._snapOnto = options?.snapOnto; this._reusePointDistance = options?.reusePointWithinMeters ?? 1 + this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0) this.meta = { theme: options.theme, changeType: options.changeType } } + public static registerIdRewrites(mappings: Map) { + const toAdd: [string, number][] = [] + + this.previouslyCreatedPoints.forEach((oldId, key) => { + if (!mappings.has("node/" + oldId)) { + return; + } + + const newId = Number(mappings.get("node/" + oldId).substr("node/".length)) + toAdd.push([key, newId]) + }) + for (const [key, newId] of toAdd) { + CreateNewNodeAction.previouslyCreatedPoints.set(key, newId) + } + } + async CreateChangeDescriptions(changes: Changes): Promise { + if (this._reusePreviouslyCreatedPoint) { + + const key = this._lat + "," + this._lon + const prev = CreateNewNodeAction.previouslyCreatedPoints + if (prev.has(key)) { + this.newElementIdNumber = prev.get(key) + this.newElementId = "node/" + this.newElementIdNumber + return [] + } + } + + const id = changes.getNewID() const properties = { id: "node/" + id } - this.newElementId = "node/" + id + this.setElementId(id) for (const kv of this._basicTags) { if (typeof kv.value !== "string") { throw "Invalid value: don't use a regex in a preset" @@ -84,8 +122,7 @@ export default class CreateNewNodeAction extends OsmChangeAction { } if (reusedPointId !== undefined) { console.log("Reusing an existing point:", reusedPointId) - this.newElementId = "node/" + reusedPointId - + this.setElementId(reusedPointId) return [{ tags: new And(this._basicTags).asChange(properties), type: "node", @@ -112,10 +149,20 @@ export default class CreateNewNodeAction extends OsmChangeAction { coordinates: locations, nodes: ids }, - meta:this.meta + meta: this.meta } ] } + private setElementId(id: number) { + this.newElementIdNumber = id; + this.newElementId = "node/"+id + if (!this._reusePreviouslyCreatedPoint) { + return + } + const key = this._lat + "," + this._lon + CreateNewNodeAction.previouslyCreatedPoints.set(key, id) + } + } \ No newline at end of file diff --git a/Logic/Osm/Actions/CreateNewWayAction.ts b/Logic/Osm/Actions/CreateNewWayAction.ts new file mode 100644 index 000000000..0fa9d9912 --- /dev/null +++ b/Logic/Osm/Actions/CreateNewWayAction.ts @@ -0,0 +1,74 @@ +import {ChangeDescription} from "./ChangeDescription"; +import OsmChangeAction from "./OsmChangeAction"; +import {Changes} from "../Changes"; +import {Tag} from "../../Tags/Tag"; +import CreateNewNodeAction from "./CreateNewNodeAction"; +import {And} from "../../Tags/And"; + +export default class CreateNewWayAction extends OsmChangeAction { + public newElementId: string = undefined + private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; + private readonly tags: Tag[]; + private readonly _options: { theme: string }; + + + /*** + * Creates a new way to upload to OSM + * @param tags: the tags to apply to the wya + * @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used + * @param options + */ + constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[], options: { + theme: string + }) { + super() + this.coordinates = coordinates; + this.tags = tags; + this._options = options; + } + + protected async CreateChangeDescriptions(changes: Changes): Promise { + + const newElements: ChangeDescription[] = [] + + const pointIds: number[] = [] + for (const coordinate of this.coordinates) { + if (coordinate.nodeId !== undefined) { + pointIds.push(coordinate.nodeId) + continue + } + + const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, { + changeType: null, + theme: this._options.theme + }) + await changes.applyAction(newPoint) + pointIds.push(newPoint.newElementIdNumber) + } + + // We have all created (or reused) all the points! + // Time to create the actual way + + + const id = changes.getNewID() + + const newWay = { + id, + type: "way", + meta:{ + theme: this._options.theme, + changeType: "import" + }, + tags: new And(this.tags).asChange({}), + changes: { + nodes: pointIds, + coordinates: this.coordinates.map(c => [c.lon, c.lat]) + } + } + newElements.push(newWay) + this.newElementId = "way/"+id + return newElements + } + + +} \ No newline at end of file diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index f17697770..48fd10cf7 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -7,6 +7,7 @@ import {ChangeDescription} from "./Actions/ChangeDescription"; import {Utils} from "../../Utils"; import {LocalStorageSource} from "../Web/LocalStorageSource"; import SimpleMetaTagger from "../SimpleMetaTagger"; +import CreateNewNodeAction from "./Actions/CreateNewNodeAction"; /** * Handles all changes made to OSM. @@ -14,22 +15,20 @@ import SimpleMetaTagger from "../SimpleMetaTagger"; */ export class Changes { - - private _nextId: number = -1; // Newly assigned ID's are negative public readonly name = "Newly added features" /** * All the newly created features as featureSource + all the modified features */ public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); - public readonly pendingChanges: UIEventSource = LocalStorageSource.GetParsed("pending-changes", []) public readonly allChanges = new UIEventSource(undefined) + private _nextId: number = -1; // Newly assigned ID's are negative private readonly isUploading = new UIEventSource(false); private readonly previouslyCreated: OsmObject[] = [] private readonly _leftRightSensitive: boolean; - constructor(leftRightSensitive : boolean = false) { + constructor(leftRightSensitive: boolean = false) { this._leftRightSensitive = leftRightSensitive; // We keep track of all changes just as well this.allChanges.setData([...this.pendingChanges.data]) @@ -114,21 +113,34 @@ export class Changes { }) } + public async applyAction(action: OsmChangeAction): Promise { + const changes = await action.Perform(this) + console.log("Received changes:", changes) + this.pendingChanges.data.push(...changes); + this.pendingChanges.ping(); + this.allChanges.data.push(...changes) + this.allChanges.ping() + } + + public registerIdRewrites(mappings: Map): void { + CreateNewNodeAction.registerIdRewrites(mappings) + } + /** * UPload the selected changes to OSM. * Returns 'true' if successfull and if they can be removed * @param pending * @private */ - private async flushSelectChanges(pending: ChangeDescription[]): Promise{ + private async flushSelectChanges(pending: ChangeDescription[]): Promise { const self = this; const neededIds = Changes.GetNeededIds(pending) const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id))); - - if(this._leftRightSensitive){ + + if (this._leftRightSensitive) { osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags)) } - + console.log("Got the fresh objects!", osmObjects, "pending: ", pending) const changes: { newObjects: OsmObject[], @@ -137,35 +149,38 @@ export class Changes { } = self.CreateChangesetObjects(pending, osmObjects) if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { console.log("No changes to be made") - return true + return true } const meta = pending[0].meta - - const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({ - key: key, - value: count, - aggregate: true - })) + + const perType = Array.from( + Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) + .map(descr => descr.meta.changeType)), ([key, count]) => ( + { + key: key, + value: count, + aggregate: true + })) const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined) .map(descr => ({ - key: descr.meta.changeType+":"+descr.type+"/"+descr.id, - value: descr.meta.specialMotivation + key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, + value: descr.meta.specialMotivation })) const metatags = [{ key: "comment", - value: "Adding data with #MapComplete for theme #"+meta.theme + value: "Adding data with #MapComplete for theme #" + meta.theme }, { - key:"theme", - value:meta.theme + key: "theme", + value: meta.theme }, ...perType, ...motivations ] - + await State.state.osmConnection.changesetHandler.UploadChangeset( - (csId) => Changes.createChangesetFor(""+csId, changes), + (csId) => Changes.createChangesetFor("" + csId, changes), metatags ) @@ -178,27 +193,27 @@ export class Changes { try { // At last, we build the changeset and upload const pending = self.pendingChanges.data; - + const pendingPerTheme = new Map() for (const changeDescription of pending) { const theme = changeDescription.meta.theme - if(!pendingPerTheme.has(theme)){ + if (!pendingPerTheme.has(theme)) { pendingPerTheme.set(theme, []) } pendingPerTheme.get(theme).push(changeDescription) } - - const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value) + + const successes = await Promise.all(Array.from(pendingPerTheme, ([key, value]) => value) .map(async pendingChanges => { - try{ + try { return await self.flushSelectChanges(pendingChanges); - }catch(e){ - console.error("Could not upload some changes:",e) + } catch (e) { + console.error("Could not upload some changes:", e) return false } })) - - if(!successes.some(s => s == false)){ + + if (!successes.some(s => s == false)) { // All changes successfull, we clear the data! this.pendingChanges.setData([]); } @@ -206,22 +221,13 @@ export class Changes { } catch (e) { console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e) self.pendingChanges.setData([]) - }finally { + } finally { self.isUploading.setData(false) } } - public async applyAction(action: OsmChangeAction): Promise { - const changes = await action.Perform(this) - console.log("Received changes:", changes) - this.pendingChanges.data.push(...changes); - this.pendingChanges.ping(); - this.allChanges.data.push(...changes) - this.allChanges.ping() - } - private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { newObjects: OsmObject[], modifiedObjects: OsmObject[] @@ -373,8 +379,4 @@ export class Changes { return result } - - public registerIdRewrites(mappings: Map): void { - - } } \ No newline at end of file diff --git a/UI/BigComponents/ImportButton.ts b/UI/BigComponents/ImportButton.ts index 197f1f122..362237e00 100644 --- a/UI/BigComponents/ImportButton.ts +++ b/UI/BigComponents/ImportButton.ts @@ -4,20 +4,34 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import {VariableUiElement} from "../Base/VariableUIElement"; import Translations from "../i18n/Translations"; -import State from "../../State"; import Constants from "../../Models/Constants"; import Toggle from "../Input/Toggle"; import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; import {Tag} from "../../Logic/Tags/Tag"; import Loading from "../Base/Loading"; +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; +import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {Changes} from "../../Logic/Osm/Changes"; +import {ElementStorage} from "../../Logic/ElementStorage"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; export default class ImportButton extends Toggle { - constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, + constructor(imageUrl: string | BaseUIElement, + message: string | BaseUIElement, originalTags: UIEventSource, - newTags: UIEventSource, - lat: number, lon: number, + newTags: UIEventSource, + feature: any, minZoom: number, - state: { + state: { + featureSwitchUserbadge: UIEventSource; + featurePipeline: FeaturePipeline; + allElements: ElementStorage; + selectedElement: UIEventSource; + layoutToUse: LayoutConfig, + osmConnection: OsmConnection, + changes: Changes, locationControl: UIEventSource<{ zoom: number }> }) { const t = Translations.t.general.add; @@ -32,7 +46,7 @@ export default class ImportButton extends Toggle { const txt = parts.join(" & ") return t.presetInfo.Subs({tags: txt}).SetClass("subtle") })), undefined, - State.state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt) + state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt) ) const button = new SubtleButton(imageUrl, message) @@ -44,15 +58,12 @@ export default class ImportButton extends Toggle { } originalTags.data["_imported"] = "yes" originalTags.ping() // will set isImported as per its definition - const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, { - theme: State.state.layoutToUse.id, - changeType: "import" - }) - await State.state.changes.applyAction(newElementAction) - State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( + const newElementAction = ImportButton.createAddActionForFeature(newTags.data, feature, state.layoutToUse.id) + await state.changes.applyAction(newElementAction) + state.selectedElement.setData(state.allElements.ContainingFeatures.get( newElementAction.newElementId )) - console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get( + console.log("Did set selected element to", state.allElements.ContainingFeatures.get( newElementAction.newElementId )) @@ -60,25 +71,70 @@ export default class ImportButton extends Toggle { }) const withLoadingCheck = new Toggle(new Toggle( - new Loading(t.stillLoading.Clone()), - new Combine([button, appliedTags]).SetClass("flex flex-col"), - State.state.featurePipeline.runningQuery - ),t.zoomInFurther.Clone(), - state.locationControl.map(l => l.zoom >= minZoom) - ) + new Loading(t.stillLoading.Clone()), + new Combine([button, appliedTags]).SetClass("flex flex-col"), + state.featurePipeline.runningQuery + ), t.zoomInFurther.Clone(), + state.locationControl.map(l => l.zoom >= minZoom) + ) const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported) const pleaseLoginButton = new Toggle(t.pleaseLogin.Clone() - .onClick(() => State.state.osmConnection.AttemptLogin()) + .onClick(() => state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"), undefined, - State.state.featureSwitchUserbadge) - + state.featureSwitchUserbadge) - super(importButton, - pleaseLoginButton, - State.state.osmConnection.isLoggedIn + + super(new Toggle(importButton, + pleaseLoginButton, + state.osmConnection.isLoggedIn + ), + t.wrongType, + new UIEventSource(ImportButton.canBeImported(feature)) ) } + + + private static canBeImported(feature: any) { + const type = feature.geometry.type + return type === "Point" || type === "LineString" || (type === "Polygon" && feature.geometry.coordinates.length === 1) + } + + private static createAddActionForFeature(newTags: Tag[], feature: any, theme: string): OsmChangeAction & { newElementId: string } { + const geometry = feature.geometry + const type = geometry.type + if (type === "Point") { + const lat = geometry.coordinates[1] + const lon = geometry.coordinates[0]; + return new CreateNewNodeAction(newTags, lat, lon, { + theme, + changeType: "import" + }) + } + + if (type === "LineString") { + return new CreateNewWayAction( + newTags, + geometry.coordinates.map(coor => ({lon: coor[0], lat: coor[1]})), + { + theme + } + ) + } + + if (type === "Polygon") { + return new CreateNewWayAction( + newTags, + geometry.coordinates[0].map(coor => ({lon: coor[0], lat: coor[1]})), + { + theme + } + ) + } + + return undefined; + + } } \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 0f13f5df5..72b0ece70 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -33,6 +33,7 @@ import AllKnownLayers from "../Customizations/AllKnownLayers"; import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; import Link from "./Base/Link"; import List from "./Base/List"; +import {OsmConnection} from "../Logic/Osm/OsmConnection"; export interface SpecialVisualization { funcName: string, @@ -480,7 +481,7 @@ export default class SpecialVisualizations { args: [ { name: "tags", - doc: "Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber=$number`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags)" + doc: "The tags to add onto the new object - see specification above" }, { name: "text", @@ -499,6 +500,8 @@ export default class SpecialVisualizations { }], docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes. +#### Importing a dataset into OpenStreetMap: requirements + If you want to import a dataset, make sure that: 1. The dataset to import has a suitable license @@ -507,17 +510,41 @@ If you want to import a dataset, make sure that: There are also some technicalities in your theme to keep in mind: -1. The new point will be added and will flow through the program as any other new point as if it came from OSM. +1. The new feature will be added and will flow through the program as any other new point as if it came from OSM. This means that there should be a layer which will match the new tags and which will display it. -2. The original point from your geojson layer will gain the tag '_imported=yes'. +2. The original feature from your geojson layer will gain the tag '_imported=yes'. This should be used to change the appearance or even to hide it (eg by changing the icon size to zero) 3. There should be a way for the theme to detect previously imported points, even after reloading. - A reference number to the original dataset is an excellen way to do this + A reference number to the original dataset is an excellent way to do this +4. When importing ways, the theme creator is also responsible of avoiding overlapping ways. + +#### Disabled in unofficial themes + +The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). +The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. +In the case that MapComplete is pointed to the testing grounds, the edit will be made on ${OsmConnection.oauth_configs["osm-test"].url} + + +#### Specifying which tags to copy or add + +The first argument of the import button takes a \`;\`-seperated list of tags to add. + +These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. +This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. + +If a value to substitute is undefined, empty string will be used instead. + +This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\` + +Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering... + +Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) + `, constr: (state, tagSource, args) => { - if (!state.layoutToUse.official && !state.featureSwitchIsTesting.data) { + if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) { return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"), - new FixedUiElement("To test, add 'test=true' to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")]) + new FixedUiElement("To test, add test=true or backend=osm-test to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")]) } const tgsSpec = args[0].split(";").map(spec => { const kv = spec.split("=").map(s => s.trim()); @@ -529,9 +556,18 @@ There are also some technicalities in your theme to keep in mind: const rewrittenTags: UIEventSource = tagSource.map(tags => { const newTags: Tag [] = [] for (const [key, value] of tgsSpec) { - if (value.startsWith('$')) { - const origKey = value.substring(1) - newTags.push(new Tag(key, tags[origKey])) + if (value.indexOf('$') >= 0) { + + let parts = value.split("$") + // THe first of the split won't start with a '$', so no substitution needed + let actualValue = parts[0] + parts.shift() + + for (const part of parts) { + const [_, varName, leftOver] = part.match(/([a-zA-Z0-9_:]*)(.*)/) + actualValue += (tags[varName] ?? "") + leftOver + } + newTags.push(new Tag(key, actualValue)) } else { newTags.push(new Tag(key, value)) } @@ -540,12 +576,12 @@ There are also some technicalities in your theme to keep in mind: }) const id = tagSource.data.id; const feature = state.allElements.ContainingFeatures.get(id) - if (feature.geometry.type !== "Point") { - return new FixedUiElement("Error: can only import point objects").SetClass("alert") - } - const [lon, lat] = feature.geometry.coordinates; + const minzoom = Number(args[3]) + const message = args[1] + const image = args[2] + return new ImportButton( - args[2], args[1], tagSource, rewrittenTags, lat, lon, Number(args[3]), state + image, message, tagSource, rewrittenTags, feature, minzoom, state ) } }, diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index 69e4673d5..57d99c145 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -565,16 +565,36 @@ "title": "GRB outline", "minzoom": 16, "calculatedTags": [ - "_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && feat.properties._surface - f.overlap < 5)[0] ?? null", - "_osm_obj:source:ref=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:ref']", - "_osm_obj:source:date=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", - "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", + "_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && (feat.get('_surface') < 20 || f.overlap / feat.get('_surface')) > 0.9)[0] ?? null", + "_overlap_absolute=feat.get('_overlaps_with')?.overlap", + "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ", + "_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']", + "_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", + "_osm_obj:building=feat.get('_overlaps_with')?.feat?.properties.building", + "_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties.id", + "_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", + "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref", "_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')", "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date" ], "tagRenderings": [ { - "render": "{import_button(Upload this geometry to OpenStreetMap)}" + "id": "Building info", + "render": "This is a {building} detected by {detection_method}" + }, + { + "id": "overlapping building type", + "render": "
The overlapping openstreetmap-building is a {_osm_obj:building} and covers {_overlap_percentage}% of the GRB building

GRB geometry:

{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}

OSM geometry:

{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}", + "condition": "_overlaps_with!=null" + }, + { + "id": "Import-button", + "render": "{import_button(building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Upload this building to OpenStreetMap)}", + "mappings": [ + { + "if": "_overlaps_with!=null", + "then": "Cannot be imported directly, there is a nearly identical building geometry in OpenStreetMap" + }] }, "all_tags" ], diff --git a/langs/en.json b/langs/en.json index cee2bae0c..02dd71dbb 100644 --- a/langs/en.json +++ b/langs/en.json @@ -108,7 +108,8 @@ "openLayerControl": "Open the layer control box", "layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point", "hasBeenImported": "This point has already been imported", - "zoomInMore": "Zoom in more to import this feature" + "zoomInMore": "Zoom in more to import this feature", + "wrongType": "This element is not a point or a way and can not be imported" }, "pickLanguage": "Choose a language: ", "about": "Easily edit and add OpenStreetMap for a certain theme",