Add createNewWay-action, more work on GRB import theme, add import button

This commit is contained in:
Pieter Vander Vennet 2021-10-29 16:38:33 +02:00
parent e4cd93ffb0
commit da65bbbc86
9 changed files with 341 additions and 100 deletions

View file

@ -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<any>,
newTags: UIEventSource<Tag[]>,
lat: number, lon: number,
newTags: UIEventSource<Tag[]>,
feature: any,
minZoom: number,
state: {
state: {
featureSwitchUserbadge: UIEventSource<boolean>;
featurePipeline: FeaturePipeline;
allElements: ElementStorage;
selectedElement: UIEventSource<any>;
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;
}
}

View file

@ -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 <b>test=true</b> or <b>backend=osm-test</b> 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<Tag[]> = 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
)
}
},