forked from MapComplete/MapComplete
Add possibility to upload your travelled track to OSM
This commit is contained in:
parent
9424364f3f
commit
312db3ad50
11 changed files with 208 additions and 44 deletions
|
@ -1,9 +1,9 @@
|
|||
import * as turf from '@turf/turf'
|
||||
import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon} from '@turf/turf'
|
||||
import {BBox} from "./BBox";
|
||||
import togpx from "togpx"
|
||||
import Constants from "../Models/Constants";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf";
|
||||
|
||||
export class GeoOperations {
|
||||
|
||||
|
@ -383,22 +383,21 @@ export class GeoOperations {
|
|||
return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates)
|
||||
}
|
||||
|
||||
public static AsGpx(feature, generatedWithLayer?: LayerConfig) : string{
|
||||
public static AsGpx(feature: Feature, options?: {layer?: LayerConfig, gpxMetadata?: any }) : string{
|
||||
|
||||
const metadata = {}
|
||||
const metadata = options?.gpxMetadata ?? {}
|
||||
metadata["time"] = metadata["time"] ?? new Date().toISOString()
|
||||
const tags = feature.properties
|
||||
|
||||
if (generatedWithLayer !== undefined) {
|
||||
if (options?.layer !== undefined) {
|
||||
|
||||
metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
|
||||
metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id
|
||||
metadata["name"] = options?.layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
|
||||
metadata["desc"] = "Generated with MapComplete layer " + options?.layer.id
|
||||
if (tags._backend?.contains("openstreetmap")) {
|
||||
metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["author"] = tags["_last_edit:contributor"]
|
||||
metadata["link"] = "https://www.openstreetmap.org/" + tags.id
|
||||
metadata["time"] = tags["_last_edit:timestamp"]
|
||||
} else {
|
||||
metadata["time"] = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {Utils} from "../../Utils";
|
|||
import {OsmObject} from "./OsmObject";
|
||||
import {Changes} from "./Changes";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import { Feature } from "@turf/turf";
|
||||
|
||||
export default class UserDetails {
|
||||
|
||||
|
@ -322,7 +323,7 @@ export class OsmConnection {
|
|||
|
||||
}
|
||||
|
||||
public async uploadGpxTrack(geojson: any, options: {
|
||||
public async uploadGpxTrack(gpx: string, options: {
|
||||
description: string,
|
||||
visibility: "private" | "public" | "trackable" | "identifiable",
|
||||
filename?: string
|
||||
|
@ -333,7 +334,6 @@ export class OsmConnection {
|
|||
*/
|
||||
labels: string[]
|
||||
}): Promise<{ id: number }> {
|
||||
const gpx = GeoOperations.AsGpx(geojson)
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
|
||||
return new Promise<{ id: number }>((ok, error) => {
|
||||
|
@ -355,8 +355,8 @@ export class OsmConnection {
|
|||
const auth = this.auth;
|
||||
const boundary ="987654"
|
||||
|
||||
var body = ""
|
||||
for (var key in contents) {
|
||||
let body = ""
|
||||
for (const key in contents) {
|
||||
body += "--" + boundary + "\r\n"
|
||||
body += "Content-Disposition: form-data; name=\"" + key + "\""
|
||||
if(extras[key] !== undefined){
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class MapState extends UserRelatedState {
|
|||
public currentUserLocation: SimpleFeatureSource;
|
||||
|
||||
/**
|
||||
* All previously visited points
|
||||
* All previously visited points, with their metadata
|
||||
*/
|
||||
public historicalUserLocations: SimpleFeatureSource;
|
||||
/**
|
||||
|
@ -77,6 +77,11 @@ export default class MapState extends UserRelatedState {
|
|||
* Time in seconds
|
||||
*/
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(7 * 24 * 60 * 60, "gps_location_retention")
|
||||
/**
|
||||
* A featureSource containing a single linestring which has the GPS-history of the user.
|
||||
* However, metadata (such as when every single point was visited) is lost here (but is kept in `historicalUserLocations`.
|
||||
* Note that this featureSource is _derived_ from 'historicalUserLocations'
|
||||
*/
|
||||
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled;
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,7 +78,6 @@ export class SubtleButton extends UIElement {
|
|||
})
|
||||
const loading = new Lazy(() => new Loading(loadingText) )
|
||||
return new VariableUiElement(state.map(st => {
|
||||
console.log("State is: ", st)
|
||||
if(st === "idle"){
|
||||
return button
|
||||
}
|
||||
|
|
96
UI/BigComponents/UploadTraceToOsmUI.ts
Normal file
96
UI/BigComponents/UploadTraceToOsmUI.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import Toggle from "../Input/Toggle";
|
||||
import {RadioButton} from "../Input/RadioButton";
|
||||
import {FixedInputElement} from "../Input/FixedInputElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Title from "../Base/Title";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
|
||||
|
||||
export default class UploadTraceToOsmUI extends Toggle {
|
||||
|
||||
|
||||
constructor(
|
||||
trace: () => string,
|
||||
state: {
|
||||
layoutToUse: LayoutConfig;
|
||||
osmConnection: OsmConnection
|
||||
}, options?: {
|
||||
whenUploaded?: () => void | Promise<void>
|
||||
}) {
|
||||
const t = Translations.t.general.uploadGpx
|
||||
|
||||
const traceVisibilities: {
|
||||
key: "private" | "public",
|
||||
name: Translation,
|
||||
docs: Translation
|
||||
}[] = [
|
||||
{
|
||||
key: "private",
|
||||
...Translations.t.general.uploadGpx.modes.private
|
||||
},
|
||||
{
|
||||
key: "public",
|
||||
...Translations.t.general.uploadGpx.modes.public
|
||||
}
|
||||
]
|
||||
|
||||
const dropdown = new RadioButton<"private" | "public">(
|
||||
traceVisibilities.map(tv => new FixedInputElement<"private" | "public">(
|
||||
new Combine([Translations.W(
|
||||
tv.name
|
||||
).SetClass("font-bold"), tv.docs]).SetClass("flex flex-col")
|
||||
, tv.key)),
|
||||
{
|
||||
value: <any>state?.osmConnection?.GetPreference("gps.trace.visibility")
|
||||
}
|
||||
)
|
||||
const description = new TextField({
|
||||
placeholder: t.placeHolder
|
||||
})
|
||||
const clicked = new UIEventSource<boolean>(false)
|
||||
|
||||
const confirmPanel = new Combine([
|
||||
new Title(t.title),
|
||||
t.intro0,
|
||||
t.intro1,
|
||||
|
||||
t.choosePermission,
|
||||
dropdown,
|
||||
new Title(t.description.title, 4),
|
||||
t.description.intro,
|
||||
description,
|
||||
new Combine([
|
||||
new SubtleButton(Svg.close_svg(), Translations.t.general.cancel).onClick(() => {
|
||||
clicked.setData(false)
|
||||
}).SetClass(""),
|
||||
new SubtleButton(Svg.upload_svg(), t.confirm).OnClickWithLoading(t.uploading, async () => {
|
||||
await state?.osmConnection?.uploadGpxTrack(trace(), {
|
||||
visibility: dropdown.GetValue().data,
|
||||
description: description.GetValue().data,
|
||||
labels: ["MapComplete", state?.layoutToUse?.id]
|
||||
})
|
||||
|
||||
if (options?.whenUploaded !== undefined) {
|
||||
await options.whenUploaded()
|
||||
}
|
||||
|
||||
}).SetClass("")
|
||||
]).SetClass("flex flex-wrap flex-wrap-reverse justify-between items-stretch")
|
||||
]).SetClass("flex flex-col p-4 rounded border-2 m-2 border-subtle")
|
||||
|
||||
|
||||
super(
|
||||
confirmPanel,
|
||||
new SubtleButton(Svg.upload_svg(), t.title)
|
||||
.onClick(() => clicked.setData(true)),
|
||||
clicked
|
||||
)
|
||||
}
|
||||
}
|
|
@ -14,14 +14,15 @@ export class RadioButton<T> extends InputElement<T> {
|
|||
elements: InputElement<T>[],
|
||||
options?: {
|
||||
selectFirstAsDefault?: true | boolean,
|
||||
dontStyle?: boolean
|
||||
dontStyle?: boolean,
|
||||
value?: UIEventSource<T>
|
||||
}
|
||||
) {
|
||||
super();
|
||||
options = options ?? {}
|
||||
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true;
|
||||
this._elements = Utils.NoNull(elements);
|
||||
this.value = new UIEventSource<T>(undefined);
|
||||
this.value = options?.value ?? new UIEventSource<T>(undefined);
|
||||
this._dontStyle = options.dontStyle ?? false
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,10 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel";
|
|||
import {OsmFeature} from "../Models/OsmFeature";
|
||||
import EditableTagRendering from "./Popup/EditableTagRendering";
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
||||
import UploadTraceToOsmUI from "./BigComponents/UploadTraceToOsmUI";
|
||||
import {Feature} from "geojson";
|
||||
import {GeoLocationPointProperties} from "../Logic/Actors/GeoLocationHandler";
|
||||
import {Point} from "@turf/turf";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -864,7 +868,7 @@ export default class SpecialVisualizations {
|
|||
const tags = tagSource.data
|
||||
const feature = state.allElements.ContainingFeatures.get(tags.id)
|
||||
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
|
||||
const gpx = GeoOperations.AsGpx(feature, matchingLayer)
|
||||
const gpx = GeoOperations.AsGpx(feature, {layer: matchingLayer})
|
||||
const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
||||
mimetype: "{gpx=application/gpx+xml}"
|
||||
|
@ -874,6 +878,35 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "upload_to_osm",
|
||||
docs: "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored.",
|
||||
args:[],
|
||||
constr(state, featureTags, args) {
|
||||
|
||||
function getTrace() {
|
||||
const userLocations : Feature<Point, GeoLocationPointProperties>[] = state.historicalUserLocations.features.data.map(f => f.feature)
|
||||
const trackPoints: string[] = []
|
||||
for (const l of userLocations) {
|
||||
let trkpt = ` <trkpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
||||
trkpt += ` <time>${l.properties.date}</time>`
|
||||
if(l.properties.altitude !== null && l.properties.altitude !== undefined ){
|
||||
trkpt += ` <ele>${l.properties.altitude}</ele>`
|
||||
}
|
||||
trkpt += " </trkpt>"
|
||||
trackPoints.push(trkpt)
|
||||
}
|
||||
const header = '<gpx version="1.1" creator="MapComplete track uploader" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
return header+"\n<trk><trkseg>\n"+trackPoints.join("\n")+"\n</trkseg></trk></gpx>"
|
||||
}
|
||||
|
||||
return new UploadTraceToOsmUI(getTrace, state,{
|
||||
whenUploaded: async () => {
|
||||
state.historicalUserLocations.features.setData([])
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "export_as_geojson",
|
||||
docs: "Exports the selected feature as GeoJson-file",
|
||||
|
|
|
@ -12,9 +12,12 @@
|
|||
"maxCacheAge": 0
|
||||
},
|
||||
"shownByDefault": false,
|
||||
"mapRendering":[
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": ["point","centroid"],
|
||||
"location": [
|
||||
"point",
|
||||
"centroid"
|
||||
],
|
||||
"icon": "square:red",
|
||||
"iconSize": "5,5,center"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": "gps_track",
|
||||
"description": "Meta layer showing the previous locations of the user as single line. Add this to your theme and override the icon to change the appearance of the current location.",
|
||||
"description": "Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track.",
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
"osmTags": "id=location_track",
|
||||
|
@ -22,6 +22,7 @@
|
|||
},
|
||||
"export_as_gpx",
|
||||
"export_as_geojson",
|
||||
"{upload_to_osm()}",
|
||||
"minimap",
|
||||
{
|
||||
"id": "delete",
|
||||
|
|
|
@ -811,6 +811,10 @@ video {
|
|||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.m-4 {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
@ -819,10 +823,6 @@ video {
|
|||
margin: 1.25rem;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.m-0\.5 {
|
||||
margin: 0.125rem;
|
||||
}
|
||||
|
@ -866,14 +866,6 @@ video {
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
@ -886,6 +878,10 @@ video {
|
|||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
@ -934,6 +930,10 @@ video {
|
|||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
@ -1162,6 +1162,10 @@ video {
|
|||
width: 2rem;
|
||||
}
|
||||
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
@ -1407,14 +1411,14 @@ video {
|
|||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.rounded-3xl {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-3xl {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
@ -1436,14 +1440,14 @@ video {
|
|||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-4 {
|
||||
border-width: 4px;
|
||||
}
|
||||
|
@ -2866,10 +2870,6 @@ input {
|
|||
width: 75%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
|
@ -2878,6 +2878,10 @@ input {
|
|||
width: 16.666667%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
|
|
@ -239,6 +239,29 @@
|
|||
"skip": "Skip this question",
|
||||
"skippedQuestions": "Some questions are skipped",
|
||||
"testing": "Testing - changes won't be saved",
|
||||
"uploadGpx": {
|
||||
"choosePermission": "Choose below if your track should be shared:",
|
||||
"confirm": "Confirm upload",
|
||||
"description": {
|
||||
"intro": "Optionally, you can enter a description of your trace below",
|
||||
"title": "Description"
|
||||
},
|
||||
"intro0": "By uploading your track, OpenStreetMap.org will retain a full copy of the track.",
|
||||
"intro1": "You will be able to download your track again and to load them into OpenStreetMap editing programs",
|
||||
"modes": {
|
||||
"private": {
|
||||
"docs": "The points of your track will be shared and aggregated among other tracks. The full track will be visible to you and you will be able to load it into other editing programs. OpenStreetMap.org retains a copy of your trace",
|
||||
"name": "Anonymous"
|
||||
},
|
||||
"public": {
|
||||
"docs": "Your trace will be visible to everyone, both on your user profile and on the list of GPS-traces on openstreetmap.org",
|
||||
"name": "Public"
|
||||
}
|
||||
},
|
||||
"placeHolder": "Enter a description of your trace",
|
||||
"title": "Upload your track to OpenStreetMap.org",
|
||||
"uploading": "Uploading your trace..."
|
||||
},
|
||||
"useSearch": "Use the search above to see presets",
|
||||
"useSearchForMore": "Use the search function to search within {total} more values....",
|
||||
"weekdays": {
|
||||
|
|
Loading…
Reference in a new issue