Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2024-06-20 15:14:10 +02:00
commit 943f90e1a7
14 changed files with 297 additions and 111 deletions

View file

@ -17,7 +17,7 @@ countrycoder.mapcomplete.org {
report.mapcomplete.org { report.mapcomplete.org {
reverse_proxy { reverse_proxy {
to http://127.0.0.1:2600 to http://127.0.0.1:2348
} }
} }

View file

@ -2,11 +2,49 @@
This server hosts the studio files and is used for expermintal builds. This server hosts the studio files and is used for expermintal builds.
For used hosts, see the Caddyfile For used ports, see the Caddyfile
To update caddy
```
cp Caddyfile /etc/caddy/
# If caddy was running via a console instead of as a service, do `caddy stop` now
systemctl reload caddy
```
Debug logs with: `journalctl -u caddy --no-pager | less +G`
Services:
## Cache forwarding ### studio + theme sync
As the ISP of Nerdlab is a bit picky, we use SSH-port-forwarding on the cache server: The studio server, handling those requests.
`ssh -R 5445:127.0.0.1:5445 hetzner` `npm run server:studio`
Additionally, this runs syncthing to make a backup of all theme files.
### LOD-server
A server scraping other websites.
`npm run server:ldjson`
### Error report server
A simple server logging everything it receives
`npm run server:errorreport`
### geo-ip
Provides geolocation based on
```
git clone https://github.com/pietervdvn/geoip-server
cd geoip-server
mkdir data
# Drop the databases from https://lite.ip2location.com/ in the data dir
npm run start
```

View file

@ -1,13 +1,17 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"title": { "title": {
"en": "Changes made with MapComplete" "en": "Changes made with MapComplete",
"de": "Änderungen mit MapComplete"
}, },
"shortDescription": { "shortDescription": {
"en": "Shows changes made by MapComplete" "en": "Shows changes made by MapComplete",
"de": "Änderungen von MapComplete anzeigen"
}, },
"description": { "description": {
"en": "This maps shows all the changes made with MapComplete" "en": "This maps shows all the changes made with MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
"pl": "Ta mapa pokazuje wszystkie zmiany wprowadzone za pomocą MapComplete"
}, },
"icon": "./assets/svg/logo.svg", "icon": "./assets/svg/logo.svg",
"hideFromOverview": true, "hideFromOverview": true,
@ -18,7 +22,9 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"name": { "name": {
"en": "Changeset centers" "en": "Changeset centers",
"de": "Zentrum der Änderungssätze",
"zh_Hant": "變更集中心"
}, },
"minzoom": 0, "minzoom": 0,
"source": { "source": {
@ -28,41 +34,48 @@
}, },
"title": { "title": {
"render": { "render": {
"en": "Changeset for {theme}" "en": "Changeset for {theme}",
"de": "Änderungssatz für {theme}"
} }
}, },
"description": { "description": {
"en": "Shows all MapComplete changes" "en": "Shows all MapComplete changes",
"de": "Alle MapComplete-Änderungen anzeigen"
}, },
"tagRenderings": [ "tagRenderings": [
{ {
"id": "show_changeset_id", "id": "show_changeset_id",
"render": { "render": {
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>" "en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
} }
}, },
{ {
"id": "contributor", "id": "contributor",
"question": { "question": {
"en": "What contributor did make this change?" "en": "What contributor did make this change?",
"de": "Welcher Mitwirkende hat diese Änderung vorgenommen?"
}, },
"freeform": { "freeform": {
"key": "user" "key": "user"
}, },
"render": { "render": {
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>" "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"de": "Änderung vorgenommen von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
} }
}, },
{ {
"id": "theme-id", "id": "theme-id",
"question": { "question": {
"en": "What theme was used to make this change?" "en": "What theme was used to make this change?",
"de": "Welches Thema wurde für die Änderung verwendet?"
}, },
"freeform": { "freeform": {
"key": "theme" "key": "theme"
}, },
"render": { "render": {
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>" "en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
} }
}, },
{ {
@ -71,19 +84,23 @@
"key": "locale" "key": "locale"
}, },
"question": { "question": {
"en": "What locale (language) was this change made in?" "en": "What locale (language) was this change made in?",
"de": "In welcher Benutzersprache wurde die Änderung vorgenommen?"
}, },
"render": { "render": {
"en": "User locale is {locale}" "en": "User locale is {locale}",
"de": "Benutzersprache {locale}"
} }
}, },
{ {
"id": "host", "id": "host",
"render": { "render": {
"en": "Change with with <a href='{host}'>{host}</a>" "en": "Change with with <a href='{host}'>{host}</a>",
"de": "Änderung über <a href='{host}'>{host}</a>"
}, },
"question": { "question": {
"en": "What host (website) was this change made with?" "en": "What host (website) was this change made with?",
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?"
}, },
"freeform": { "freeform": {
"key": "host" "key": "host"
@ -104,10 +121,12 @@
{ {
"id": "version", "id": "version",
"question": { "question": {
"en": "What version of MapComplete was used to make this change?" "en": "What version of MapComplete was used to make this change?",
"de": "Mit welcher MapComplete Version wurde die Änderung vorgenommen?"
}, },
"render": { "render": {
"en": "Made with {editor}" "en": "Made with {editor}",
"de": "Erstellt mit {editor}"
}, },
"freeform": { "freeform": {
"key": "editor" "key": "editor"
@ -493,7 +512,9 @@
} }
], ],
"question": { "question": {
"en": "Themename contains {search}" "en": "Themename contains {search}",
"de": "Themenname enthält {search}",
"pl": "Nazwa tematu zawiera {search}"
} }
} }
] ]
@ -509,7 +530,8 @@
} }
], ],
"question": { "question": {
"en": "Themename does <b>not</b> contain {search}" "en": "Themename does <b>not</b> contain {search}",
"de": "Themename enthält <b>not</b> {search}"
} }
} }
] ]
@ -525,7 +547,8 @@
} }
], ],
"question": { "question": {
"en": "Made by contributor {search}" "en": "Made by contributor {search}",
"de": "Erstellt vom Mitwirkenden {search}"
} }
} }
] ]
@ -541,7 +564,8 @@
} }
], ],
"question": { "question": {
"en": "<b>Not</b> made by contributor {search}" "en": "<b>Not</b> made by contributor {search}",
"de": "<b>Nicht</b> erstellt von Mitwirkendem {search}"
} }
} }
] ]
@ -558,7 +582,8 @@
} }
], ],
"question": { "question": {
"en": "Made before {search}" "en": "Made before {search}",
"de": "Erstellt vor {search}"
} }
} }
] ]
@ -575,7 +600,8 @@
} }
], ],
"question": { "question": {
"en": "Made after {search}" "en": "Made after {search}",
"de": "Erstellt nach {search}"
} }
} }
] ]
@ -591,7 +617,8 @@
} }
], ],
"question": { "question": {
"en": "User language (iso-code) {search}" "en": "User language (iso-code) {search}",
"de": "Benutzersprache (ISO-Code) {search}"
} }
} }
] ]
@ -607,7 +634,8 @@
} }
], ],
"question": { "question": {
"en": "Made with host {search}" "en": "Made with host {search}",
"de": "Erstellt mit Host {search}"
} }
} }
] ]
@ -618,7 +646,8 @@
{ {
"osmTags": "add-image>0", "osmTags": "add-image>0",
"question": { "question": {
"en": "Changeset added at least one image" "en": "Changeset added at least one image",
"de": "Änderungssatz hat mindestens ein Bild hinzugefügt"
} }
} }
] ]
@ -629,7 +658,8 @@
{ {
"osmTags": "theme!=grb", "osmTags": "theme!=grb",
"question": { "question": {
"en": "Exclude GRB theme" "en": "Exclude GRB theme",
"de": "GRB-Thema ausschließen"
} }
} }
] ]
@ -640,7 +670,8 @@
{ {
"osmTags": "theme!=etymology", "osmTags": "theme!=etymology",
"question": { "question": {
"en": "Exclude etymology theme" "en": "Exclude etymology theme",
"de": "Etymologie-Thema ausschließen"
} }
} }
] ]
@ -655,7 +686,8 @@
{ {
"id": "link_to_more", "id": "link_to_more",
"render": { "render": {
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>" "en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"de": "Weitere Statistiken gibt es <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>"
} }
}, },
{ {

View file

@ -288,6 +288,7 @@
"loadingTheme": "Loading {theme}…", "loadingTheme": "Loading {theme}…",
"loginFailed": "Logging in into OpenStreetMap failed", "loginFailed": "Logging in into OpenStreetMap failed",
"loginFailedOfflineMode": "OpenStreetMap.org is currently not available due to maintenance. Making edits will be possible soon", "loginFailedOfflineMode": "OpenStreetMap.org is currently not available due to maintenance. Making edits will be possible soon",
"loginFailedOfflineOther": "OpenStreetMap.org could not be reached",
"loginFailedReadonlyMode": "OpenStreetMap.org is currently in readonly mode due to maintenance. Making edits will be possible soon", "loginFailedReadonlyMode": "OpenStreetMap.org is currently in readonly mode due to maintenance. Making edits will be possible soon",
"loginFailedUnreachableMode": "OpenStreetMap.org is currently not reachable. Are you connected to the internet or do you block third parties? Try again later", "loginFailedUnreachableMode": "OpenStreetMap.org is currently not reachable. Are you connected to the internet or do you block third parties? Try again later",
"loginOnlyNeededToEdit": "if you want to make changes", "loginOnlyNeededToEdit": "if you want to make changes",

View file

@ -25,6 +25,7 @@
"#summary_server": "Should be the endpoint; appending status.json should work", "#summary_server": "Should be the endpoint; appending status.json should work",
"summary_server": "https://cache.mapcomplete.org/", "summary_server": "https://cache.mapcomplete.org/",
"geoip_server": "https://ipinfo.mapcomplete.org/", "geoip_server": "https://ipinfo.mapcomplete.org/",
"error_server": "https://report.mapcomplete.org/report",
"disabled:oauth_credentials": { "disabled:oauth_credentials": {
"##": "DEV", "##": "DEV",
"#": "This client-id is registered by 'MapComplete' on https://master.apis.dev.openstreetmap.org/", "#": "This client-id is registered by 'MapComplete' on https://master.apis.dev.openstreetmap.org/",
@ -114,8 +115,10 @@
"dloadVelopark": "vite-node scripts/velopark/veloParkToGeojson.ts ", "dloadVelopark": "vite-node scripts/velopark/veloParkToGeojson.ts ",
"compareVelopark": "vite-node scripts/velopark/compare.ts -- velopark_nonsynced_.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\\ Fietsenstallingen_OSM_met_velopark_ref.geojson\n", "compareVelopark": "vite-node scripts/velopark/compare.ts -- velopark_nonsynced_.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\\ Fietsenstallingen_OSM_met_velopark_ref.geojson\n",
"scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/", "scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/",
"summary-server": "vite-node scripts/osm2pgsql/tilecountServer.ts", "server:summary": "vite-node scripts/osm2pgsql/tilecountServer.ts",
"ldjson-server": "vite-node scripts/serverLdScrape.ts", "server:ldjson": "vite-node scripts/serverLdScrape.ts",
"sever:studio": "vite-node scripts/studioServer -- /root/git/MapComplete/assets",
"server:errorreport": "vite-node scripts/serverErrorReport.ts -- /root/error_reports/",
"generate:buildDbScript": "vite-node scripts/osm2pgsql/generateBuildDbScript.ts" "generate:buildDbScript": "vite-node scripts/osm2pgsql/generateBuildDbScript.ts"
}, },
"keywords": [ "keywords": [

View file

@ -1229,14 +1229,14 @@ video {
height: 6rem; height: 6rem;
} }
.h-screen {
height: 100vh;
}
.h-full { .h-full {
height: 100%; height: 100%;
} }
.h-screen {
height: 100vh;
}
.h-fit { .h-fit {
height: -webkit-fit-content; height: -webkit-fit-content;
height: -moz-fit-content; height: -moz-fit-content;

View file

@ -1,17 +1,36 @@
import http from "node:http" import http from "node:http"
export interface Handler {
mustMatch: string | RegExp
mimetype: string
addHeaders?: Record<string, string>
handle: (path: string, queryParams: URLSearchParams, req: http.IncomingMessage) => Promise<string>
}
class ServerUtils {
public static getBody(req: http.IncomingMessage): Promise<string> {
return new Promise<string>((resolve) => {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
resolve(body)
});
})
}
}
export class Server { export class Server {
constructor( constructor(
port: number, port: number,
options: { options: {
ignorePathPrefix?: string[] ignorePathPrefix?: string[]
}, },
handle: { handle: Handler[]
mustMatch: string | RegExp
mimetype: string
addHeaders?: Record<string, string>
handle: (path: string, queryParams: URLSearchParams) => Promise<string>
}[]
) { ) {
handle.push({ handle.push({
mustMatch: "", mustMatch: "",
@ -81,8 +100,9 @@ export class Server {
res.end() res.end()
return return
} }
let body = undefined
if (req.method === "POST" || req.method === "UPDATE") { if (req.method === "POST" || req.method === "UPDATE") {
return body = await ServerUtils.getBody(req)
} }
if (req.method === "DELETE") { if (req.method === "DELETE") {
@ -90,7 +110,7 @@ export class Server {
} }
try { try {
const result = await handler.handle(path, url.searchParams) const result = await handler.handle(path, url.searchParams, req, body)
if (result === undefined) { if (result === undefined) {
res.writeHead(500) res.writeHead(500)
res.write("Could not fetch this website, probably blocked by them") res.write("Could not fetch this website, probably blocked by them")

View file

@ -0,0 +1,52 @@
import { Handler, Server } from "./server"
import Script from "./Script"
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs"
import { mkdir } from "node:fs"
import ScriptUtils from "./ScriptUtils"
class ServerErrorReport extends Script {
constructor() {
super("A server which receives and logs error reports")
}
async main(args: string[]): Promise<void> {
const logDirectory = args[0] ?? "./error_logs"
console.log("Logging to directory", logDirectory)
if (!existsSync(logDirectory)) {
mkdirSync(logDirectory)
console.log("Created this directory")
}
let errorReport = 0
new Server(2348, {},
[<Handler>{
mustMatch: "report",
mimetype: "application/json",
handle: async (_, queryParams, req, body) => {
if (!body) {
throw "{\"error\": \"No body; use a post request\"}"
}
console.log(body)
const ip = <string>req.headers["x-forwarded-for"]
const d = new Date()
const date = d.toISOString()
const file = logDirectory + "/" + d.getUTCFullYear() + "_" + d.getUTCMonth() + "_" + d.getUTCDay() + ".lines.json"
try{
body = JSON.parse(body)
}catch (e) {
// could not parse, we'll save it as is
}
const contents = "\n"+JSON.stringify({ ip, index: errorReport, date, message: body })
if (!existsSync(file)) {
writeFileSync(file, contents)
} else {
appendFileSync(file, contents)
}
errorReport++
return `{"status":"ok", "nr": ${errorReport}}`
},
}])
}
}
new ServerErrorReport().run()

View file

@ -43,6 +43,7 @@ export class Changes {
private readonly previouslyCreated: OsmObject[] = [] private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean private readonly _leftRightSensitive: boolean
private readonly _changesetHandler: ChangesetHandler private readonly _changesetHandler: ChangesetHandler
private readonly _reportError?: (string: string | Error) => void
constructor( constructor(
state: { state: {
@ -53,7 +54,8 @@ export class Changes {
historicalUserLocations?: FeatureSource historicalUserLocations?: FeatureSource
featureSwitches?: FeatureSwitchState featureSwitches?: FeatureSwitchState
}, },
leftRightSensitive: boolean = false leftRightSensitive: boolean = false,
reportError?: (string: string | Error) => void,
) { ) {
this._leftRightSensitive = leftRightSensitive this._leftRightSensitive = leftRightSensitive
// We keep track of all changes just as well // We keep track of all changes just as well
@ -62,11 +64,13 @@ export class Changes {
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? []))
this.state = state this.state = state
this.backend = state.osmConnection.Backend() this.backend = state.osmConnection.Backend()
this._reportError = reportError
this._changesetHandler = new ChangesetHandler( this._changesetHandler = new ChangesetHandler(
state.dryRun, state.dryRun,
state.osmConnection, state.osmConnection,
state.featurePropertiesStore, state.featurePropertiesStore,
this this,
e => this._reportError(e)
) )
this.historicalUserLocations = state.historicalUserLocations this.historicalUserLocations = state.historicalUserLocations
@ -234,6 +238,7 @@ export class Changes {
console.log("Changes flushed. Your changeset is " + csNumber) console.log("Changes flushed. Your changeset is " + csNumber)
this.errors.setData([]) this.errors.setData([])
} catch (e) { } catch (e) {
this._reportError(e)
this.isUploading.setData(false) this.isUploading.setData(false)
this.errors.data.push(e) this.errors.data.push(e)
this.errors.ping() this.errors.ping()
@ -518,6 +523,9 @@ export class Changes {
const osmObj = await downloader.DownloadObjectAsync(id, 0) const osmObj = await downloader.DownloadObjectAsync(id, 0)
return { id, osmObj } return { id, osmObj }
} catch (e) { } catch (e) {
this._reportError( "Could not download OSM-object"+
id+
" dropping it from the changes (" + e + ")")
console.error( console.error(
"Could not download OSM-object", "Could not download OSM-object",
id, id,
@ -685,6 +693,7 @@ export class Changes {
} }
return result return result
} catch (e) { } catch (e) {
this._reportError(e)
console.error("Could not upload some changes:", e) console.error("Could not upload some changes:", e)
this.errors.data.push(e) this.errors.data.push(e)
this.errors.ping() this.errors.ping()

View file

@ -26,6 +26,7 @@ export class ChangesetHandler {
* @private * @private
*/ */
private readonly _remappings = new Map<string, string>() private readonly _remappings = new Map<string, string>()
private readonly _reportError: (e: (string | Error)) => void
constructor( constructor(
dryRun: Store<boolean>, dryRun: Store<boolean>,
@ -34,9 +35,11 @@ export class ChangesetHandler {
| FeaturePropertiesStore | FeaturePropertiesStore
| { addAlias: (id0: string, id1: string) => void } | { addAlias: (id0: string, id1: string) => void }
| undefined, | undefined,
changes: Changes changes: Changes,
reportError: (e: string | Error) => void
) { ) {
this.osmConnection = osmConnection this.osmConnection = osmConnection
this._reportError = reportError
this.allElements = <FeaturePropertiesStore>allElements this.allElements = <FeaturePropertiesStore>allElements
this.changes = changes this.changes = changes
this._dryRun = dryRun this._dryRun = dryRun
@ -148,8 +151,13 @@ export class ChangesetHandler {
await this.UpdateTags(csId, extraMetaTags) await this.UpdateTags(csId, extraMetaTags)
} }
} catch (e) { } catch (e) {
console.error("Could not open/upload changeset due to ", e) if(this._reportError){
this._reportError(e)
}
console.warn("Could not open/upload changeset due to ", e,"trying t")
openChangeset.setData(undefined) openChangeset.setData(undefined)
throw e
} }
} else { } else {
// There still exists an open changeset (or at least we hope so) // There still exists an open changeset (or at least we hope so)
@ -178,8 +186,12 @@ export class ChangesetHandler {
) )
await this.UpdateTags(csId, rewrittenTags) await this.UpdateTags(csId, rewrittenTags)
} catch (e) { } catch (e) {
if(this._reportError){
this._reportError(e)
}
console.warn("Could not upload, changeset is probably closed: ", e) console.warn("Could not upload, changeset is probably closed: ", e)
openChangeset.setData(undefined) openChangeset.setData(undefined)
throw e
} }
} }
} }

View file

@ -164,6 +164,7 @@ export default class Constants {
*/ */
public static VectorTileServer: string | undefined = Constants.config.mvt_layer_server public static VectorTileServer: string | undefined = Constants.config.mvt_layer_server
public static GeoIpServer: string | undefined = Constants.config.geoip_server public static GeoIpServer: string | undefined = Constants.config.geoip_server
public static ErrorReportServer: string | undefined = Constants.config.error_server
public static readonly maptilerApiKey = "GvoVAJgu46I5rZapJuAy" public static readonly maptilerApiKey = "GvoVAJgu46I5rZapJuAy"
public static readonly SummaryServer: string = Constants.config.summary_server public static readonly SummaryServer: string = Constants.config.summary_server

View file

@ -2,11 +2,7 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization" import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource"
import { import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties" import { ExportableMap, MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState" import LayerState from "../Logic/State/LayerState"
@ -50,9 +46,7 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, { import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"
FeatureViewState,
} from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer" import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
@ -158,7 +152,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureSwitches = new FeatureSwitchState(layout) this.featureSwitches = new FeatureSwitchState(layout)
this.guistate = new MenuState( this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data, this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id layout.id,
) )
this.map = new UIEventSource<MlMap>(undefined) this.map = new UIEventSource<MlMap>(undefined)
const geolocationState = new GeoLocationState() const geolocationState = new GeoLocationState()
@ -174,14 +168,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
oauth_token: QueryParameters.GetQueryParameter( oauth_token: QueryParameters.GetQueryParameter(
"oauth_token", "oauth_token",
undefined, undefined,
"Used to complete the login" "Used to complete the login",
), ),
}) })
this.userRelatedState = new UserRelatedState( this.userRelatedState = new UserRelatedState(
this.osmConnection, this.osmConnection,
layout, layout,
this.featureSwitches, this.featureSwitches,
this.mapProperties this.mapProperties,
) )
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
this.mapProperties.allowRotating.setData(fixated !== "yes") this.mapProperties.allowRotating.setData(fixated !== "yes")
@ -192,13 +186,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
geolocationState, geolocationState,
this.selectedElement, this.selectedElement,
this.mapProperties, this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime this.userRelatedState.gpsLocationHistoryRetentionTime,
) )
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties) this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
this.availableLayers = AvailableRasterLayers.layersAvailableAt( this.availableLayers = AvailableRasterLayers.layersAvailableAt(
this.mapProperties.location, this.mapProperties.location,
this.osmConnection.isLoggedIn this.osmConnection.isLoggedIn,
) )
const self = this const self = this
@ -210,7 +204,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const isDisplayed = QueryParameters.GetBooleanQueryParameter( const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id, "overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true, rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown" "Wether or not overlayer layer " + rasterInfo.id + " is shown",
) )
const state = { isDisplayed } const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state) overlayLayerStates.set(rasterInfo.id, state)
@ -235,7 +229,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(), this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed, (id) => self.layerState.filteredLayers.get(id).isDisplayed,
mvtAvailableLayers, mvtAvailableLayers,
this.fullNodeDatabase this.fullNodeDatabase,
) )
let currentViewIndex = 0 let currentViewIndex = 0
@ -253,7 +247,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
id: "current_view_" + currentViewIndex, id: "current_view_" + currentViewIndex,
}), }),
] ]
}) }),
) )
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
@ -270,19 +264,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
historicalUserLocations: this.geolocation.historicalUserLocations, historicalUserLocations: this.geolocation.historicalUserLocations,
featureSwitches: this.featureSwitches, featureSwitches: this.featureSwitches,
}, },
layout?.isLeftRightSensitive() ?? false layout?.isLeftRightSensitive() ?? false,
e => this.reportError(e),
) )
this.historicalUserLocations = this.geolocation.historicalUserLocations this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource( this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes, this.changes,
layoutSource, layoutSource,
this.featureProperties this.featureProperties,
) )
layoutSource.addSource(this.newFeatures) layoutSource.addSource(this.newFeatures)
const perLayer = new PerLayerFeatureSourceSplitter( const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter( Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef?.source !== null (l) => l.layerDef?.source !== null,
), ),
new ChangeGeometryApplicator(this.indexedFeatures, this.changes), new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
{ {
@ -293,10 +288,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
"Got ", "Got ",
features.length, features.length,
"leftover features, such as", "leftover features, such as",
features[0].properties features[0].properties,
) )
}, },
} },
) )
this.perLayer = perLayer.perLayer this.perLayer = perLayer.perLayer
} }
@ -335,12 +330,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.lastClickObject = new LastClickFeatureSource( this.lastClickObject = new LastClickFeatureSource(
this.layout, this.layout,
this.mapProperties.lastClickLocation this.mapProperties.lastClickLocation,
) )
this.osmObjectDownloader = new OsmObjectDownloader( this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(), this.osmConnection.Backend(),
this.changes this.changes,
) )
this.perLayerFiltered = this.showNormalDataOn(this.map) this.perLayerFiltered = this.showNormalDataOn(this.map)
@ -351,7 +346,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
currentZoom: this.mapProperties.zoom, currentZoom: this.mapProperties.zoom,
layerState: this.layerState, layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds, bounds: this.visualFeedbackViewportBounds,
} },
) )
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.imageUploadManager = new ImageUploadManager( this.imageUploadManager = new ImageUploadManager(
@ -359,7 +354,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Imgur.singleton, Imgur.singleton,
this.featureProperties, this.featureProperties,
this.osmConnection, this.osmConnection,
this.changes this.changes,
) )
this.favourites = new FavouritesFeatureSource(this) this.favourites = new FavouritesFeatureSource(this)
@ -402,7 +397,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
LayoutSource.fromCacheZoomLevel, LayoutSource.fromCacheZoomLevel,
fs, fs,
this.featureProperties, this.featureProperties,
fs.layer.layerDef.maxAgeOfCache fs.layer.layerDef.maxAgeOfCache,
) )
toLocalStorage.set(layerId, storage) toLocalStorage.set(layerId, storage)
}) })
@ -415,7 +410,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const doShowLayer = this.mapProperties.zoom.map( const doShowLayer = this.mapProperties.zoom.map(
(z) => (z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed] [fs.layer.isDisplayed],
) )
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
@ -432,7 +427,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
fs.layer, fs.layer,
fs, fs,
(id) => this.featureProperties.getStore(id), (id) => this.featureProperties.getStore(id),
this.layerState.globalFilters this.layerState.globalFilters,
) )
filteringFeatureSource.set(layerName, filtered) filteringFeatureSource.set(layerName, filtered)
@ -573,7 +568,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
return return
} }
this.selectClosestAtCenter(0) this.selectClosestAtCenter(0)
} },
) )
for (let i = 1; i < 9; i++) { for (let i = 1; i < 9; i++) {
@ -591,7 +586,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
onUp: true, onUp: true,
}, },
doc, doc,
() => this.selectClosestAtCenter(i - 1) () => this.selectClosestAtCenter(i - 1),
) )
} }
@ -608,7 +603,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.featureSwitches.featureSwitchBackgroundSelection.data) { if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
this.guistate.backgroundLayerSelectionIsOpened.setData(true) this.guistate.backgroundLayerSelectionIsOpened.setData(true)
} }
} },
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
@ -620,14 +615,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.featureSwitches.featureSwitchFilter.data) { if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView() this.guistate.openFilterView()
} }
} },
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ shift: "O" }, { shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik, Translations.t.hotkeyDocumentation.selectMapnik,
() => { () => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
} },
) )
const setLayerCategory = (category: EliCategory) => { const setLayerCategory = (category: EliCategory) => {
const available = this.availableLayers.data const available = this.availableLayers.data
@ -635,7 +630,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const best = RasterLayerUtils.SelectBestLayerAccordingTo( const best = RasterLayerUtils.SelectBestLayerAccordingTo(
available, available,
category, category,
current.data current.data,
) )
console.log("Best layer for category", category, "is", best.properties.id) console.log("Best layer for category", category, "is", best.properties.id)
current.setData(best) current.setData(best)
@ -644,26 +639,26 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "O" }, { nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap, Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap") () => setLayerCategory("osmbasedmap"),
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "M" }, { nomod: "M" },
Translations.t.hotkeyDocumentation.selectMap, Translations.t.hotkeyDocumentation.selectMap,
() => setLayerCategory("map") () => setLayerCategory("map"),
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "P" }, { nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial, Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo") () => setLayerCategory("photo"),
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "L" }, { nomod: "L" },
Translations.t.hotkeyDocumentation.geolocate, Translations.t.hotkeyDocumentation.geolocate,
() => { () => {
this.geolocationControl.handleClick() this.geolocationControl.handleClick()
} },
) )
return true return true
}) })
@ -675,7 +670,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Translations.t.hotkeyDocumentation.translationMode, Translations.t.hotkeyDocumentation.translationMode,
() => { () => {
Locale.showLinkToWeblate.setData(!Locale.showLinkToWeblate.data) Locale.showLinkToWeblate.setData(!Locale.showLinkToWeblate.data)
} },
) )
} }
@ -686,7 +681,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const normalLayers = this.layout.layers.filter( const normalLayers = this.layout.layers.filter(
(l) => (l) =>
Constants.priviliged_layers.indexOf(<any>l.id) < 0 && Constants.priviliged_layers.indexOf(<any>l.id) < 0 &&
!l.id.startsWith("note_import") !l.id.startsWith("note_import"),
) )
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom)) const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
@ -694,7 +689,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
(l) => (l) =>
Constants.priviliged_layers.indexOf(<any>l.id) < 0 && Constants.priviliged_layers.indexOf(<any>l.id) < 0 &&
l.source.geojsonSource === undefined && l.source.geojsonSource === undefined &&
l.doCount l.doCount,
) )
const summaryTileSource = new SummaryTileSource( const summaryTileSource = new SummaryTileSource(
Constants.SummaryServer, Constants.SummaryServer,
@ -703,7 +698,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties, this.mapProperties,
{ {
isActive: this.mapProperties.zoom.map((z) => z <= maxzoom), isActive: this.mapProperties.zoom.map((z) => z <= maxzoom),
} },
) )
return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers) return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
} }
@ -723,12 +718,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
gps_location_history: this.geolocation.historicalUserLocations, gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack, gps_track: this.geolocation.historicalUserLocationsTrack,
selected_element: new StaticFeatureSource( selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f])) this.selectedElement.map((f) => (f === undefined ? empty : [f])),
), ),
range: new StaticFeatureSource( range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) => this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })] bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })],
) ),
), ),
current_view: this.currentView, current_view: this.currentView,
favourite: this.favourites, favourite: this.favourites,
@ -743,7 +738,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
ShowDataLayer.showRange( ShowDataLayer.showRange(
this.map, this.map,
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
this.featureSwitches.featureSwitchIsTesting this.featureSwitches.featureSwitchIsTesting,
) )
} }
const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view") const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view")
@ -757,7 +752,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
currentViewLayer, currentViewLayer,
this.layout, this.layout,
this.osmObjectDownloader, this.osmObjectDownloader,
this.featureProperties this.featureProperties,
) )
}) })
} }
@ -801,20 +796,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
const lastClickLayerConfig = new LayerConfig( const lastClickLayerConfig = new LayerConfig(
<LayerConfigJson>last_click_layerconfig, <LayerConfigJson>last_click_layerconfig,
"last_click" "last_click",
) )
const lastClickFiltered = const lastClickFiltered =
lastClickLayerConfig.isShown === undefined lastClickLayerConfig.isShown === undefined
? specialLayers.last_click ? specialLayers.last_click
: specialLayers.last_click.features.mapD((fs) => : specialLayers.last_click.features.mapD((fs) =>
fs.filter((f) => { fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties( const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties f.properties,
) )
console.log("LastClick ", f, "matches", matches) console.log("LastClick ", f, "matches", matches)
return matches return matches
}) }),
) )
new ShowDataLayer(this.map, { new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered), features: new StaticFeatureSource(lastClickFiltered),
layer: lastClickLayerConfig, layer: lastClickLayerConfig,
@ -859,7 +854,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.rasterLayer, this.mapProperties.rasterLayer,
this.availableLayers, this.availableLayers,
this.featureSwitches.backgroundLayerId, this.featureSwitches.backgroundLayerId,
this.userRelatedState.preferredBackgroundLayer this.userRelatedState.preferredBackgroundLayer,
) )
} }
@ -867,4 +862,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.guistate.closeAll() this.guistate.closeAll()
this.selectedElement.setData(this.currentView.features?.data?.[0]) this.selectedElement.setData(this.currentView.features?.data?.[0])
} }
public async reportError(message: string | Error) {
console.log(">>> Reporting error to",Constants.ErrorReportServer, message)
let stacktrace: string = new Error().stack
await fetch(Constants.ErrorReportServer, {
method: "POST",
body: JSON.stringify({
stacktrace,
message: ""+message,
layout: this.layout.id,
username: this.osmConnection.userDetails.data?.name,
userid: this.osmConnection.userDetails.data?.uid,
pendingChanges: this.changes.pendingChanges.data,
}),
})
}
} }

View file

@ -7,6 +7,7 @@
import Tr from "./Tr.svelte" import Tr from "./Tr.svelte"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import Invalid from "../../assets/svg/Invalid.svelte" import Invalid from "../../assets/svg/Invalid.svelte"
import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath"
export let state: { export let state: {
osmConnection: OsmConnection osmConnection: OsmConnection
@ -40,9 +41,13 @@
</slot> </slot>
{:else if !silentFail && $loadingStatus === "error"} {:else if !silentFail && $loadingStatus === "error"}
<slot name="error"> <slot name="error">
<div class="alert max-w-64 flex items-center"> <div class="alert max-w-64 flex items-center ">
<Invalid class="m-2 h-8 w-8 shrink-0" /> <Invalid class="m-2 h-8 w-8 shrink-0" />
<Tr t={offlineModes[$apiState]} /> <Tr t={offlineModes[$apiState] ?? t.loginFailedOfflineOther} />
<button class="justify-self-end" on:click={() => state.osmConnection.AttemptLogin()}>
<ArrowPath class="w-6 h-6"/>
Retry
</button>
</div> </div>
</slot> </slot>
{:else if $loadingStatus === "logged-in"} {:else if $loadingStatus === "logged-in"}

View file

@ -95,6 +95,7 @@ export interface SpecialVisualizationState {
readonly geolocation: GeoLocationHandler readonly geolocation: GeoLocationHandler
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
reportError(message: string): Promise<void>
} }
export interface SpecialVisualization { export interface SpecialVisualization {