Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-10-11 04:37:19 +02:00
commit e1a785ba9c
34 changed files with 1406 additions and 1306 deletions

View file

@ -20,7 +20,7 @@
]
}
},
"minzoom": 19,
"minzoom": 18,
"title": {
"render": {
"en": "Ticket Machine",

View file

@ -13,7 +13,7 @@
"source": {
"osmTags": "amenity=ticket_validator"
},
"minzoom": 19,
"minzoom": 18,
"title": {
"render": {
"en": "Ticket Validator",

View file

@ -47,7 +47,7 @@
"osmTags": "building~*",
"maxCacheAge": 0
},
"minzoom": 19,
"minzoom": 18,
"calculatedTags": [
"_surface:strict:=feat(get)('_surface')"
],
@ -115,10 +115,23 @@
},
"maxCacheAge": 0
},
"minzoom": 19,
"pointRendering": [],
"lineRendering": [
{}
"minzoom": 18,
"pointRendering": [
{
"label": {
"render": "<div style='color: black' class='rounded-full p-1 font-bold relative'>{addr:housenumber}</div>",
"condition": "addr:housenumber~*"
},
"location": [
"point",
"centroid"
]
},
{
"width": {
"render": 1
}
}
]
},
{
@ -142,7 +155,7 @@
"osmTags": "identificatie~*",
"maxCacheAge": 0
},
"minzoom": 19,
"minzoom": 18,
"calculatedTags": [
"_overlaps_with_buildings=overlapWith(feat)('osm:buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
"_overlaps_with=feat(get)('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )",
@ -309,7 +322,7 @@
"osmTags": "identificatie~*",
"maxCacheAge": 0
},
"minzoom": 19,
"minzoom": 18,
"calculatedTags": [
"_closed_osm_addr:=closest(feat)('osm:adresses').properties",
"_bag_obj:addr:housenumber=`${feat.properties.huisnummer}${feat.properties.huisletter}${(feat.properties.toevoeging != '') ? '-' : ''}${feat.properties.toevoeging}`",

View file

@ -68,7 +68,7 @@
{
"builtin": "kerbs",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"iconBadges": [

View file

@ -468,7 +468,10 @@
"guidepost"
],
"override": {
"minzoom": 15
"minzoom": 15,
"mapRendering": [{
"iconSize": "30,30"
}]
}
}
],

View file

@ -0,0 +1,14 @@
{
"id": "guideposts",
"title": {
"en": "Guideposts"
},
"description": {
"en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking, cycling, skiing or horseback riding routes to indicate the directions to different destinations. Additionally, they are often named after a region or place and show the altitude.\n\nThe position of a signpost can be used by a hiker/biker/rider/skier as a confirmation of the current position, especially if they use a printed map without a GPS receiver. "
},
"icon": "./assets/layers/guidepost/guidepost.svg",
"startZoom": 2,
"layers": [
"guidepost"
]
}

View file

@ -111,7 +111,7 @@
"=presets": [],
"=name": null,
"override": {
"minzoom": 19
"minzoom": 18
}
}
]

View file

@ -32,7 +32,7 @@
{
"builtin": "indoors",
"override": {
"minzoom": 19,
"minzoom": 18,
"name": null,
"passAllFeatures": true
}
@ -43,7 +43,7 @@
"name": null,
"tagRendering": null,
"title": "null",
"minzoom": 19,
"minzoom": 18,
"shownByDefault": false
}
},
@ -71,7 +71,7 @@
{
"builtin": "entrance",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/onwheels/entrance.svg"
@ -131,7 +131,7 @@
{
"builtin": "kerbs",
"override": {
"minzoom": 19,
"minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{
@ -289,7 +289,7 @@
{
"builtin": "toilet",
"override": {
"minzoom": 19,
"minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{
@ -349,7 +349,7 @@
{
"builtin": "reception_desk",
"override": {
"minzoom": 19,
"minzoom": 18,
"syncSelection": "theme-only"
}
},
@ -357,7 +357,7 @@
{
"builtin": "elevator",
"override": {
"minzoom": 19,
"minzoom": 18,
"syncSelection": "theme-only",
"mapRendering": [
{

View file

@ -165,7 +165,7 @@
{
"builtin": "food",
"override": {
"minzoom": 19,
"minzoom": 18,
"filter": null,
"name": null
}
@ -181,7 +181,7 @@
{
"builtin": "shops",
"override": {
"minzoom": 19,
"minzoom": 18,
"filter": null,
"presets": [
{

View file

@ -32,7 +32,7 @@
{
"builtin": "indoors",
"override": {
"minzoom": 19,
"minzoom": 18,
"passAllFeatures": true,
"mapRendering": [
{},
@ -50,7 +50,7 @@
{
"builtin": "stairs",
"override": {
"minzoom": 19
"minzoom": 18
}
},
{
@ -114,7 +114,7 @@
]
},
"presets": null,
"minzoom": 19
"minzoom": 18
}
},
{
@ -127,7 +127,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/bicycle_parking.svg"
@ -145,7 +145,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/rental_bicycle.svg"
@ -163,7 +163,7 @@
]
},
"presets": null,
"minzoom": 19
"minzoom": 18
}
},
{
@ -179,7 +179,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering+": [
{
"color": "#00f",
@ -198,7 +198,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering+": [
{
"color": "yellow",
@ -219,13 +219,13 @@
"clock"
],
"override": {
"minzoom": 19
"minzoom": 18
}
},
{
"builtin": "bench",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "./assets/themes/stations/bench.svg"
@ -236,7 +236,7 @@
{
"builtin": "drinking_water",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/drinking_water.svg"
@ -277,7 +277,7 @@
"zh_Hant": "時刻表"
}
},
"minzoom": 19,
"minzoom": 18,
"source": {
"osmTags": {
"and": [

View file

@ -220,7 +220,7 @@
]
}
},
"minzoom": 19,
"minzoom": 18,
"title": {
"render": {
"en": "Street",

View file

@ -36,22 +36,19 @@
{
"builtin": "bike_parking",
"override": {
"minzoom": 19,
"minzoomVisible": 19
"minzoom": 18
}
},
{
"builtin": "parking",
"override": {
"minzoom": 19,
"minzoomVisible": 19
"minzoom": 18
}
},
{
"builtin": "shelter",
"override": {
"minzoom": 19,
"minzoomVisible": 19,
"minzoom": 18,
"source": {
"osmTags": {
"and": [

View file

@ -124,7 +124,7 @@
"pleaseLogin": "Please log in to add a new feature",
"presetInfo": "The new POI will have {tags}",
"stillLoading": "The data is still loading. Please wait a bit before you add a new feature.",
"title": "Add a new feature?",
"title": "Add a new feature",
"warnVisibleForEveryone": "Your addition will be visible for everyone",
"wrongType": "This feature is not a node or a way and can not be imported",
"zoomInFurther": "Zoom in further to add a feature.",

View file

@ -124,7 +124,7 @@
"pleaseLogin": "Gelieve je aan te melden om een object toe te voegen",
"presetInfo": "Het nieuwe object krijgt de attributen {tags}",
"stillLoading": "De data worden nog geladen. Nog even geduld en dan kan je een object toevoegen.",
"title": "Nieuw object toevoegen?",
"title": "Nieuw object toevoegen",
"warnVisibleForEveryone": "Je toevoeging is voor iedereen zichtbaar",
"wrongType": "Dit object is geen punt of lijn en kan daarom niet geïmporteerd worden",
"zoomInFurther": "Gelieve verder in te zoomen om een object toe te voegen.",

View file

@ -37,7 +37,7 @@
},
"scripts": {
"start": "npm run generate:layeroverview && npm run strt",
"strt": "vite --host",
"strt": "vite --host | sed 's/localhost:/127.0.0.1:/g'",
"strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html assets/templates/*.svg assets/templates/fonts/*.ttf",
"watch:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css --watch",
"generate:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css",

View file

@ -8,8 +8,7 @@ rm -rf dist/*
rm -rf .cache
mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null
mkdir dist/assets/langs 2> /dev/null
mkdir dist/assets/langs/layers 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192"
@ -48,11 +47,12 @@ fi
export NODE_OPTIONS=--max-old-space-size=7000
vite build $SRC_MAPS
# Copy the layer files, as these might contain assets (e.g. svgs)
cp -r assets/layers/ dist/assets/layers/
cp -r assets/themes/ dist/assets/themes/
cp -r assets/svg/ dist/assets/svg/
cp -r langs/layers/ dist/assets/langs/layers/
mkdir dist/assets/langs
mkdir dist/assets/langs/layers
cp -r langs/layers/ dist/assets/langs/
ls dist/assets/langs/layers/
export NODE_OPTIONS=""

View file

@ -1,24 +1,21 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs"
import Locale from "../src/UI/i18n/Locale"
import Translations from "../src/UI/i18n/Translations"
import { Translation } from "../src/UI/i18n/Translation"
import all_known_layouts from "../src/assets/generated/known_themes.json"
import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson"
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
import xml2js from "xml2js"
import ScriptUtils from "./ScriptUtils"
import { Utils } from "../src/Utils"
import SpecialVisualizations from "../src/UI/SpecialVisualizations"
import Constants from "../src/Models/Constants"
import {
AvailableRasterLayers,
EditorLayerIndexProperties,
RasterLayerPolygon,
} from "../src/Models/RasterLayers"
import { ImmutableStore } from "../src/Logic/UIEventSource"
import * as crypto from "crypto"
import * as eli from "../src/assets/editor-layer-index.json"
import dom from "svelte/types/compiler/compile/render_dom"
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs";
import Locale from "../src/UI/i18n/Locale";
import Translations from "../src/UI/i18n/Translations";
import { Translation } from "../src/UI/i18n/Translation";
import all_known_layouts from "../src/assets/generated/known_themes.json";
import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson";
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig";
import xml2js from "xml2js";
import ScriptUtils from "./ScriptUtils";
import { Utils } from "../src/Utils";
import SpecialVisualizations from "../src/UI/SpecialVisualizations";
import Constants from "../src/Models/Constants";
import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers";
import { ImmutableStore } from "../src/Logic/UIEventSource";
import * as crypto from "crypto";
import * as eli from "../src/assets/editor-layer-index.json";
import * as eli_global from "../src/assets/global-raster-layers.json";
const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8")
const codeTemplate = readFileSync("src/index_theme.ts.template", "utf8")
@ -219,7 +216,8 @@ function eliUrls(): string[] {
}
const urls: string[] = []
const regex = /{switch:([^}]+)}/
for (const feature of eli.features) {
const rasterLayers = [...AvailableRasterLayers.vectorLayers, ...eli.features, ...eli_global.layers.map(properties => ({properties})) ]
for (const feature of rasterLayers) {
const url = (<RasterLayerPolygon>feature).properties.url
const match = url.match(regex)
if (match) {
@ -245,8 +243,6 @@ function generateCsp(
...Constants.defaultOverpassUrls,
Constants.countryCoderEndpoint,
Constants.nominatimEndpoint,
AvailableRasterLayers.maptilerCarto.properties.url,
AvailableRasterLayers.maptilerDefaultLayer.properties.url,
"https://api.openstreetmap.org",
"https://pietervdvn.goatcounter.com",
]

View file

@ -28,4 +28,3 @@ studio.mapcomplete.org {
to http://127.0.0.1:1235
}
}

View file

@ -5,6 +5,7 @@ import { Feature, Point } from "geojson"
import { TagUtils } from "../../Tags/TagUtils"
import BaseUIElement from "../../../UI/BaseUIElement"
import { Utils } from "../../../Utils"
import { OsmTags } from "../../../Models/OsmFeature"
/**
* Highly specialized feature source.
@ -12,8 +13,14 @@ import { Utils } from "../../../Utils"
*/
export class LastClickFeatureSource implements WritableFeatureSource {
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
public readonly hasNoteLayer: boolean
public readonly renderings: string[]
public readonly hasPresets: boolean
private i: number = 0
constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) {
this.hasNoteLayer = layout.layers.some((l) => l.id === "note")
this.hasPresets = layout.layers.some((l) => l.presets?.length > 0)
const allPresets: BaseUIElement[] = []
for (const layer of layout.layers)
for (let i = 0; i < (layer.presets ?? []).length; i++) {
@ -26,35 +33,36 @@ export class LastClickFeatureSource implements WritableFeatureSource {
allPresets.push(html)
}
const renderings = Utils.Dedup(
this.renderings = Utils.Dedup(
allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
)
)
let i = 0
location.addCallbackAndRunD(({ lon, lat }) => {
const properties = {
lastclick: "yes",
id: "last_click_" + i,
has_note_layer: layout.layers.some((l) => l.id === "note") ? "yes" : "no",
has_presets: layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no",
renderings: renderings.join(""),
number_of_presets: "" + renderings.length,
first_preset: renderings[0],
}
i++
const point = <Feature<Point>>{
type: "Feature",
properties,
geometry: {
type: "Point",
coordinates: [lon, lat],
},
}
this.features.setData([point])
this.features.setData([this.createFeature(lon, lat)])
})
}
public createFeature(lon: number, lat: number): Feature<Point, OsmTags> {
const properties: OsmTags = {
lastclick: "yes",
id: "last_click_" + this.i,
has_note_layer: this.hasNoteLayer ? "yes" : "no",
has_presets: this.hasPresets ? "yes" : "no",
renderings: this.renderings.join(""),
number_of_presets: "" + this.renderings.length,
first_preset: this.renderings[0],
}
this.i++
return <Feature<Point, OsmTags>>{
type: "Feature",
properties,
geometry: {
type: "Point",
coordinates: [lon, lat],
},
}
}
}

View file

@ -99,7 +99,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
)
this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch(
"fs-community-index",
true,
this.featureSwitchEnableLogin.data,
"Disables/enables the button to get in touch with the community"
)
this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch(

View file

@ -175,7 +175,6 @@ export default class ThemeViewStateHashActor {
}
private back() {
console.log("Got a back event")
const state = this._state
// history.pushState(null, null, window.location.pathname);
if (state.selectedElement.data) {

View file

@ -58,7 +58,7 @@ export default class Constants {
importHelperUnlock: 5000,
}
static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 18 : 19
static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 17 : 18
/**
* Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes.
* (Note that pendingChanges might upload sooner if the popup is closed or similar)

View file

@ -1,172 +1,174 @@
import { Feature, Polygon } from "geojson"
import * as editorlayerindex from "../assets/editor-layer-index.json"
import * as globallayers from "../assets/global-raster-layers.json"
import { BBox } from "../Logic/BBox"
import { Store, Stores } from "../Logic/UIEventSource"
import { GeoOperations } from "../Logic/GeoOperations"
import { RasterLayerProperties } from "./RasterLayerProperties"
import { Feature, Polygon } from "geojson";
import * as editorlayerindex from "../assets/editor-layer-index.json";
import * as globallayers from "../assets/global-raster-layers.json";
import { BBox } from "../Logic/BBox";
import { Store, Stores } from "../Logic/UIEventSource";
import { GeoOperations } from "../Logic/GeoOperations";
import { RasterLayerProperties } from "./RasterLayerProperties";
export class AvailableRasterLayers {
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
RasterLayerPolygon)[] = <any>editorlayerindex.features
public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
(properties) =>
<RasterLayerPolygon>{
type: "Feature",
properties,
geometry: BBox.global.asGeometry(),
}
)
public static readonly osmCartoProperties: RasterLayerProperties = {
id: "osm",
name: "OpenStreetMap",
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright",
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap",
}
public static readonly osmCarto: RasterLayerPolygon = {
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
RasterLayerPolygon)[] = <any>editorlayerindex.features;
public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
(properties) =>
<RasterLayerPolygon>{
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry(),
}
properties,
geometry: BBox.global.asGeometry()
}
);
public static readonly osmCartoProperties: RasterLayerProperties = {
id: "osm",
name: "OpenStreetMap",
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright"
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap"
};
public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler",
url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/",
},
},
geometry: BBox.global.asGeometry(),
}
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry()
};
public static readonly maptilerCarto: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Carto",
url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.carto",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/",
},
},
geometry: BBox.global.asGeometry(),
}
public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler",
url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/"
}
},
geometry: BBox.global.asGeometry()
};
public static readonly maptilerBackdrop: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Backdrop",
url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.backdrop",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/",
},
},
geometry: BBox.global.asGeometry(),
}
public static readonly americana: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "Americana",
url: "https://zelonewolf.github.io/openstreetmap-americana/style.json",
category: "osmbasedmap",
id: "americana",
type: "vector",
attribution: {
text: "Americana",
url: "https://github.com/ZeLonewolf/openstreetmap-americana/",
},
},
geometry: BBox.global.asGeometry(),
}
public static readonly maptilerCarto: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Carto",
url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.carto",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/"
}
},
geometry: BBox.global.asGeometry()
};
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>
): Store<RasterLayerPolygon[]> {
const availableLayersBboxes = Stores.ListStabilized(
location.mapD((loc) => {
const lonlat: [number, number] = [loc.lon, loc.lat]
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
BBox.get(eliPolygon).contains(lonlat)
)
})
)
const available = Stores.ListStabilized(
availableLayersBboxes.map((eliPolygons) => {
const loc = location.data
const lonlat: [number, number] = [loc.lon, loc.lat]
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) {
return true // global ELI-layer
}
return GeoOperations.inside(lonlat, eliPolygon)
})
matching.push(...AvailableRasterLayers.globalLayers)
matching.unshift(
AvailableRasterLayers.maptilerDefaultLayer,
AvailableRasterLayers.osmCarto,
AvailableRasterLayers.maptilerCarto,
AvailableRasterLayers.maptilerBackdrop,
AvailableRasterLayers.americana
)
return matching
})
)
return available
}
public static readonly maptilerBackdrop: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Backdrop",
url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.backdrop",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/"
}
},
geometry: BBox.global.asGeometry()
};
public static readonly americana: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "Americana",
url: "https://zelonewolf.github.io/openstreetmap-americana/style.json",
category: "osmbasedmap",
id: "americana",
type: "vector",
attribution: {
text: "Americana",
url: "https://github.com/ZeLonewolf/openstreetmap-americana/"
}
},
geometry: BBox.global.asGeometry()
};
public static readonly vectorLayers = [
AvailableRasterLayers.maptilerDefaultLayer,
AvailableRasterLayers.osmCarto,
AvailableRasterLayers.maptilerCarto,
AvailableRasterLayers.maptilerBackdrop,
AvailableRasterLayers.americana
];
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>
): Store<RasterLayerPolygon[]> {
const availableLayersBboxes = Stores.ListStabilized(
location.mapD((loc) => {
const lonlat: [number, number] = [loc.lon, loc.lat];
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
BBox.get(eliPolygon).contains(lonlat)
);
})
);
const available = Stores.ListStabilized(
availableLayersBboxes.map((eliPolygons) => {
const loc = location.data;
const lonlat: [number, number] = [loc.lon, loc.lat];
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) {
return true; // global ELI-layer
}
return GeoOperations.inside(lonlat, eliPolygon);
});
matching.push(...AvailableRasterLayers.globalLayers);
matching.unshift(...AvailableRasterLayers.vectorLayers);
return matching;
})
);
return available;
}
}
export class RasterLayerUtils {
/**
* Selects, from the given list of available rasterLayerPolygons, a rasterLayer.
* This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available).
* Returns 'undefined' if no such layer is available
* @param available
* @param preferredCategory
* @param ignoreLayer
*/
public static SelectBestLayerAccordingTo(
available: RasterLayerPolygon[],
preferredCategory: string,
ignoreLayer?: RasterLayerPolygon
): RasterLayerPolygon {
let secondBest: RasterLayerPolygon = undefined
for (const rasterLayer of available) {
if (rasterLayer === ignoreLayer) {
continue
}
const p = rasterLayer.properties
if (p.category === preferredCategory) {
if (p.best) {
return rasterLayer
}
if (!secondBest) {
secondBest = rasterLayer
}
}
/**
* Selects, from the given list of available rasterLayerPolygons, a rasterLayer.
* This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available).
* Returns 'undefined' if no such layer is available
* @param available
* @param preferredCategory
* @param ignoreLayer
*/
public static SelectBestLayerAccordingTo(
available: RasterLayerPolygon[],
preferredCategory: string,
ignoreLayer?: RasterLayerPolygon
): RasterLayerPolygon {
let secondBest: RasterLayerPolygon = undefined;
for (const rasterLayer of available) {
if (rasterLayer === ignoreLayer) {
continue;
}
const p = rasterLayer.properties;
if (p.category === preferredCategory) {
if (p.best) {
return rasterLayer;
}
return secondBest
if (!secondBest) {
secondBest = rasterLayer;
}
}
}
return secondBest;
}
}
export type RasterLayerPolygon = Feature<Polygon, RasterLayerProperties>
@ -178,165 +180,165 @@ export type RasterLayerPolygon = Feature<Polygon, RasterLayerProperties>
* which was then converted with http://borischerny.com/json-schema-to-typescript-browser/
*/
export interface EditorLayerIndexProperties extends RasterLayerProperties {
/**
* The name of the imagery source
*/
readonly name: string;
/**
* Whether the imagery name should be translated
*/
readonly i18n?: boolean;
readonly type:
| "tms"
| "wms"
| "bing"
| "scanex"
| "wms_endpoint"
| "wmts"
| "vector"; /* Vector is not actually part of the ELI-spec, we add it for vector layers */
/**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/
readonly category?:
| "photo"
| "map"
| "historicmap"
| "osmbasedmap"
| "historicphoto"
| "qa"
| "elevation"
| "other";
/**
* A URL template for imagery tiles
*/
readonly url: string;
readonly min_zoom?: number;
readonly max_zoom?: number;
/**
* explicit/implicit permission by the owner for use in OSM
*/
readonly permission_osm?: "explicit" | "implicit" | "no";
/**
* A URL for the license or permissions for the imagery
*/
readonly license_url?: string;
/**
* A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
*/
readonly privacy_policy_url?: string | boolean;
/**
* A unique identifier for the source; used in imagery_used changeset tag
*/
readonly id: string;
/**
* A short English-language description of the source
*/
readonly description?: string;
/**
* The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
*/
readonly country_code?: string;
/**
* Whether this imagery should be shown in the default world-wide menu
*/
readonly default?: boolean;
/**
* Whether this imagery is the best source for the region
*/
readonly best?: boolean;
/**
* The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly start_date?: string;
/**
* The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly end_date?: string;
/**
* HTTP header to check for information if the tile is invalid
*/
readonly no_tile_header?: {
/**
* The name of the imagery source
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "^.*$".
*/
readonly name: string
/**
* Whether the imagery name should be translated
*/
readonly i18n?: boolean
readonly type:
| "tms"
| "wms"
| "bing"
| "scanex"
| "wms_endpoint"
| "wmts"
| "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */
/**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/
readonly category?:
| "photo"
| "map"
| "historicmap"
| "osmbasedmap"
| "historicphoto"
| "qa"
| "elevation"
| "other"
/**
* A URL template for imagery tiles
*/
readonly url: string
readonly min_zoom?: number
readonly max_zoom?: number
/**
* explicit/implicit permission by the owner for use in OSM
*/
readonly permission_osm?: "explicit" | "implicit" | "no"
/**
* A URL for the license or permissions for the imagery
*/
readonly license_url?: string
/**
* A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
*/
readonly privacy_policy_url?: string | boolean
/**
* A unique identifier for the source; used in imagery_used changeset tag
*/
readonly id: string
/**
* A short English-language description of the source
*/
readonly description?: string
/**
* The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
*/
readonly country_code?: string
/**
* Whether this imagery should be shown in the default world-wide menu
*/
readonly default?: boolean
/**
* Whether this imagery is the best source for the region
*/
readonly best?: boolean
/**
* The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly start_date?: string
/**
* The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly end_date?: string
/**
* HTTP header to check for information if the tile is invalid
*/
readonly no_tile_header?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "^.*$".
*/
[k: string]: string[] | null
[k: string]: string[] | null
};
/**
* 'true' if tiles are transparent and can be overlaid on another source
*/
readonly overlay?: boolean & string;
readonly available_projections?: string[];
readonly attribution?: {
readonly url?: string
readonly text?: string
readonly html?: string
readonly required?: boolean
};
/**
* A URL for an image, that can be displayed in the list of imagery layers next to the name
*/
readonly icon?: string;
/**
* A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
*/
readonly eula?: string;
/**
* A URL for an image, that is displayed in the mapview for attribution
*/
readonly "logo-image"?: string;
/**
* Customized text for the terms of use link (default is "Background Terms of Use")
*/
readonly "terms-of-use-text"?: string;
/**
* Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
*/
readonly "no-tile-checksum"?: string;
/**
* header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
*/
readonly "metadata-header"?: string;
/**
* Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
*/
readonly "valid-georeference"?: boolean;
/**
* Size of individual tiles delivered by a TMS service
*/
readonly "tile-size"?: number;
/**
* Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
*/
readonly "mod-tile-features"?: string;
/**
* HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
*/
readonly "custom-http-headers"?: {
readonly "header-name"?: string
readonly "header-value"?: string
};
/**
* Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
*/
readonly "default-layers"?: {
layer?: {
"layer-name"?: string
"layer-style"?: string
[k: string]: unknown
}
/**
* 'true' if tiles are transparent and can be overlaid on another source
*/
readonly overlay?: boolean & string
readonly available_projections?: string[]
readonly attribution?: {
readonly url?: string
readonly text?: string
readonly html?: string
readonly required?: boolean
}
/**
* A URL for an image, that can be displayed in the list of imagery layers next to the name
*/
readonly icon?: string
/**
* A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
*/
readonly eula?: string
/**
* A URL for an image, that is displayed in the mapview for attribution
*/
readonly "logo-image"?: string
/**
* Customized text for the terms of use link (default is "Background Terms of Use")
*/
readonly "terms-of-use-text"?: string
/**
* Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
*/
readonly "no-tile-checksum"?: string
/**
* header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
*/
readonly "metadata-header"?: string
/**
* Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
*/
readonly "valid-georeference"?: boolean
/**
* Size of individual tiles delivered by a TMS service
*/
readonly "tile-size"?: number
/**
* Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
*/
readonly "mod-tile-features"?: string
/**
* HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
*/
readonly "custom-http-headers"?: {
readonly "header-name"?: string
readonly "header-value"?: string
}
/**
* Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
*/
readonly "default-layers"?: {
layer?: {
"layer-name"?: string
"layer-style"?: string
[k: string]: unknown
}
[k: string]: unknown
}[]
/**
* format to use when connecting tile server (when using WMS_ENDPOINT type)
*/
readonly format?: string
/**
* If `true` transparent tiles will be requested from WMS server
*/
readonly transparent?: boolean & string
/**
* minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
*/
readonly "minimum-tile-expire"?: number
[k: string]: unknown
}[];
/**
* format to use when connecting tile server (when using WMS_ENDPOINT type)
*/
readonly format?: string;
/**
* If `true` transparent tiles will be requested from WMS server
*/
readonly transparent?: boolean & string;
/**
* minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
*/
readonly "minimum-tile-expire"?: number;
}

View file

@ -95,6 +95,9 @@ export default class LayoutConfig implements LayoutInformation {
}
const context = this.id
this.credits = json.credits
if(!json.title){
throw `The theme ${json.id} does not have a title defined.`
}
this.language = json.mustHaveLanguage ?? Object.keys(json.title)
this.usedImages = Array.from(
new ExtractImages(official, undefined)

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,28 @@
let id = Math.random() * 1000000000 + ""
</script>
<form>
<form on:change|preventDefault={() => {
drawAttention = false
dispatcher("submit", inputElement.files)
}}
on:dragend={() => {
console.log("Drag end")
drawAttention = false
}}
on:dragenter|preventDefault|stopPropagation={(e) => {
console.log("Dragging enter")
drawAttention = true
e.dataTransfer.drop = "copy"
}}
on:dragstart={() => {
console.log("DragStart")
drawAttention = false
}}
on:drop|preventDefault|stopPropagation={(e) => {
console.log("Got a 'drop'")
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
}}>
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput" + id}>
<slot />
</label>
@ -23,26 +44,7 @@
id={"fileinput" + id}
{multiple}
name="file-input"
on:change|preventDefault={() => {
drawAttention = false
dispatcher("submit", inputElement.files)
}}
on:dragend={() => {
drawAttention = false
}}
on:dragover|preventDefault|stopPropagation={(e) => {
console.log("Dragging over!")
drawAttention = true
e.dataTransfer.drop = "copy"
}}
on:dragstart={() => {
drawAttention = false
}}
on:drop|preventDefault|stopPropagation={(e) => {
console.log("Got a 'drop'")
drawAttention = false
dispatcher("submit", e.dataTransfer.files)
}}
type="file"
/>
</form>

View file

@ -11,8 +11,9 @@
<div
class="absolute top-0 right-0 h-screen w-screen p-4 md:p-6"
style="background-color: #00000088"
on:click={() => {dispatch("close")}}
>
<div class="content normal-background">
<div class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
<slot />
</div>

View file

@ -22,7 +22,14 @@ export default class Hotkeys {
}[]
>([])
private static textElementSelected(): boolean {
private static textElementSelected(event: KeyboardEvent): boolean {
if(event.ctrlKey || event.altKey){
// This is an event with a modifier-key, lets not ignore it
return false
}
if(event.key === "Escape"){
return false // Another not-printable character that should not be ignored
}
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
}
public static RegisterHotkey(
@ -68,7 +75,7 @@ export default class Hotkeys {
})
} else if (key["shift"] !== undefined) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected()) {
if (Hotkeys.textElementSelected(event)) {
// A text element is selected, we don't do anything special
return
}
@ -86,7 +93,7 @@ export default class Hotkeys {
})
} else if (key["nomod"] !== undefined) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected()) {
if (Hotkeys.textElementSelected(event)) {
// A text element is selected, we don't do anything special
return
}

View file

@ -1,20 +1,32 @@
<script lang="ts">
/**
* Thin wrapper around 'TabGroup' which binds the state
*/
/**
* Thin wrapper around 'TabGroup' which binds the state
*/
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"
import { UIEventSource } from "../../Logic/UIEventSource"
import { twJoin } from "tailwind-merge"
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
import { twJoin } from "tailwind-merge";
export let tab: UIEventSource<number>
let tabElements: HTMLElement[] = []
$: tabElements[$tab]?.click()
$: {
if (tabElements[tab.data]) {
window.setTimeout(() => tabElements[tab.data].click(), 50)
/**
* If a condition is given for a certain tab, it will only be shown if this condition is true.
* E.g.
* condition3 = new ImmutableStore(false) will always hide tab3 (the fourth tab)
*/
let tr = new ImmutableStore(true)
export let condition0: Store<boolean> = tr
export let condition1: Store<boolean> = tr
export let condition2: Store<boolean> = tr
export let condition3: Store<boolean> = tr
export let condition4: Store<boolean> = tr
export let tab: UIEventSource<number>;
let tabElements: HTMLElement[] = [];
$: tabElements[$tab]?.click();
$: {
if (tabElements[tab.data]) {
window.setTimeout(() => tabElements[tab.data].click(), 50);
}
}
}
</script>
<div class="tabbedgroup flex h-full w-full">
@ -29,41 +41,31 @@
>
<div class="interactive sticky top-0 flex items-center justify-between">
<TabList class="flex flex-wrap">
{#if $$slots.title1}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<div bind:this={tabElements[0]} class="flex">
<slot name="title0">Tab 0</slot>
</div>
</Tab>
{/if}
{#if $$slots.title1}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<div bind:this={tabElements[1]} class="flex">
<slot name="title1" />
</div>
</Tab>
{/if}
{#if $$slots.title2}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<div bind:this={tabElements[2]} class="flex">
<slot name="title2" />
</div>
</Tab>
{/if}
{#if $$slots.title3}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<div bind:this={tabElements[3]} class="flex">
<slot name="title3" />
</div>
</Tab>
{/if}
{#if $$slots.title4}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<div bind:this={tabElements[4]} class="flex">
<slot name="title4" />
</div>
</Tab>
{/if}
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition0 && "hidden")}>
<div bind:this={tabElements[0]} class="flex">
<slot name="title0">Tab 0</slot>
</div>
</Tab>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition1 && "hidden")}>
<div bind:this={tabElements[1]} class="flex">
<slot name="title1" />
</div>
</Tab>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition2 && "hidden")}>
<div bind:this={tabElements[2]} class="flex">
<slot name="title2" />
</div>
</Tab>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition3 && "hidden")}>
<div bind:this={tabElements[3]} class="flex">
<slot name="title3" />
</div>
</Tab>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition4 && "hidden")}>
<div bind:this={tabElements[4]} class="flex">
<slot name="title4" />
</div>
</Tab>
</TabList>
<slot name="post-tablist" />
</div>
@ -75,16 +77,24 @@
</slot>
</TabPanel>
<TabPanel class="tabpanel">
<slot name="content1" />
<slot name="content1">
<div />
</slot>
</TabPanel>
<TabPanel class="tabpanel">
<slot name="content2" />
<slot name="content2">
<div />
</slot>
</TabPanel>
<TabPanel class="tabpanel">
<slot name="content3" />
<slot name="content3">
<div />
</slot>
</TabPanel>
<TabPanel class="tabpanel">
<slot name="content4" />
<slot name="content4">
<div />
</slot>
</TabPanel>
</TabPanels>
</div>
@ -92,44 +102,44 @@
</div>
<style>
.tabbedgroup {
max-height: 100vh;
height: 100%;
}
.tabbedgroup {
max-height: 100vh;
height: 100%;
}
:global(.tabpanel) {
height: 100%;
}
:global(.tabpanel) {
height: 100%;
}
:global(.tabpanels) {
height: calc(100% - 2rem);
}
:global(.tabpanels) {
height: calc(100% - 2rem);
}
:global(.tab) {
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 1rem;
}
:global(.tab) {
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 1rem;
}
:global(.tab .flex) {
align-items: center;
gap: 0.25rem;
}
:global(.tab .flex) {
align-items: center;
gap: 0.25rem;
}
:global(.tab span|div) {
align-items: center;
gap: 0.25rem;
display: flex;
}
:global(.tab span|div) {
align-items: center;
gap: 0.25rem;
display: flex;
}
:global(.tab-selected svg) {
fill: var(--catch-detail-color-contrast);
}
:global(.tab-selected svg) {
fill: var(--catch-detail-color-contrast);
}
:global(.tab-unselected) {
background-color: var(--background-color) !important;
color: var(--foreground-color) !important;
}
:global(.tab-unselected) {
background-color: var(--background-color) !important;
color: var(--foreground-color) !important;
}
</style>

View file

@ -1,47 +1,48 @@
<script lang="ts">
import Translations from "../i18n/Translations"
import Svg from "../../Svg"
import Tr from "../Base/Tr.svelte"
import NextButton from "../Base/NextButton.svelte"
import Geosearch from "./Geosearch.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twJoin } from "tailwind-merge"
import { Utils } from "../../Utils"
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import Tr from "../Base/Tr.svelte";
import NextButton from "../Base/NextButton.svelte";
import Geosearch from "./Geosearch.svelte";
import ToSvelte from "../Base/ToSvelte.svelte";
import ThemeViewState from "../../Models/ThemeViewState";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
import { twJoin } from "tailwind-merge";
import { Utils } from "../../Utils";
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
import If from "../Base/If.svelte";
/**
* The theme introduction panel
*/
export let state: ThemeViewState
let layout = state.layout
let selectedElement = state.selectedElement
let selectedLayer = state.selectedLayer
/**
* The theme introduction panel
*/
export let state: ThemeViewState;
let layout = state.layout;
let selectedElement = state.selectedElement;
let selectedLayer = state.selectedLayer;
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
let searchEnabled = false;
let geopermission: Store<GeolocationPermissionState> =
state.geolocation.geolocationState.permission
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
let geopermission: Store<GeolocationPermissionState> =
state.geolocation.geolocationState.permission;
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
geopermission.addCallback((perm) => console.log(">>>> Permission", perm))
geopermission.addCallback((perm) => console.log(">>>> Permission", perm));
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
state.guistate.themeIsOpened.setData(false)
const coor = { lon: c.longitude, lat: c.latitude }
state.mapProperties.location.setData(coor)
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState;
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
state.guistate.themeIsOpened.setData(false);
const coor = { lon: c.longitude, lat: c.latitude };
state.mapProperties.location.setData(coor);
}
if (glstate.permission.data !== "granted") {
glstate.requestPermission();
return;
}
}
if (glstate.permission.data !== "granted") {
glstate.requestPermission()
return
}
}
</script>
<div class="flex h-full flex-col justify-between">
@ -62,61 +63,67 @@
</div>
</NextButton>
<div class="flex w-full flex-wrap sm:flex-nowrap">
{#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
</button>
<!-- No geolocation granted - we don't show the button -->
{:else if $geopermission === "requested"}
<button class="disabled flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<ToSvelte
construct={Svg.crosshair_svg()
.SetClass("w-8 h-8")
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
/>
<Tr t={Translations.t.general.waitingForGeopermission} />
</button>
{:else if $geopermission === "denied"}
<button class="disabled flex w-full items-center gap-x-2">
<ToSvelte construct={Svg.location_refused_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.geopermissionDenied} />
</button>
{:else}
<button class="disabled flex w-full items-center gap-x-2">
<ToSvelte
construct={Svg.crosshair_svg()
.SetClass("w-8 h-8")
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
/>
<Tr t={Translations.t.general.waitingForLocation} />
</button>
{/if}
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
<div class="w-full">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
on:searchIsValid={(isValid) => {
<div class="flex w-full flex-wrap sm:flex-nowrap">
<If condition={state.featureSwitches.featureSwitchGeolocation}>
{#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
</button>
<!-- No geolocation granted - we don't show the button -->
{:else if $geopermission === "requested"}
<button class="disabled flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<ToSvelte
construct={Svg.crosshair_svg()
.SetClass("w-8 h-8")
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
/>
<Tr t={Translations.t.general.waitingForGeopermission} />
</button>
{:else if $geopermission === "denied"}
<button class="disabled flex w-full items-center gap-x-2">
<ToSvelte construct={Svg.location_refused_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.geopermissionDenied} />
</button>
{:else}
<button class="disabled flex w-full items-center gap-x-2">
<ToSvelte
construct={Svg.crosshair_svg()
.SetClass("w-8 h-8")
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
/>
<Tr t={Translations.t.general.waitingForLocation} />
</button>
{/if}
</If>
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
<div class="w-full">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
on:searchIsValid={(isValid) => {
searchEnabled = isValid
}}
perLayer={state.perLayer}
{selectedElement}
{selectedLayer}
{triggerSearch}
/>
perLayer={state.perLayer}
{selectedElement}
{selectedLayer}
{triggerSearch}
/>
</div>
<button
class={twJoin("flex items-center justify-between gap-x-2", !searchEnabled && "disabled")}
on:click={() => triggerSearch.ping()}
>
<Tr t={Translations.t.general.search.searchShort} />
<SearchIcon class="h-6 w-6" />
</button>
</div>
<button
class={twJoin("flex items-center justify-between gap-x-2", !searchEnabled && "disabled")}
on:click={() => triggerSearch.ping()}
>
<Tr t={Translations.t.general.search.searchShort} />
<SearchIcon class="h-6 w-6" />
</button>
</div>
</If>
</div>
</div>

View file

@ -1,14 +1,14 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Map as MLMap } from "maplibre-gl"
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "./MaplibreMap.svelte"
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
import * as htmltoimage from "html-to-image"
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import type { Map as MLMap } from "maplibre-gl";
import { Map as MlMap, SourceSpecification } from "maplibre-gl";
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
import { Utils } from "../../Utils";
import { BBox } from "../../Logic/BBox";
import { ExportableMap, MapProperties } from "../../Models/MapProperties";
import SvelteUIElement from "../Base/SvelteUIElement";
import MaplibreMap from "./MaplibreMap.svelte";
import { RasterLayerProperties } from "../../Models/RasterLayerProperties";
import * as htmltoimage from "html-to-image";
/**
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`

View file

@ -16,6 +16,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { CLIENT_RENEG_LIMIT } from "tls";
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
@ -406,13 +407,10 @@ class LineRenderingLayer {
} else {
const tags = this._fetchStore(id)
this._listenerInstalledOn.add(id)
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties)
)
tags.addCallbackD((properties) => {
if (!map.getLayer(this._layername)) {
return
tags.addCallbackAndRunD((properties) => {
// Make sure to use 'getSource' here, the layer names are different!
if(map.getSource(this._layername) === undefined){
return true
}
map.setFeatureState(
{ source: this._layername, id },

View file

@ -92,7 +92,7 @@
<LoginToggle ignoreLoading={true} {state}>
{#if currentState === "start"}
<button
class="flex"
class="flex items-center"
on:click={() => {
currentState = "confirm"
}}
@ -112,7 +112,7 @@
<button
slot="save-button"
on:click={onDelete}
class={twJoin(selectedTags === undefined && "disabled", "primary flex bg-red-600")}
class={twJoin(selectedTags === undefined && "disabled", "primary flex bg-red-600 items-center")}
>
<TrashIcon
class={twJoin(
@ -122,7 +122,7 @@
/>
<Tr t={t.delete} />
</button>
<button slot="cancel" on:click={() => (currentState = "start")}>
<button slot="cancel" class="items-center" on:click={() => (currentState = "start")}>
<Tr t={t.cancel} />
</button>
<XCircleIcon

View file

@ -1,114 +1,114 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import Svg from "../Svg"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import { OpenJosm } from "./BigComponents/OpenJosm"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import LanguagePicker from "./LanguagePicker"
import Locale from "./i18n/Locale"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import Svg from "../Svg";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import { OpenJosm } from "./BigComponents/OpenJosm";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import LanguagePicker from "./LanguagePicker";
import Locale from "./i18n/Locale";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
export let state: ThemeViewState
let layout = state.layout
export let state: ThemeViewState;
let layout = state.layout;
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
let maplibremap: UIEventSource<MlMap> = state.map;
let selectedElement: UIEventSource<Feature> = state.selectedElement;
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer;
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined
}
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
if (selectedElement === undefined || layer === undefined) {
return undefined;
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined;
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags })
},
[selectedLayer]
)
const tags = state.featureProperties.getStore(selectedElement.properties.id);
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags });
},
[selectedLayer]
);
const selectedElementTitle = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined
}
const selectedElementTitle = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
if (selectedElement === undefined || layer === undefined) {
return undefined;
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
},
[selectedLayer]
)
const tags = state.featureProperties.getStore(selectedElement.properties.id);
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags });
},
[selectedLayer]
);
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
)
let mapproperties: MapProperties = state.mapProperties;
let featureSwitches: FeatureSwitchState = state.featureSwitches;
let availableLayers = state.availableLayers;
let userdetails = state.osmConnection.userDetails;
let currentViewLayer = layout.layers.find((l) => l.id === "current_view");
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer;
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name;
})
);
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -168,18 +168,39 @@
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
<!-- bottom controls -->
<div class="flex w-full items-end justify-between px-4">
<div class="flex">
<!-- bottom left elements -->
<OpenBackgroundSelectorButton hideTooltip={true} {state} />
<a
class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100"
on:click={() => {
<div class="flex flex-col">
<If condition={featureSwitches.featureSwitchEnableLogin}>
{#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}
<button class="w-fit pointer-events-auto" on:click={() => {state.openNewDialog()}}>
{#if state.lastClickObject.hasPresets}
<Tr t={Translations.t.general.add.title} />
{:else}
<Tr t={Translations.t.notes.addAComment} />
{/if}
</button>
{/if}
</If>
<div class="flex">
<!-- bottom left elements -->
<If condition={state.featureSwitches.featureSwitchFilter}>
<MapControlButton on:click={() => state.guistate.openFilterView()}>
<ToSvelte construct={Svg.filter_svg().SetClass("h-6 w-6")} />
</MapControlButton>
</If>
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
<OpenBackgroundSelectorButton hideTooltip={true} {state} />
</If>
<a
class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100"
on:click={() => {
state.guistate.themeViewTab.setData("copyright")
state.guistate.themeIsOpened.setData(true)
}}
>
© OpenStreetMap, <span class="w-24">{rasterLayerName}</span>
</a>
>
© OpenStreetMap, <span class="w-24">{rasterLayerName}</span>
</a>
</div>
</div>
<div class="flex flex-col items-end">
@ -255,9 +276,9 @@
<If condition={state.guistate.themeIsOpened}>
<!-- Theme menu -->
<FloatOver>
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
<span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup tab={state.guistate.themeViewTabIndex}>
<TabbedGroup condition1={state.featureSwitches.featureSwitchFilter} tab={state.guistate.themeViewTabIndex}>
<div slot="post-tablist">
<XCircleIcon
class="mr-2 h-8 w-8"
@ -275,10 +296,8 @@
</div>
<div class="flex" slot="title1">
<If condition={state.featureSwitches.featureSwitchFilter}>
<ToSvelte construct={Svg.filter_svg().SetClass("w-4 h-4")} />
<Tr t={Translations.t.general.menu.filter} />
</If>
<ToSvelte construct={Svg.filter_svg().SetClass("w-4 h-4")} />
<Tr t={Translations.t.general.menu.filter} />
</div>
<div class="m-2 flex flex-col" slot="content1">
@ -298,6 +317,7 @@
/>
{/each}
</div>
<div class="flex" slot="title2">
<If condition={state.featureSwitches.featureSwitchEnableExport}>
<ToSvelte construct={Svg.download_svg().SetClass("w-4 h-4")} />
@ -314,7 +334,7 @@
<ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" />
<div slot="title4" class="flex">
<div class="flex" slot="title4">
<ToSvelte construct={Svg.share_svg().SetClass("w-4 h-4")} />
<Tr t={Translations.t.general.sharescreen.title} />
</div>
@ -327,7 +347,7 @@
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<!-- background layer selector -->
<FloatOver on:close={() => state.guistate.backgroundLayerSelectionIsOpened.setData(false)}>
<FloatOver on:close={() => {state.guistate.backgroundLayerSelectionIsOpened.setData(false)}}>
<div class="h-full p-2">
<RasterLayerOverview
{availableLayers}
@ -342,9 +362,10 @@
<If condition={state.guistate.menuIsOpened}>
<!-- Menu page -->
<FloatOver>
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false) }>
<span slot="close-button"><!-- Hide the default close button --></span>
<TabbedGroup tab={state.guistate.menuViewTabIndex}>
<TabbedGroup condition1={featureSwitches.featureSwitchEnableLogin} condition2={state.featureSwitches. featureSwitchCommunityIndex}
tab={state.guistate.menuViewTabIndex}>
<div slot="post-tablist">
<XCircleIcon
class="mr-2 h-8 w-8"
@ -419,7 +440,6 @@
<div class="m-2" slot="content2">
<CommunityIndexView location={state.mapProperties.location} />
</div>
<div class="flex" slot="title3">
<EyeIcon class="w-6" />
<Tr t={Translations.t.privacy.title} />
@ -430,12 +450,15 @@
<Tr slot="title4" t={Translations.t.advanced.title} />
<div class="m-2 flex flex-col" slot="content4">
<OpenIdEditor mapProperties={state.mapProperties} />
<ToSvelte
construct={() =>
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<ToSvelte
construct={() =>
new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")}
/>
<MapillaryLink mapProperties={state.mapProperties} />
/>
<MapillaryLink mapProperties={state.mapProperties} />
</If>
<ToSvelte construct={Hotkeys.generateDocumentationDynamic} />
</div>
</TabbedGroup>