forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
ac1b4a010c
40 changed files with 5706 additions and 4746 deletions
|
@ -19,9 +19,82 @@ import Loc from "../../Models/Loc";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import Constants from "../../Models/Constants";
|
||||
import PrivacyPolicy from "./PrivacyPolicy";
|
||||
import ContributorCount from "../../Logic/ContributorCount";
|
||||
|
||||
export class OpenIdEditor extends VariableUiElement {
|
||||
constructor(state : {locationControl: UIEventSource<Loc>}, iconStyle? : string, objectId?: string) {
|
||||
const t = Translations.t.general.attribution
|
||||
super(state.locationControl.map(location => {
|
||||
let elementSelect = "";
|
||||
if(objectId !== undefined){
|
||||
const parts = objectId.split("/")
|
||||
const tp = parts[0]
|
||||
if(parts.length === 2 && !isNaN(Number(parts[1])) && (tp === "node" || tp === "way" || tp === "relation")){
|
||||
elementSelect = "&"+ tp+"="+parts[1]
|
||||
}
|
||||
}
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`
|
||||
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true})
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class OpenMapillary extends VariableUiElement {
|
||||
constructor(state : {locationControl: UIEventSource<Loc>}, iconStyle? : string) {
|
||||
const t = Translations.t.general.attribution
|
||||
super( state.locationControl.map(location => {
|
||||
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
|
||||
url: mapillaryLink,
|
||||
newTab: true
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenJosm extends Combine {
|
||||
|
||||
constructor(state : {osmConnection: OsmConnection, currentBounds: UIEventSource<BBox>,}, iconStyle? : string) {
|
||||
const t = Translations.t.general.attribution
|
||||
|
||||
const josmState = new UIEventSource<string>(undefined)
|
||||
// Reset after 15s
|
||||
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
|
||||
|
||||
const stateIndication = new VariableUiElement(josmState.map(state => {
|
||||
if (state === undefined) {
|
||||
return undefined
|
||||
}
|
||||
state = state.toUpperCase()
|
||||
if (state === "OK") {
|
||||
return t.josmOpened.SetClass("thanks")
|
||||
}
|
||||
return t.josmNotOpened.SetClass("alert")
|
||||
}));
|
||||
|
||||
const toggle = new Toggle(
|
||||
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
|
||||
const bounds: any = state.currentBounds.data;
|
||||
if (bounds === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const top = bounds.getNorth();
|
||||
const bottom = bounds.getSouth();
|
||||
const right = bounds.getEast();
|
||||
const left = bounds.getWest();
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
|
||||
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible))
|
||||
|
||||
super([stateIndication, toggle]);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The attribution panel shown on mobile
|
||||
*/
|
||||
|
@ -39,10 +112,7 @@ export default class CopyrightPanel extends Combine {
|
|||
|
||||
const t = Translations.t.general.attribution
|
||||
const layoutToUse = state.layoutToUse
|
||||
const josmState = new UIEventSource<string>(undefined)
|
||||
// Reset after 15s
|
||||
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
|
||||
const iconStyle = "height: 1.5rem; width: auto"
|
||||
const iconStyle = "height: 1.5rem; width: auto"
|
||||
const actionButtons = [
|
||||
new SubtleButton(Svg.liberapay_ui().SetStyle(iconStyle), t.donate, {
|
||||
url: "https://liberapay.com/pietervdvn/",
|
||||
|
@ -56,42 +126,9 @@ export default class CopyrightPanel extends Combine {
|
|||
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
|
||||
newTab: true
|
||||
}),
|
||||
new VariableUiElement(state.locationControl.map(location => {
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`
|
||||
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true})
|
||||
})),
|
||||
|
||||
new VariableUiElement(state.locationControl.map(location => {
|
||||
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
|
||||
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
|
||||
url: mapillaryLink,
|
||||
newTab: true
|
||||
})
|
||||
})),
|
||||
new VariableUiElement(josmState.map(state => {
|
||||
if (state === undefined) {
|
||||
return undefined
|
||||
}
|
||||
state = state.toUpperCase()
|
||||
if (state === "OK") {
|
||||
return t.josmOpened.SetClass("thanks")
|
||||
}
|
||||
return t.josmNotOpened.SetClass("alert")
|
||||
})),
|
||||
new Toggle(
|
||||
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
|
||||
const bounds: any = state.currentBounds.data;
|
||||
if (bounds === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const top = bounds.getNorth();
|
||||
const bottom = bounds.getSouth();
|
||||
const right = bounds.getEast();
|
||||
const left = bounds.getWest();
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR"))
|
||||
}), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)),
|
||||
|
||||
new OpenIdEditor(state, iconStyle),
|
||||
new OpenMapillary(state, iconStyle),
|
||||
new OpenJosm(state, iconStyle)
|
||||
]
|
||||
|
||||
const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
|
||||
|
|
|
@ -350,8 +350,19 @@ export default class ValidatedTextField {
|
|||
ValidatedTextField.tp(
|
||||
"email",
|
||||
"An email adress",
|
||||
(str) => EmailValidator.validate(str),
|
||||
undefined,
|
||||
(str) => {
|
||||
if(str.startsWith("mailto:")){
|
||||
str = str.substring("mailto:".length)
|
||||
}
|
||||
return EmailValidator.validate(str);
|
||||
},
|
||||
str => {
|
||||
if(str === undefined){return undefined}
|
||||
if(str.startsWith("mailto:")){
|
||||
str = str.substring("mailto:".length)
|
||||
}
|
||||
return str;
|
||||
},
|
||||
undefined,
|
||||
"email"),
|
||||
ValidatedTextField.tp(
|
||||
|
@ -395,9 +406,17 @@ export default class ValidatedTextField {
|
|||
if (str === undefined) {
|
||||
return false;
|
||||
}
|
||||
if(str.startsWith("tel:")){
|
||||
str = str.substring("tel:".length)
|
||||
}
|
||||
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
|
||||
},
|
||||
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational(),
|
||||
(str, country: () => string) => {
|
||||
if(str.startsWith("tel:")){
|
||||
str = str.substring("tel:".length)
|
||||
}
|
||||
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational();
|
||||
},
|
||||
undefined,
|
||||
"tel"
|
||||
),
|
||||
|
|
|
@ -189,8 +189,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
new VariableUiElement(
|
||||
State.state.featureSwitchIsDebugging.map(isDebugging => {
|
||||
if (isDebugging) {
|
||||
const config: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, "");
|
||||
return new TagRenderingAnswer(tags, config, "all_tags")
|
||||
const config_all_tags: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, "");
|
||||
const config_download: TagRenderingConfig = new TagRenderingConfig({render: "{export_as_geojson()}"}, "");
|
||||
const config_id: TagRenderingConfig = new TagRenderingConfig({render: "{open_in_iD()}"}, "");
|
||||
|
||||
return new Combine([new TagRenderingAnswer(tags, config_all_tags, "all_tags"),
|
||||
new TagRenderingAnswer(tags, config_download, ""),
|
||||
new TagRenderingAnswer(tags, config_id, "")])
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
|
@ -233,7 +233,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
onCancel: () => void): BaseUIElement {
|
||||
const self = this;
|
||||
const confirmationMap = Minimap.createMiniMap({
|
||||
allowMoving: false,
|
||||
allowMoving: state.featureSwitchIsDebugging.data ?? false,
|
||||
background: state.backgroundLayer
|
||||
})
|
||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||
|
@ -298,10 +298,19 @@ export class ConflateButton extends AbstractImportButton {
|
|||
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]): string[] {
|
||||
const deps = super.getLayerDependencies(argsRaw);
|
||||
// Force 'type_node' as dependency
|
||||
deps.push("type_node")
|
||||
return deps;
|
||||
}
|
||||
|
||||
constructElement(state: FeaturePipelineState,
|
||||
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
|
||||
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
|
||||
|
||||
return new FixedUiElement("ReplaceGeometry is currently very broken - use mapcomplete.osm.be for now").SetClass("alert")
|
||||
|
||||
const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
||||
const mergeConfigs = []
|
||||
|
@ -357,19 +366,19 @@ export class ImportWayButton extends AbstractImportButton {
|
|||
{
|
||||
name: "move_osm_point_if",
|
||||
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
|
||||
},{
|
||||
name:"max_move_distance",
|
||||
}, {
|
||||
name: "max_move_distance",
|
||||
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
|
||||
defaultValue: "1"
|
||||
},{
|
||||
name:"snap_onto_layers",
|
||||
doc:"If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
|
||||
|
||||
},{
|
||||
name:"snap_to_layer_max_distance",
|
||||
doc:"Distance to distort the geometry to snap to this layer",
|
||||
defaultValue: "0.1"
|
||||
}],
|
||||
}, {
|
||||
name: "snap_onto_layers",
|
||||
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
|
||||
|
||||
}, {
|
||||
name: "snap_to_layer_max_distance",
|
||||
doc: "Distance to distort the geometry to snap to this layer",
|
||||
defaultValue: "0.1"
|
||||
}],
|
||||
false
|
||||
)
|
||||
}
|
||||
|
@ -420,14 +429,14 @@ defaultValue: "0.1"
|
|||
}
|
||||
mergeConfigs.push(mergeConfig)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
||||
if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) {
|
||||
const moveDistance = Math.min(20, Number(args["max_move_distance"]))
|
||||
const moveDistance = Math.min(20, Number(args["max_move_distance"]))
|
||||
const mergeConfig: MergePointConfig = {
|
||||
mode: "move_osm_point" ,
|
||||
mode: "move_osm_point",
|
||||
ifMatches: new And(moveOsmPointIfTags),
|
||||
withinRangeOfM: moveDistance
|
||||
}
|
||||
|
|
|
@ -145,7 +145,8 @@ export default class ShowDataLayer {
|
|||
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
||||
onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer)
|
||||
});
|
||||
|
||||
|
||||
const selfLayer = this.geoLayer;
|
||||
const allFeats = this._features.features.data;
|
||||
for (const feat of allFeats) {
|
||||
if (feat === undefined) {
|
||||
|
@ -153,12 +154,11 @@ export default class ShowDataLayer {
|
|||
}
|
||||
try {
|
||||
if (feat.geometry.type === "LineString") {
|
||||
const self = this;
|
||||
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
|
||||
const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties);
|
||||
let offsettedLine;
|
||||
tagsSource
|
||||
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
|
||||
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags), [], undefined, true)
|
||||
.withEqualityStabilized((a, b) => {
|
||||
if (a === b) {
|
||||
return true
|
||||
|
@ -176,6 +176,9 @@ export default class ShowDataLayer {
|
|||
offsettedLine = L.polyline(coords, lineStyle);
|
||||
this.postProcessFeature(feat, offsettedLine)
|
||||
offsettedLine.addTo(this.geoLayer)
|
||||
|
||||
// If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
|
||||
return self.geoLayer !== selfLayer
|
||||
})
|
||||
} else {
|
||||
this.geoLayer.addData(feat);
|
||||
|
@ -186,11 +189,13 @@ export default class ShowDataLayer {
|
|||
}
|
||||
|
||||
if (options.zoomToFeatures ?? false) {
|
||||
try {
|
||||
const bounds = this.geoLayer.getBounds()
|
||||
mp.fitBounds(bounds, {animate: false})
|
||||
} catch (e) {
|
||||
console.debug("Invalid bounds", e)
|
||||
if(this.geoLayer.getLayers().length > 0){
|
||||
try {
|
||||
const bounds = this.geoLayer.getBounds()
|
||||
mp.fitBounds(bounds, {animate: false})
|
||||
} catch (e) {
|
||||
console.debug("Invalid bounds", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/Import
|
|||
import TagApplyButton from "./Popup/TagApplyButton";
|
||||
import AutoApplyButton from "./Popup/AutoApplyButton";
|
||||
import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json";
|
||||
import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -542,7 +543,7 @@ export default class SpecialVisualizations {
|
|||
const t = Translations.t.general.download;
|
||||
|
||||
return new SubtleButton(Svg.download_ui(),
|
||||
new Combine([t.downloadGpx.SetClass("font-bold text-lg"),
|
||||
new Combine([t.downloadFeatureAsGpx.SetClass("font-bold text-lg"),
|
||||
t.downloadGpxHelper.SetClass("subtle")]).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
console.log("Exporting as GPX!")
|
||||
|
@ -559,6 +560,41 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "export_as_geojson",
|
||||
docs: "Exports the selected feature as GeoJson-file",
|
||||
args: [],
|
||||
constr: (state, tagSource, args) => {
|
||||
const t = Translations.t.general.download;
|
||||
|
||||
return new SubtleButton(Svg.download_ui(),
|
||||
new Combine([t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
|
||||
t.downloadGeoJsonHelper.SetClass("subtle")]).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
console.log("Exporting as Geojson")
|
||||
const tags = tagSource.data
|
||||
const feature = state.allElements.ContainingFeatures.get(tags.id)
|
||||
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
|
||||
const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
||||
const data = JSON.stringify(feature, null, " ");
|
||||
Utils.offerContentsAsDownloadableFile(data, title + "_mapcomplete_export.geojson", {
|
||||
mimetype: "application/vnd.geo+json"
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "open_in_iD",
|
||||
docs: "Opens the current view in the iD-editor",
|
||||
args: [],
|
||||
constr: (state, feature ) => {
|
||||
return new OpenIdEditor(state, undefined, feature.data.id)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
funcName: "clear_location_history",
|
||||
docs: "A button to remove the travelled track information from the device",
|
||||
|
|
|
@ -138,10 +138,11 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
|
||||
const key = extraProperty.property
|
||||
const display = extraProperty.display
|
||||
const value: string[] = Array.from(wikidata.claims.get(key))
|
||||
if (value === undefined) {
|
||||
if (wikidata.claims?.get(key) === undefined) {
|
||||
continue
|
||||
}
|
||||
const value: string[] = Array.from(wikidata.claims.get(key))
|
||||
|
||||
if (display instanceof Translation) {
|
||||
els.push(display.Subs({value: value.join(", ")}).SetClass("m-2"))
|
||||
continue
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue