Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-10-14 22:41:52 +02:00
commit 2001ef09bf
50 changed files with 1443 additions and 1341 deletions

View file

@ -6,22 +6,22 @@ Hi! Thanks for checking out how to contribute to MapComplete!
There are multiple ways to contribute:
- Translating MapComplete to your own language can be done
on [this website](https://hosted.weblate.org/projects/mapcomplete/)
on [the Weblate website](https://hosted.weblate.org/projects/mapcomplete/)
- If you encounter a bug, the [issue tracker](https://github.com/pietervdvn/MapComplete/issues) is the place to be
- A good start to contribute is to create a single map layer showing features which interest you. Read more about [making your own theme](/Docs/Making_Your_Own_Theme.md).
- If you want to improve a theme, create a new theme, spot a typo in the repo... the best way is to open a pull request.
People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except . This is
People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except the branches *master* and *develop*). This is
done on a purely subjective basis, e.g. after a few pull requests and if you are a member of the OSM community.
Rights of contributors
-----------------------
If you have write access to the repository, you can make a fork of an already existing branch and push this new branch
to github. This means that this branch will be _automatically built_ and be **deployed**
to GitHub. This means that this branch will be _automatically built_ and be **deployed**
to `https://pietervdvn.github.io/mc/<branchname>`. You can see the deploy process
on [Github Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These
deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one.
on [GitHub Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These
deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no one.
Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull
requests easier.
@ -58,6 +58,6 @@ again to start fresh.
What not to contribute
----------------------
I'm currently _not_ accepting files for integration with some editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess.
Furthermore, MapComplete doesn't want to encourage or discourage some editors.
I'm currently _not_ accepting files for integration with some text editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess.
Furthermore, MapComplete doesn't want to encourage or discourage some text editors.
At last, these files are hard to maintain and are hard to detect if they have fallen out of use.

View file

@ -0,0 +1,53 @@
{
"id": "guidepost",
"name": {
"en": "Guideposts"
},
"description": {
"en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
},
"source": {
"osmTags": "information=guidepost"
},
"minzoom": 14,
"presets": [
{
"title": {
"en": "a guidepost"
},
"tags": [
"information=guidepost",
"tourism=information"
],
"description": {
"en": "A guidepost (also known as fingerpost) is often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
},
"exampleImages": [
"./assets/layers/guidepost/guidepost_example.jpg"
]
}
],
"deletion": true,
"allowMove": {
"enableImproveAccuracy": "true",
"enableRelocation": "false"
},
"title": {
"render": {
"en": "Guidepost"
}
},
"mapRendering": [
{
"location": [
"centroid",
"point"
],
"icon": "./assets/layers/guidepost/guidepost.svg",
"anchor": "bottom"
}
],
"tagRenderings": [
"images"
]
}

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
width="500"
height="500"
viewBox="0 0 500 500"
id="svg2"
sodipodi:docname="guidepost.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.4932029"
inkscape:cx="111.51597"
inkscape:cy="276.76236"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6" />
<rect
width="499.41919"
height="499.41919"
x="0"
y="0"
id="canvas"
style="visibility:hidden;fill:none;stroke:none;stroke-width:35.6728" />
<path
d="m 249.7096,8.9181999 c -13.3773,0 -26.7546,8.9182001 -26.7546,26.7546001 v 463.74639 h 53.5092 V 35.6728 c 0,-17.8364 -13.3773,-26.7546001 -26.7546,-26.7546001 z M 71.345599,35.6728 0,89.181999 71.345599,142.6912 H 214.0368 V 35.6728 Z M 285.3824,142.6912 v 107.0184 h 142.6912 l 71.34559,-53.5092 -71.34559,-53.5092 z"
id="guidepost"
style="fill:#555555;fill-opacity:1;stroke:none;stroke-width:35.6728" />
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: OSM Carto
SPDX-License-Identifier: CC0-1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Mschaeuble
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,22 @@
[
{
"path": "guidepost.svg",
"license": "CC0-1.0",
"authors": [
"OSM Carto"
],
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Guidepost-14.svg"
]
},
{
"path": "guidepost_example.jpg",
"license": "CC0-1.0",
"authors": [
"Mschaeuble"
],
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Signpost.jpg"
]
}
]

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Mschaeuble
SPDX-License-Identifier: CC0-1.0

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')"
],
@ -154,7 +154,7 @@
},
"maxCacheAge": 0
},
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"label": {
@ -194,7 +194,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 */ )",
@ -379,7 +379,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

@ -464,10 +464,14 @@
{
"builtin": [
"toilet",
"drinking_water"
"drinking_water",
"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
}
},
{
@ -130,7 +130,7 @@
]
},
"presets": null,
"minzoom": 19
"minzoom": 18
}
},
{
@ -143,7 +143,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/bicycle_parking.svg"
@ -161,7 +161,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/rental_bicycle.svg"
@ -179,7 +179,7 @@
]
},
"presets": null,
"minzoom": 19
"minzoom": 18
}
},
{
@ -195,7 +195,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering+": [
{
"color": "#00f",
@ -214,7 +214,7 @@
]
},
"presets": null,
"minzoom": 19,
"minzoom": 18,
"mapRendering+": [
{
"color": "yellow",
@ -235,13 +235,13 @@
"clock"
],
"override": {
"minzoom": 19
"minzoom": 18
}
},
{
"builtin": "bench",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "./assets/themes/stations/bench.svg"
@ -252,7 +252,7 @@
{
"builtin": "drinking_water",
"override": {
"minzoom": 19,
"minzoom": 18,
"mapRendering": [
{
"icon": "circle:white;./assets/themes/stations/drinking_water.svg"
@ -293,7 +293,7 @@
"zh_Hant": "時刻表"
}
},
"minzoom": 19,
"minzoom": 18,
"source": {
"osmTags": {
"and": [

View file

@ -221,7 +221,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

@ -32,7 +32,7 @@
51.190748429411705
]
],
"defaultBackgroundId": "CartoDB.DarkMatterNoLabels",
"defaultBackgroundId": "alidade.smooth_dark",
"layers": [
{
"id": "street_with_width",

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.",

12
package-lock.json generated
View file

@ -9612,9 +9612,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz",
"integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@ -20529,9 +20529,9 @@
}
},
"postcss": {
"version": "8.4.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz",
"integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"requires": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.33.7",
"version": "0.33.8",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -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 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

@ -63,7 +63,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
"Not creating a social image for " +
layout.id +
" as it is _not_ a .svg: " +
layout.icon
layout.icon,
)
return undefined
}
@ -85,7 +85,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
delete svg["defs"]
delete svg["$"]
let templateSvg = await ScriptUtils.ReadSvg(
"./public/assets/SocialImageTemplate" + template + ".svg"
"./public/assets/SocialImageTemplate" + template + ".svg",
)
templateSvg = Utils.WalkJson(
templateSvg,
@ -104,7 +104,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
return false
}
return mightBeTokenToReplace.circle[0]?.$?.style?.indexOf("fill:#ff00ff") >= 0
}
},
)
const builder = new xml2js.Builder()
@ -116,7 +116,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
async function createManifest(
layout: LayoutConfig,
alreadyWritten: string[]
alreadyWritten: string[],
): Promise<{
manifest: any
whiteIcons: string[]
@ -210,19 +210,17 @@ function asLangSpan(t: Translation, tag = "span"): string {
let previousSrc: Set<string> = new Set<string>()
let eliUrlsCached: string[]
function eliUrls(): string[] {
async function eliUrls(): Promise<string[]> {
if (eliUrlsCached) {
return eliUrlsCached
}
const urls: string[] = []
const regex = /{switch:([^}]+)}/
const rasterLayers = [
...AvailableRasterLayers.vectorLayers,
...eli.features,
...eli_global.layers.map((properties) => ({ properties })),
]
const rasterLayers = [AvailableRasterLayers.maptilerDefaultLayer, ...eli.features, ...eli_global.layers.map(properties => ({ properties }))]
for (const feature of rasterLayers) {
const url = (<RasterLayerPolygon>feature).properties.url
const f = <RasterLayerPolygon>feature
const url = f.properties.url
const match = url.match(regex)
if (match) {
const domains = match[1].split(",")
@ -231,17 +229,43 @@ function eliUrls(): string[] {
} else {
urls.push(url)
}
if (f.properties.type === "vector") {
// We also need to whitelist eventual sources
const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120)
for (const key of Object.keys(styleSpec.sources)) {
const url = styleSpec.sources[key].url
if(!url){
continue
}
eliUrlsCached = urls
return urls
let urlClipped = url
if(url.indexOf("?") > 0){
urlClipped = url?.substring(0, url.indexOf("?"))
}
console.log("Source url ",key,url)
urls.push(url)
if(urlClipped.endsWith(".json")){
const tileInfo = await Utils.downloadJsonCached(url, 1000*120)
urls.push(tileInfo["tiles"] ?? [])
}
function generateCsp(
}
urls.push(...(styleSpec["tiles"] ?? []))
urls.push(styleSpec["sprite"])
urls.push(styleSpec["glyphs"])
}
}
eliUrlsCached = urls
return Utils.NoNull(urls).sort()
}
async function generateCsp(
layout: LayoutConfig,
options: {
scriptSrcs: string[]
}
): string {
},
): Promise<string> {
const apiUrls: string[] = [
"'self'",
...Constants.defaultOverpassUrls,
@ -251,12 +275,12 @@ function generateCsp(
"https://pietervdvn.goatcounter.com",
]
.concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls))
.concat(...eliUrls())
.concat(...await eliUrls())
const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource)
const hosts = new Set<string>()
const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt(
new ImmutableStore({ lon: 0, lat: 0 })
new ImmutableStore({ lon: 0, lat: 0 }),
).data
const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector")
const vectorSources = vectorLayers.map((l) => l.properties.url)
@ -283,14 +307,14 @@ function generateCsp(
"connect-src items for theme",
layout.id,
"(extra sources: ",
newSrcs.join(" ") + ")"
newSrcs.join(" ") + ")",
)
previousSrc = hosts
const csp: Record<string, string> = {
"default-src": "'self'",
"script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join(
" "
" ",
),
"img-src": "* data:", // maplibre depends on 'data:' to load
"connect-src": connectSrc.join(" "),
@ -320,12 +344,12 @@ const removeOtherLanguagesHash = crypto
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
Locale.language.setData(layout.language[0])
const targetLanguage = layout.language[0]
const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"')
const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, "\\\"")
const ogDescr = Translations.T(
layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap"
layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap",
)
.textFor(targetLanguage)
.replace(/"/g, '\\"')
.replace(/"/g, "\\\"")
let ogImage = layout.socialImage
let twitterImage = ogImage
if (ogImage === LayoutConfig.defaultSocialImage && layout.official) {
@ -390,34 +414,34 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title })
const templateLines = template.split("\n")
const removeOtherLanguagesReference = templateLines.find(
(line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0
(line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0,
)
let output = template
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
.replace(
"Made with OpenStreetMap",
Translations.t.general.poweredByOsm.textFor(targetLanguage)
Translations.t.general.poweredByOsm.textFor(targetLanguage),
)
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
.replace(
/<!-- CSP -->/,
generateCsp(layout, {
await generateCsp(layout, {
scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`],
})
}),
)
.replace(removeOtherLanguagesReference, "<script>" + removeOtherLanguages + "</script>")
.replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription)
asLangSpan(layout.shortDescription),
)
.replace(
/<!-- IMAGE-START -->.*<!-- IMAGE-END -->/s,
"<img class='p-8 h-32 w-32 self-start' src='" + icon + "' />"
"<img class='p-8 h-32 w-32 self-start' src='" + icon + "' />",
)
.replace(
/.*\/src\/index\.ts.*/,
`<script type="module" src="./index_${layout.id}.ts"></script>`
`<script type="module" src="./index_${layout.id}.ts"></script>`,
)
return output
@ -508,13 +532,14 @@ async function main(): Promise<void> {
title: { en: "MapComplete" },
description: { en: "A thematic map viewer and editor based on OpenStreetMap" },
}),
alreadyWritten
alreadyWritten,
)
const manif = JSON.stringify(manifest, undefined, 2)
writeFileSync("public/index.webmanifest", manif)
}
ScriptUtils.fixUtils()
main().then(() => {
console.log("All done!")
})

View file

@ -22,3 +22,9 @@ report.mapcomplete.org {
to http://127.0.0.1:2600
}
}
studio.mapcomplete.org {
reverse_proxy {
to http://127.0.0.1:1235
}
}

View file

@ -1,60 +1,68 @@
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
import { WritableFeatureSource } from "../FeatureSource"
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import { Feature, Point } from "geojson"
import { TagUtils } from "../../Tags/TagUtils"
import BaseUIElement from "../../../UI/BaseUIElement"
import { Utils } from "../../../Utils"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import { WritableFeatureSource } from "../FeatureSource";
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource";
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.
* Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties
*/
export class LastClickFeatureSource implements WritableFeatureSource {
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
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) {
const allPresets: BaseUIElement[] = []
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++) {
const preset = layer.presets[i]
const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags))
const preset = layer.presets[i];
const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags));
const { html } = layer.mapRendering[0].RenderIcon(tags, false, {
noSize: true,
includeBadges: false,
})
allPresets.push(html)
includeBadges: false
});
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],
this.features.setData([this.createFeature(lon, lat)]);
});
}
i++
const point = <Feature<Point>>{
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],
},
coordinates: [lon, lat]
}
this.features.setData([point])
})
};
}
}

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

@ -4,39 +4,11 @@ export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}

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

@ -53,61 +53,6 @@ export class AvailableRasterLayers {
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 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[]> {
@ -119,7 +64,7 @@ export class AvailableRasterLayers {
)
})
)
const available = Stores.ListStabilized(
return Stores.ListStabilized(
availableLayersBboxes.map((eliPolygons) => {
const loc = location.data
const lonlat: [number, number] = [loc.lon, loc.lat]
@ -129,12 +74,11 @@ export class AvailableRasterLayers {
}
return GeoOperations.inside(lonlat, eliPolygon)
})
matching.push(AvailableRasterLayers.maptilerDefaultLayer)
matching.push(...AvailableRasterLayers.globalLayers)
matching.unshift(...AvailableRasterLayers.vectorLayers)
return matching
})
)
return available
}
}

View file

@ -94,6 +94,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)

View file

@ -1,62 +1,58 @@
import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import LayoutConfig from "./ThemeConfig/LayoutConfig";
import { SpecialVisualizationState } from "../UI/SpecialVisualization";
import { Changes } from "../Logic/Osm/Changes";
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { ExportableMap, MapProperties } from "./MapProperties";
import LayerState from "../Logic/State/LayerState";
import { Feature, Point, Polygon } from "geojson";
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import { Map as MlMap } from "maplibre-gl";
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor";
import { GeoLocationState } from "../Logic/State/GeoLocationState";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import { QueryParameters } from "../Logic/Web/QueryParameters";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LayerConfig from "./ThemeConfig/LayerConfig";
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers";
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore";
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource";
import ShowDataLayer from "../UI/Map/ShowDataLayer";
import TitleHandler from "../Logic/Actors/TitleHandler";
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
import { BBox } from "../Logic/BBox";
import Constants from "./Constants";
import Hotkeys from "../UI/Base/Hotkeys";
import Translations from "../UI/i18n/Translations";
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource";
import { MenuState } from "./MenuState";
import MetaTagging from "../Logic/MetaTagging";
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator";
import {
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState"
import { Feature, Point, Polygon } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Map as MlMap } from "maplibre-gl"
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import { BBox } from "../Logic/BBox"
import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import { MenuState } from "./MenuState"
import MetaTagging from "../Logic/MetaTagging"
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
import { Utils } from "../Utils"
import { EliCategory } from "./RasterLayerProperties"
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, {
FeatureViewState,
} from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { Imgur } from "../Logic/ImageProviders/Imgur"
NewGeometryFromChangesFeatureSource
} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource";
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer";
import { Utils } from "../Utils";
import { EliCategory } from "./RasterLayerProperties";
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter";
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage";
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource";
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor";
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector";
import FilteredLayer from "./FilteredLayer";
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector";
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
import { Imgur } from "../Logic/ImageProviders/Imgur";
/**
*
@ -67,71 +63,74 @@ import { Imgur } from "../Logic/ImageProviders/Imgur"
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>
readonly changes: Changes
readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>
readonly layout: LayoutConfig;
readonly map: UIEventSource<MlMap>;
readonly changes: Changes;
readonly featureSwitches: FeatureSwitchState;
readonly featureSwitchIsTesting: Store<boolean>;
readonly featureSwitchUserbadge: Store<boolean>;
readonly featureProperties: FeaturePropertiesStore
readonly featureProperties: FeaturePropertiesStore;
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly mapProperties: MapProperties & ExportableMap
readonly osmObjectDownloader: OsmObjectDownloader
readonly osmConnection: OsmConnection;
readonly selectedElement: UIEventSource<Feature>;
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
readonly mapProperties: MapProperties & ExportableMap;
readonly osmObjectDownloader: OsmObjectDownloader;
readonly dataIsLoading: Store<boolean>
readonly dataIsLoading: Store<boolean>;
/**
* Indicates if there is _some_ data in view, even if it is not shown due to the filters
*/
readonly hasDataInView: Store<FeatureViewState>
readonly hasDataInView: Store<FeatureViewState>;
readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly guistate: MenuState;
readonly fullNodeDatabase?: FullNodeDatabaseSource;
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
readonly currentView: FeatureSource<Feature<Polygon>>
readonly featuresInView: FeatureSource
readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>;
readonly indexedFeatures: IndexedFeatureSource & LayoutSource;
readonly currentView: FeatureSource<Feature<Polygon>>;
readonly featuresInView: FeatureSource;
readonly newFeatures: WritableFeatureSource;
readonly layerState: LayerState;
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>;
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>;
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
readonly availableLayers: Store<RasterLayerPolygon[]>;
readonly selectedLayer: UIEventSource<LayerConfig>;
readonly userRelatedState: UserRelatedState;
readonly geolocation: GeoLocationHandler;
readonly imageUploadManager: ImageUploadManager
readonly imageUploadManager: ImageUploadManager;
readonly lastClickObject: WritableFeatureSource
readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false);
readonly lastClickObject: LastClickFeatureSource;
readonly overlayLayerStates: ReadonlyMap<
string,
{ readonly isDisplayed: UIEventSource<boolean> }
>
>;
/**
* All 'level'-tags that are available with the current features
*/
readonly floors: Store<string[]>
readonly floors: Store<string[]>;
private readonly newPointDialog: FilteredLayer;
constructor(layout: LayoutConfig) {
Utils.initDomPurify()
this.layout = layout
this.featureSwitches = new FeatureSwitchState(layout)
Utils.initDomPurify();
this.layout = layout;
this.featureSwitches = new FeatureSwitchState(layout);
this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id
)
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
);
this.map = new UIEventSource<MlMap>(undefined);
const initial = new InitialMapPositioning(layout);
this.mapProperties = new MapLibreAdaptor(this.map, initial);
const geolocationState = new GeoLocationState();
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting;
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin;
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
@ -140,57 +139,57 @@ export default class ThemeViewState implements SpecialVisualizationState {
"oauth_token",
undefined,
"Used to complete the login"
),
})
)
});
this.userRelatedState = new UserRelatedState(
this.osmConnection,
layout?.language,
layout,
this.featureSwitches,
this.mapProperties
)
);
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
this.mapProperties.allowRotating.setData(fixated !== "yes")
})
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.mapProperties.allowRotating.setData(fixated !== "yes");
});
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element");
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer");
this.selectedElementAndLayer = this.selectedElement.mapD(
(feature) => {
const layer = this.selectedLayer.data
const layer = this.selectedLayer.data;
if (!layer) {
return undefined
return undefined;
}
return { layer, feature }
return { layer, feature };
},
[this.selectedLayer]
)
);
this.geolocation = new GeoLocationHandler(
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime
)
);
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
const self = this
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
const self = this;
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
{
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>();
for (const rasterInfo of this.layout.tileLayerSources) {
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown"
)
const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state)
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
);
const state = { isDisplayed };
overlayLayerStates.set(rasterInfo.id, state);
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state);
}
this.overlayLayerStates = overlayLayerStates
this.overlayLayerStates = overlayLayerStates;
}
{
@ -199,7 +198,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/
if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
this.fullNodeDatabase = new FullNodeDatabaseSource()
this.fullNodeDatabase = new FullNodeDatabaseSource();
}
const layoutSource = new LayoutSource(
@ -209,49 +208,49 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed,
this.fullNodeDatabase
)
);
this.indexedFeatures = layoutSource
this.indexedFeatures = layoutSource;
const empty = []
let currentViewIndex = 0
const empty = [];
this.currentView = new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) => {
if (!bbox) {
return empty
return empty;
}
currentViewIndex++
currentViewIndex++;
return <Feature[]>[
bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view",
}),
]
id: "current_view_"+currentViewIndex
})
)
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading
];
})
);
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds);
this.dataIsLoading = layoutSource.isLoading;
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const indexedElements = this.indexedFeatures;
this.featureProperties = new FeaturePropertiesStore(indexedElements);
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
historicalUserLocations: this.geolocation.historicalUserLocations
},
layout?.isLeftRightSensitive() ?? false
)
this.historicalUserLocations = this.geolocation.historicalUserLocations
);
this.historicalUserLocations = this.geolocation.historicalUserLocations;
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
indexedElements,
this.featureProperties
)
layoutSource.addSource(this.newFeatures)
);
layoutSource.addSource(this.newFeatures);
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
@ -267,11 +266,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
features.length,
"leftover features, such as",
features[0].properties
)
},
);
}
)
this.perLayer = perLayer.perLayer
}
);
this.perLayer = perLayer.perLayer;
}
this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage(
@ -281,80 +280,80 @@ export default class ThemeViewState implements SpecialVisualizationState {
fs,
this.featureProperties,
fs.layer.layerDef.maxAgeOfCache
)
})
);
});
this.newPointDialog = this.layerState.filteredLayers.get("last_click");
this.floors = this.featuresInView.features.stabilized(500).map((features) => {
if (!features) {
return []
return [];
}
const floors = new Set<string>()
const floors = new Set<string>();
for (const feature of features) {
let level = feature.properties["_level"]
let level = feature.properties["_level"];
if (level) {
const levels = level.split(";")
const levels = level.split(";");
for (const l of levels) {
floors.add(l)
floors.add(l);
}
} else {
floors.add("0") // '0' is the default and is thus _always_ present
floors.add("0"); // '0' is the default and is thus _always_ present
}
}
const sorted = Array.from(floors)
const sorted = Array.from(floors);
// Sort alphabetically first, to deal with floor "A", "B" and "C"
sorted.sort()
sorted.sort();
sorted.sort((a, b) => {
// We use the laxer 'parseInt' to deal with floor '1A'
const na = parseInt(a)
const nb = parseInt(b)
const na = parseInt(a);
const nb = parseInt(b);
if (isNaN(na) || isNaN(nb)) {
return 0
return 0;
}
return na - nb
})
sorted.reverse(/* new list, no side-effects */)
return sorted
})
return na - nb;
});
sorted.reverse(/* new list, no side-effects */);
return sorted;
});
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
))
));
this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(),
this.changes
)
);
this.perLayerFiltered = this.showNormalDataOn(this.map)
this.perLayerFiltered = this.showNormalDataOn(this.map);
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
this.imageUploadManager = new ImageUploadManager(
layout,
Imgur.singleton,
this.featureProperties,
this.osmConnection,
this.changes
)
);
this.initActors()
this.addLastClick(lastClick)
this.drawSpecialLayers()
this.initHotkeys()
this.miscSetup()
this.initActors();
this.drawSpecialLayers();
this.initHotkeys();
this.miscSetup();
if (!Utils.runningFromConsole) {
console.log("State setup completed", this)
console.log("State setup completed", this);
}
}
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
const filteringFeatureSource = new Map<string, FilteringFeatureSource>();
this.perLayer.forEach((fs, layerName) => {
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
);
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
@ -364,15 +363,15 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
* However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
* */
return
return;
}
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
filteringFeatureSource.set(layerName, filtered)
);
filteringFeatureSource.set(layerName, filtered);
new ShowDataLayer(map, {
layer: fs.layer.layerDef,
@ -380,30 +379,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
return filteringFeatureSource
fetchStore: (id) => this.featureProperties.getStore(id)
});
});
return filteringFeatureSource;
}
/**
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout)
this.userRelatedState.markLayoutAsVisited(this.layout);
this.selectedElement.addCallbackAndRunD((feature) => {
// As soon as we have a selected element, we clear the selected element
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
if (feature.properties.id === "last_click") {
return
return;
}
this.lastClickObject.features.setData([])
})
this.lastClickObject.features.setData([]);
});
if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
Utils.LoadCustomCss(this.layout.customCss)
Utils.LoadCustomCss(this.layout.customCss);
}
}
@ -412,97 +411,116 @@ export default class ThemeViewState implements SpecialVisualizationState {
{ nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
this.selectedElement.setData(undefined)
this.guistate.closeAll()
this.selectedElement.setData(undefined);
this.guistate.closeAll();
}
)
);
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun(enable => {
if(!enable){
return
}
Hotkeys.RegisterHotkey(
{
nomod: "b",
nomod: "b"
},
Translations.t.hotkeyDocumentation.openLayersPanel,
() => {
if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView()
this.guistate.openFilterView();
}
}
)
);
Hotkeys.RegisterHotkey(
{ shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik,
() => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto);
}
)
);
const setLayerCategory = (category: EliCategory) => {
const available = this.availableLayers.data
const current = this.mapProperties.rasterLayer
const available = this.availableLayers.data;
const current = this.mapProperties.rasterLayer;
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
available,
category,
current.data
)
console.log("Best layer for category", category, "is", best.properties.id)
current.setData(best)
}
);
console.log("Best layer for category", category, "is", best.properties.id);
current.setData(best);
};
Hotkeys.RegisterHotkey(
{ nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap")
)
);
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map")
)
);
Hotkeys.RegisterHotkey(
{ nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo")
)
);
return true
})
}
private addLastClick(last_click: LastClickFeatureSource) {
// The last_click gets a _very_ special treatment as it interacts with various parts
const last_click_layer = this.layerState.filteredLayers.get("last_click")
this.featureProperties.trackFeatureSource(last_click)
this.indexedFeatures.addSource(last_click)
this.featureProperties.trackFeatureSource(last_click);
this.indexedFeatures.addSource(last_click);
last_click.features.addCallbackAndRunD((features) => {
if (this.selectedLayer.data?.id === "last_click") {
// The last-click location moved, but we have selected the last click of the previous location
// So, we update _after_ clearing the selection to make sure no stray data is sticking around
this.selectedElement.setData(undefined)
this.selectedElement.setData(features[0])
this.selectedElement.setData(undefined);
this.selectedElement.setData(features[0]);
}
})
});
new ShowDataLayer(this.map, {
features: new FilteringFeatureSource(last_click_layer, last_click),
features: new FilteringFeatureSource(this.newPointDialog, last_click),
doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
layer: last_click_layer.layerDef,
layer: this.newPointDialog.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
onClick: (feature: Feature) => {
if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint,
center: this.mapProperties.lastClickLocation.data,
})
return
center: this.mapProperties.lastClickLocation.data
});
return;
}
// We first clear the selection to make sure no weird state is around
this.selectedLayer.setData(undefined)
this.selectedElement.setData(undefined)
this.selectedLayer.setData(undefined);
this.selectedElement.setData(undefined);
this.selectedElement.setData(feature)
this.selectedLayer.setData(last_click_layer.layerDef)
},
})
this.selectedElement.setData(feature);
this.selectedLayer.setData(this.newPointDialog.layerDef);
}
});
}
public openNewDialog() {
this.selectedLayer.setData(undefined);
this.selectedElement.setData(undefined);
const { lon, lat } = this.mapProperties.location.data;
const feature = this.lastClickObject.createFeature(lon, lat)
this.featureProperties.trackFeature(feature)
this.selectedElement.setData(feature);
this.selectedLayer.setData(this.newPointDialog.layerDef);
}
/**
@ -510,7 +528,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/
private drawSpecialLayers() {
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
const empty = []
const empty = [];
/**
* A listing which maps the layerId onto the featureSource
*/
@ -530,21 +548,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
)
),
current_view: this.currentView,
}
current_view: this.currentView
};
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
const bbox = new BBox(this.layout.lockLocation);
this.mapProperties.maxbounds.setData(bbox);
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
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");
if (currentViewLayer?.tagRenderings?.length > 0) {
const params = MetaTagging.createExtraFuncParams(this)
this.featureProperties.trackFeatureSource(specialLayers.current_view)
const params = MetaTagging.createExtraFuncParams(this);
this.featureProperties.trackFeatureSource(specialLayers.current_view);
specialLayers.current_view.features.addCallbackAndRunD((features) => {
MetaTagging.addMetatags(
features,
@ -553,36 +571,36 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.layout,
this.osmObjectDownloader,
this.featureProperties
)
})
);
});
}
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range")
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range");
const rangeIsDisplayed = rangeFLayer?.isDisplayed
const rangeIsDisplayed = rangeFLayer?.isDisplayed;
if (
!QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
) {
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true);
}
this.layerState.filteredLayers.forEach((flayer) => {
const id = flayer.layerDef.id
const features: FeatureSource = specialLayers[id]
const id = flayer.layerDef.id;
const features: FeatureSource = specialLayers[id];
if (features === undefined) {
return
return;
}
this.featureProperties.trackFeatureSource(features)
this.featureProperties.trackFeatureSource(features);
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
})
})
selectedLayer: this.selectedLayer
});
});
}
/**
@ -591,35 +609,35 @@ export default class ThemeViewState implements SpecialVisualizationState {
private initActors() {
// Unselect the selected element if it is panned out of view
this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
const selected = this.selectedElement.data
const selected = this.selectedElement.data;
if (selected === undefined) {
return
return;
}
const bbox = BBox.get(selected)
const bbox = BBox.get(selected);
if (!bbox.overlapsWith(bounds)) {
this.selectedElement.setData(undefined)
this.selectedElement.setData(undefined);
}
})
});
this.selectedElement.addCallback((selected) => {
if (selected === undefined) {
// We did _unselect_ an item - we always remove the lastclick-object
this.lastClickObject.features.setData([])
this.selectedLayer.setData(undefined)
this.lastClickObject.features.setData([]);
this.selectedLayer.setData(undefined);
}
})
new ThemeViewStateHashActor(this)
new MetaTagging(this)
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement)
new SelectedElementTagsUpdater(this)
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
});
new ThemeViewStateHashActor(this);
new MetaTagging(this);
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this);
new ChangeToElementsActor(this.changes, this.featureProperties);
new PendingChangesUploader(this.changes, this.selectedElement);
new SelectedElementTagsUpdater(this);
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers);
new PreferredRasterLayerSelector(
this.mapProperties.rasterLayer,
this.availableLayers,
this.featureSwitches.backgroundLayerId,
this.userRelatedState.preferredBackgroundLayer
)
);
}
}

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

@ -3,16 +3,28 @@
* 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 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)
window.setTimeout(() => tabElements[tab.data].click(), 50);
}
}
</script>
@ -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")}>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition0 && "hidden")}>
<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")}>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition1 && "hidden")}>
<div bind:this={tabElements[1]} class="flex">
<slot name="title1" />
</div>
</Tab>
{/if}
{#if $$slots.title2}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition2 && "hidden")}>
<div bind:this={tabElements[2]} class="flex">
<slot name="title2" />
</div>
</Tab>
{/if}
{#if $$slots.title3}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition3 && "hidden")}>
<div bind:this={tabElements[3]} class="flex">
<slot name="title3" />
</div>
</Tab>
{/if}
{#if $$slots.title4}
<Tab class={({ selected }) => twJoin("tab", selected && "primary")}>
<Tab class={({ selected }) => twJoin("tab", selected && "primary", !$condition4 && "hidden")}>
<div bind:this={tabElements[4]} class="flex">
<slot name="title4" />
</div>
</Tab>
{/if}
</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>

View file

@ -1,45 +1,46 @@
<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
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
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
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)
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
glstate.requestPermission();
return;
}
}
</script>
@ -62,7 +63,9 @@
</div>
</NextButton>
<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")} />
@ -94,7 +97,10 @@
<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
@ -117,6 +123,7 @@
<SearchIcon class="h-6 w-6" />
</button>
</div>
</If>
</div>
</div>

View file

@ -1,98 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import InputElementMap from "./InputElementMap"
import Translations from "../i18n/Translations"
/**
* @deprecated
*/
export class CheckBox extends InputElementMap<number[], boolean> {
constructor(el: BaseUIElement | string, defaultValue?: boolean) {
super(
new CheckBoxes([Translations.W(el)]),
(x0, x1) => x0 === x1,
(t) => t.length > 0,
(x) => (x ? [0] : [])
)
if (defaultValue !== undefined) {
this.GetValue().setData(defaultValue)
}
}
}
/**
* A list of individual checkboxes
* The value will contain the indexes of the selected checkboxes
*/
export default class CheckBoxes extends InputElement<number[]> {
private static _nextId = 0
private readonly value: UIEventSource<number[]>
private readonly _elements: BaseUIElement[]
constructor(elements: BaseUIElement[], value = new UIEventSource<number[]>([])) {
super()
this.value = value
this._elements = Utils.NoNull(elements)
this.SetClass("flex flex-col")
}
IsValid(ts: number[]): boolean {
return ts !== undefined
}
GetValue(): UIEventSource<number[]> {
return this.value
}
protected InnerConstructElement(): HTMLElement {
const formTag = document.createElement("form")
const value = this.value
const elements = this._elements
for (let i = 0; i < elements.length; i++) {
let inputI = elements[i]
const input = document.createElement("input")
const id = CheckBoxes._nextId
CheckBoxes._nextId++
input.id = "checkbox" + id
input.type = "checkbox"
input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0")
const label = document.createElement("label")
label.htmlFor = input.id
label.appendChild(input)
label.appendChild(inputI.ConstructElement())
label.classList.add("block", "w-full", "p-2", "cursor-pointer")
formTag.appendChild(label)
value.addCallbackAndRunD((selectedValues) => {
input.checked = selectedValues.indexOf(i) >= 0
if (input.checked) {
label.classList.add("checked")
} else {
label.classList.remove("checked")
}
})
input.onchange = () => {
// Index = index in the list of already checked items
const index = value.data.indexOf(i)
if (input.checked && index < 0) {
value.data.push(i)
value.ping()
} else if (index >= 0) {
value.data.splice(index, 1)
value.ping()
}
}
}
return formTag
}
}

View file

@ -1,61 +0,0 @@
import { InputElement } from "./InputElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
/**
* @deprecated
*/
export default class InputElementMap<T, X> extends InputElement<X> {
private readonly _inputElement: InputElement<T>
private isSame: (x0: X, x1: X) => boolean
private readonly fromX: (x: X) => T
private readonly toX: (t: T) => X
private readonly _value: UIEventSource<X>
constructor(
inputElement: InputElement<T>,
isSame: (x0: X, x1: X) => boolean,
toX: (t: T) => X,
fromX: (x: X) => T,
extraSources: Store<any>[] = []
) {
super()
this.isSame = isSame
this.fromX = fromX
this.toX = toX
this._inputElement = inputElement
const self = this
this._value = inputElement.GetValue().sync(
(t) => {
const newX = toX(t)
const currentX = self.GetValue()?.data
if (isSame(currentX, newX)) {
return currentX
}
return newX
},
extraSources,
(x) => {
return fromX(x)
}
)
}
GetValue(): UIEventSource<X> {
return this._value
}
IsValid(x: X): boolean {
if (x === undefined) {
return false
}
const t = this.fromX(x)
if (t === undefined) {
return false
}
return this._inputElement.IsValid(t)
}
protected InnerConstructElement(): HTMLElement {
return this._inputElement.ConstructElement()
}
}

View file

@ -12,7 +12,7 @@ import { QueryParameters } from "../Logic/Web/QueryParameters"
export default class LanguagePicker extends Toggle {
constructor(languages: string[], assignTo: UIEventSource<string>) {
console.log("Constructing a language pîcker for languages", languages)
console.log("Constructing a language picker for languages", languages)
if (
languages === undefined ||
languages.length <= 1 ||

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
const layer = selectedLayer.data;
if (selectedElement === undefined || layer === undefined) {
return undefined
return undefined;
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined
return undefined;
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags })
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
const layer = selectedLayer.data;
if (selectedElement === undefined || layer === undefined) {
return undefined
return undefined;
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
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 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
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
rasterLayerName = l.properties.name;
})
)
);
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -168,9 +168,29 @@
<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 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={() => {
@ -181,6 +201,7 @@
© OpenStreetMap, <span class="w-24">{rasterLayerName}</span>
</a>
</div>
</div>
<div class="flex flex-col items-end">
<!-- bottom right elements -->
@ -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>
</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">
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<ToSvelte
construct={() =>
new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")}
/>
<MapillaryLink mapProperties={state.mapProperties} />
</If>
<ToSvelte construct={Hotkeys.generateDocumentationDynamic} />
</div>
</TabbedGroup>

View file

@ -1098,7 +1098,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
return { content: data }
} catch (e) {
console.error("Could not parse ", data, "due to", e, "\n", e.stack)
console.error(
"Could not parse the response of",
url,
"which contains",
data,
"due to",
e,
"\n",
e.stack
)
return { error: "malformed", url }
}
}

View file

@ -1,97 +1,169 @@
{
"layers": [
{
"id": "Stamen.TonerLite",
"name": "Toner Lite (by Stamen)",
"url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png",
"name": "Americana",
"url": "https://zelonewolf.github.io/openstreetmap-americana/style.json",
"category": "osmbasedmap",
"id": "americana",
"type": "vector",
"attribution": {
"html": "Map tiles by <a href=\"http://stamen.com\">Stamen Design</a>, <a href=\"http://creativecommons.org/licenses/by/3.0\">CC BY 3.0</a> &mdash; Map data {attribution.OpenStreetMap}"
},
"min_zoom": 0,
"max_zoom": 20
"text": "Americana",
"url": "https://github.com/ZeLonewolf/openstreetmap-americana/"
}
},
{
"id": "Stamen.TonerBackground",
"name": "Toner Background - no labels (by Stamen)",
"name": "MapTiler Backdrop",
"url": "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
"category": "osmbasedmap",
"url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png",
"id": "maptiler.backdrop",
"type": "vector",
"attribution": {
"html": "Map tiles by <a href=\"http://stamen.com\">Stamen Design</a>, <a href=\"http://creativecommons.org/licenses/by/3.0\">CC BY 3.0</a> &mdash; Map data {attribution.OpenStreetMap}"
},
"min_zoom": 0,
"max_zoom": 20
"text": "Maptiler",
"url": "https://www.maptiler.com/copyright/"
}
},
{
"id": "Stamen.Watercolor",
"name": "Watercolor (by Stamen)",
"name": "MapTiler Carto",
"url": "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
"category": "osmbasedmap",
"url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png",
"id": "maptiler.carto",
"type": "vector",
"attribution": {
"html": "Map tiles by <a href=\"http://stamen.com\">Stamen Design</a>, <a href=\"http://creativecommons.org/licenses/by/3.0\">CC BY 3.0</a> &mdash; Map data {attribution.OpenStreetMap}"
},
"min_zoom": 0,
"max_zoom": 20
"text": "Maptiler",
"url": "https://www.maptiler.com/copyright/"
}
},
{
"id": "CartoDB.Positron",
"name": "Positron (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png",
"name": "Alidade Smooth",
"url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
"category": "osmbasedmap",
"id": "alidade.smooth",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
},
"max_zoom": 20,
"category": "osmbasedmap"
"text": "Alidade",
"url": "https://stadiamaps.com/"
}
},
{
"id": "CartoDB.PositronNoLabels",
"name": "Positron - no labels (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png",
"name": "Alidade Smooth Dark",
"url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth_dark.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
"category": "osmbasedmap",
"id": "alidade.smooth_dark",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
},
"max_zoom": 20
"text": "Alidade/Stadiamaps",
"url": "https://stadiamaps.com/"
}
},
{
"id": "CartoDB.Voyager",
"name": "Voyager (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png",
"name": "Stamen Terrain",
"url": "https://tiles-eu.stadiamaps.com/styles/stamen_terrain.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
"category": "osmbasedmap",
"id": "stamen.terrain",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
},
"max_zoom": 20
"text": "Stamen/Stadiamaps",
"url": "https://stadiamaps.com/"
}
},
{
"id": "CartoDB.VoyagerNoLabels",
"name": "Voyager - no labels (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png",
"name": "Stamen Toner",
"url": "https://tiles-eu.stadiamaps.com/styles/stamen_toner.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
"category": "osmbasedmap",
"id": "stamen.toner",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
},
"max_zoom": 20
"text": "Stamen/Stadiamaps",
"url": "https://stadiamaps.com/"
}
}, {
"name": "Stamen Watercolor",
"url": "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
"category": "osmbasedmap",
"id": "stamen.watercolor",
"attribution": {
"text": "Stamen/Stadiamaps",
"url": "https://stadiamaps.com/"
}
},
{
"id": "CartoDB.DarkMatter",
"name": "Dark Matter (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
"url": "https://tiles-eu.stadiamaps.com/styles/osm_bright.json",
"name": "StadiaMaps OSM Bright",
"category": "osmbasedmap",
"id": "stadia.bright",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
},
"max_zoom": 20
"text": "Stadiamaps",
"url": "https://stadiamaps.com/"
}
},
{
"id": "CartoDB.DarkMatterNoLabels",
"name": "Dark Matter - no labels (by CartoDB)",
"url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
"url": "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Positron",
"category": "osmbasedmap",
"id": "carto.positron",
"type": "vector",
"attribution": {
"html": "<a href=\"https://carto.com/attributions\">CARTO</a>"
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
},
"max_zoom": 20
{
"url": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Dark Matter",
"category": "osmbasedmap",
"id": "carto.dark_matter",
"type": "vector",
"attribution": {
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
},
{
"url": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Voyager",
"category": "osmbasedmap",
"id": "carto.voyager",
"type": "vector",
"attribution": {
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
},
{
"url": "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Positron (no labels)",
"category": "osmbasedmap",
"id": "carto.positron_no_labels",
"type": "vector",
"attribution": {
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
},
{
"url": "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Dark Matter (no labels)",
"category": "osmbasedmap",
"id": "carto.dark_matter_no_labels",
"type": "vector",
"attribution": {
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
},
{
"url": "https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
"name": "Carto Voyager (no labels)",
"category": "osmbasedmap",
"id": "carto.voyager_no_labels",
"type": "vector",
"attribution": {
"text": "<a href=\"https://carto.com/about-carto/\" target=\"_blank\" rel=\"noopener\">CARTO</a>",
"url": "https://carto.com/"
}
}
]
}