Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-02-15 17:48:26 +01:00
commit f0823f4c4d
524 changed files with 18747 additions and 8546 deletions

View file

@ -39,7 +39,7 @@ export default class ScriptUtils {
public static DownloadFileTo(url, targetFilePath: string): Promise<void> {
ScriptUtils.erasableLog("Downloading", url, "to", targetFilePath)
return new Promise<void>((resolve, err) => {
return new Promise<void>((resolve) => {
https.get(url, (res) => {
const filePath = fs.createWriteStream(targetFilePath)
res.pipe(filePath)

View file

@ -18,11 +18,12 @@ fi
export NODE_OPTIONS=--max-old-space-size=16000
which vite
vite --version
vite build --sourcemap
vite build --sourcemap || { echo 'Vite build failed' ; exit 1; }
# 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 assets/png/ dist/assets/png/
mkdir dist/assets/langs
mkdir dist/assets/langs/layers
cp -r langs/layers/ dist/assets/langs/

View file

@ -112,7 +112,7 @@ export class Conflate extends Script {
const changedObjects: OsmObject[] = []
for (const { match, replayed } of bestMatches) {
const { external_feature, d, osm_feature } = match
const { d, osm_feature } = match
const { possibly_imported, certainly_imported, resting_properties } = replayed
const status = resting_properties["status"]
delete resting_properties["status"]

296
scripts/downloadCommons.ts Normal file
View file

@ -0,0 +1,296 @@
/**
* Script to download images from Wikimedia Commons, and save them together with license information.
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
import { unescape } from "querystring"
import SmallLicense from "../src/Models/smallLicense"
interface ExtMetadataProp {
value: string
source: string
hidden: string
}
interface ImageQueryAPIResponse {
continue: {
iistart: string
continue: string
}
query: {
normalized?: {
from: string
to: string
}[]
pages: {
[key: string]: {
pageid: number
ns: number
title: string
imagerepository: string
imageinfo?: {
user: string
url: string
descriptionurl: string
descriptionshorturl: string
extmetadata?: {
DateTime: ExtMetadataProp
ObjectName: ExtMetadataProp
CommonsMetadataExtension?: ExtMetadataProp
Categories?: ExtMetadataProp
Assessments?: ExtMetadataProp
ImageDescription?: ExtMetadataProp
DateTimeOriginal?: ExtMetadataProp
Credit?: ExtMetadataProp
Artist?: ExtMetadataProp
LicenseShortName?: ExtMetadataProp
UsageTerms?: ExtMetadataProp
AttributionRequired?: ExtMetadataProp
Copyrighted?: ExtMetadataProp
Restrictions?: ExtMetadataProp
License?: ExtMetadataProp
}
}[]
}
}
}
}
interface CategoryMember {
pageid: number
ns: number
title: string
}
interface CategoryQueryAPIResponse {
batchcomplete: string
query: {
categorymembers: CategoryMember[]
}
}
interface TemplateQueryAPIResponse {
batchcomplete: string
query: {
normalized?: {
from: string
to: string
}[]
pages: {
[key: string]: {
pageid: number
ns: number
title: string
templates?: {
ns: number
title: string
}[]
}
}
}
}
// Map license names of Wikimedia Commons to different names
const licenseMapping = {}
// Map template names to license names
const templateMapping = {
"Template:PD": "Public Domain",
}
async function main(args: string[]) {
if (args.length < 2) {
console.log("Usage: downloadCommons.ts <output folder> <url> <?url> <?url> .. ")
console.log(
"Example: npx vite-node downloadCommons.ts -- assets/svg https://commons.wikimedia.org/wiki/File:Example.jpg"
)
process.exit(1)
}
const [outputFolder, ...urls] = args
for (const url of urls) {
// Download details from the API
const commonsFileNamePath = url.split("/").pop()
if (commonsFileNamePath !== undefined) {
const commonsFileName = commonsFileNamePath.split("?").shift()
if (commonsFileName !== undefined) {
console.log(`Processing ${commonsFileName}...`)
const baseUrl = url.split("/").slice(0, 3).join("/")
// Check if it is a file or a category
if (url.includes("Category:")) {
// Download all files in the category
const apiUrl = `${baseUrl}/w/api.php?action=query&format=json&list=categorymembers&cmtitle=${commonsFileName}&cmlimit=250&cmtype=file`
const response = await fetch(apiUrl)
const apiDetails: CategoryQueryAPIResponse = await response.json()
for (const member of apiDetails.query.categorymembers) {
await downloadImage(member.title, outputFolder, baseUrl)
}
} else {
await downloadImage(commonsFileName, outputFolder, baseUrl)
}
} else {
console.log(
"\x1b[31m%s\x1b[0m",
`URL ${url} doesn't seem to contain a filename or category! Skipping...`
)
continue
}
} else {
console.log(
"\x1b[31m%s\x1b[0m",
`URL ${url} doesn't seem to be a valid URL! Skipping...`
)
continue
}
}
}
async function downloadImage(filename: string, outputFolder: string, baseUrl: string) {
const apiUrl = `${baseUrl}/w/api.php?action=query&format=json&prop=imageinfo&iiprop=url|extmetadata|user&iimetadataversion=latest&titles=${filename}`
const response = await fetch(apiUrl)
const apiDetails: ImageQueryAPIResponse = await response.json()
const missingPage = apiDetails.query.pages["-1"]
// Check if the file exists, locally or externally
if (missingPage !== undefined) {
// Image does not exist locally, check if it exists externally
if (
apiDetails.query.pages["-1"].imagerepository !== "local" &&
apiDetails.query.pages["-1"].imagerepository !== ""
) {
// Check if we actually have image info
if (missingPage.imageinfo?.length !== undefined && missingPage.imageinfo.length > 0) {
const externalUrl = missingPage.imageinfo[0].descriptionurl
const externalBase = externalUrl.split("/").slice(0, 3).join("/")
const externalFilenamePath = externalUrl.split("/").pop()
if (externalFilenamePath !== undefined) {
const externalFilename = externalFilenamePath.split("?").shift()
console.log(
`\x1b[33m%s\x1b[0m`,
`${filename} is external, re-running with ${externalUrl}...`
)
if (externalFilename !== undefined) {
await downloadImage(externalFilename, outputFolder, externalBase)
return
} else {
// Edge case
console.log(
`\x1b[33m%s\x1b[0m`,
`External URL ${externalUrl} doesn't seem to contain a filename or category! Skipping...`
)
}
} else {
// Edge case
console.log(
`\x1b[33m%s\x1b[0m`,
`External URL ${externalUrl} doesn't seem to be a valid URL! Skipping...`
)
return
}
} else {
console.log(
`\x1b[33m%s\x1b[0m`,
`${filename} does not have image info!, skipping...`
)
}
}
console.log(`\x1b[33m%s\x1b[0m`, `${filename} does not exist!, skipping...`)
} else {
// Harvest useful information
const wikiPage = apiDetails.query.pages[Object.keys(apiDetails.query.pages)[0]]
// Check if we actually have image info
if (wikiPage.imageinfo?.length !== undefined && wikiPage.imageinfo.length > 0) {
const wikiUrl = wikiPage.imageinfo[0].descriptionurl
const fileUrl = wikiPage.imageinfo[0].url
const author =
wikiPage.imageinfo[0].extmetadata?.Artist?.value || wikiPage.imageinfo[0].user
let license = wikiPage.imageinfo[0].extmetadata?.LicenseShortName?.value || null
// Check if the output folder exists
if (!existsSync(outputFolder)) {
const parts = outputFolder.split("/")
for (let i = 0; i < parts.length; i++) {
const part = parts.slice(0, i + 1).join("/")
if (!existsSync(part)) {
console.log(`Creating folder ${part}`)
mkdirSync(part)
}
}
}
// Check if the license is present
if (!license) {
console.log(
`${filename} does not have a license, falling back to checking template...`
)
const templateUrl = `${baseUrl}/w/api.php?action=query&format=json&prop=templates&titles=${filename}&tllimit=500`
const templateResponse = await fetch(templateUrl)
const templateDetails: TemplateQueryAPIResponse = await templateResponse.json()
// Loop through all templates and check if one of them is a license
const wikiPage =
templateDetails.query.pages[Object.keys(templateDetails.query.pages)[0]]
if (wikiPage.templates) {
for (const template of wikiPage.templates) {
if (templateMapping[template.title]) {
console.log(
`Found license ${templateMapping[template.title]} for ${filename}`
)
license = templateMapping[template.title]
}
}
}
// If no license was found, skip the file
if (!license) {
// Log in yellow
console.log(
`\x1b[33m%s\x1b[0m`,
`No license found for ${filename}, skipping...`
)
return
}
}
// Download the file and save it
const cleanFileName = unescape(filename).replace("File:", "")
console.log(
`Downloading ${cleanFileName} from ${fileUrl} and saving it to ${outputFolder}/${cleanFileName}...`
)
const fileResponse = await fetch(fileUrl)
const fileBuffer = await fileResponse.arrayBuffer()
const file = Buffer.from(fileBuffer)
const filePath = `${outputFolder}/${cleanFileName}`
writeFileSync(filePath, file)
// Save the license information
const licenseInfo: SmallLicense = {
path: cleanFileName,
license: licenseMapping[license] || license,
authors: [author],
sources: [wikiUrl],
}
const licensePath = `${outputFolder}/license_info.json`
if (!existsSync(licensePath)) {
// Create the file if it doesn't exist
writeFileSync(licensePath, JSON.stringify([licenseInfo], null, 2))
} else {
// Append to the file if it does exist
const licenseFile = await readFileSync(licensePath, "utf8")
const licenseData = JSON.parse(licenseFile)
licenseData.push(licenseInfo)
writeFileSync(licensePath, JSON.stringify(licenseData, null, 2))
}
} else {
console.log(`\x1b[33m%s\x1b[0m`, `${filename} does not have image info!, skipping...`)
}
}
}
main(process.argv.slice(2))

View file

@ -2,11 +2,13 @@ import Script from "./Script"
import { Utils } from "../src/Utils"
import { Eli, EliEntry } from "./@types/eli"
import fs from "fs"
import { BingRasterLayer } from "../src/UI/Map/BingRasterLayer"
class DownloadEli extends Script {
constructor() {
super("Downloads a fresh copy of the editor layer index, removes all unnecessary data.")
}
async main(args: string[]): Promise<void> {
const url = "https://osmlab.github.io/editor-layer-index/imagery.geojson"
// Target should use '.json' instead of '.geojson', as the latter cannot be imported by the build systems
@ -20,7 +22,18 @@ class DownloadEli extends Script {
if (props.type === "bing") {
// A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
continue
try {
const bing = await BingRasterLayer.get()
if (bing === "error") {
continue
}
delete props.default
props.category = "photo"
props.url = bing.properties.url.replace("%7Bquadkey%7D", "{quadkey}")
} catch (e) {
console.error("Could not fetch URL for bing", e)
continue
}
}
if (props.id === "MAPNIK") {

View file

@ -7,6 +7,7 @@ function genImages(dryrun = false) {
"add",
"addSmall",
"back",
"circle",
"blocked",
"brick_wall",
"brick_wall_raw",
@ -33,6 +34,7 @@ function genImages(dryrun = false) {
"duplicate",
"elevator",
"elevator_wheelchair",
"envelope",
"eye",
"filter",
"filter_disable",
@ -65,6 +67,7 @@ function genImages(dryrun = false) {
"mapillary_black",
"mastodon",
"min",
"move",
"move-arrows",
"move_confirm",
"move_not_allowed",
@ -72,23 +75,33 @@ function genImages(dryrun = false) {
"osm_logo_us",
"osm-logo-us",
"party",
"pencil",
"person",
"pin",
"plantnet_logo",
"plus",
"reload",
"resolved",
"ring",
"robot",
"scissors",
"search",
"search_disable",
"share",
"SocialImageForeground",
"speech_bubble",
"speech_bubble_black_outline",
"square",
"square_rounded",
"star",
"star_half",
"star_outline",
"teardrop",
"teardrop_with_hole_green",
"statistics",
"translate",
"triangle",
"up",
"Upload",
"wikidata",
"wikimedia-commons-white",

View file

@ -9,12 +9,17 @@ import {
DoesImageExist,
PrevalidateTheme,
ValidateLayer,
ValidateThemeAndLayers, ValidateThemeEnsemble,
ValidateThemeAndLayers,
ValidateThemeEnsemble,
} from "../src/Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
import { Conversion, DesugaringContext, DesugaringStep } from "../src/Models/ThemeConfig/Conversion/Conversion"
import {
Conversion,
DesugaringContext,
DesugaringStep,
} from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils"
import Script from "./Script"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
@ -49,7 +54,7 @@ class ParseLayer extends Conversion<
convert(
path: string,
context: ConversionContext,
context: ConversionContext
): {
parsed: LayerConfig
raw: LayerConfigJson
@ -65,7 +70,7 @@ class ParseLayer extends Conversion<
try {
parsed = JSON.parse(fileContents)
} catch (e) {
context.err("Could not parse file as JSON")
context.err("Could not parse file as JSON: " + e)
return undefined
}
if (parsed === undefined) {
@ -104,7 +109,7 @@ class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: Laye
const fixed = json.raw
const layerConfig = json.parsed
const pointRendering: PointRenderingConfig = layerConfig.mapRendering.find((pr) =>
pr.location.has("point"),
pr.location.has("point")
)
const defaultTags = layerConfig.GetBaseTags()
fixed["_layerIcon"] = Utils.NoNull(
@ -119,13 +124,12 @@ class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: Laye
result["color"] = c
}
return result
}),
})
)
return { raw: fixed, parsed: layerConfig }
}
}
class LayerOverviewUtils extends Script {
public static readonly layerPath = "./src/assets/generated/layers/"
public static readonly themePath = "./src/assets/generated/themes/"
@ -142,7 +146,7 @@ class LayerOverviewUtils extends Script {
private static extractLayerIdsFrom(
themeFile: LayoutConfigJson,
includeInlineLayers = true,
includeInlineLayers = true
): string[] {
const publicLayerIds = []
if (!Array.isArray(themeFile.layers)) {
@ -205,10 +209,10 @@ class LayerOverviewUtils extends Script {
| LayerConfigJson
| string
| {
builtin
}
)[]
}[],
builtin
}
)[]
}[]
) {
const perId = new Map<string, any>()
for (const theme of themes) {
@ -249,7 +253,7 @@ class LayerOverviewUtils extends Script {
writeFileSync(
"./src/assets/generated/theme_overview.json",
JSON.stringify(sorted, null, " "),
{ encoding: "utf8" },
{ encoding: "utf8" }
)
}
@ -261,7 +265,7 @@ class LayerOverviewUtils extends Script {
writeFileSync(
`${LayerOverviewUtils.themePath}${theme.id}.json`,
JSON.stringify(theme, null, " "),
{ encoding: "utf8" },
{ encoding: "utf8" }
)
}
@ -272,13 +276,13 @@ class LayerOverviewUtils extends Script {
writeFileSync(
`${LayerOverviewUtils.layerPath}${layer.id}.json`,
JSON.stringify(layer, null, " "),
{ encoding: "utf8" },
{ encoding: "utf8" }
)
}
getSharedTagRenderings(
doesImageExist: DoesImageExist,
bootstrapTagRenderings: Map<string, QuestionableTagRenderingConfigJson> = null,
bootstrapTagRenderings: Map<string, QuestionableTagRenderingConfigJson> = null
): Map<string, QuestionableTagRenderingConfigJson> {
const prepareLayer = new PrepareLayer({
tagRenderings: bootstrapTagRenderings,
@ -339,8 +343,8 @@ class LayerOverviewUtils extends Script {
if (contents.indexOf("<text") > 0) {
console.warn(
"The SVG at " +
path +
" contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path",
path +
" contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path"
)
errCount++
}
@ -352,11 +356,19 @@ class LayerOverviewUtils extends Script {
async main(args: string[]) {
console.log("Generating layer overview...")
const themeWhitelist = new Set(args.find(a => a.startsWith("--themes="))
?.substring("--themes=".length)?.split(",") ?? [])
const themeWhitelist = new Set(
args
.find((a) => a.startsWith("--themes="))
?.substring("--themes=".length)
?.split(",") ?? []
)
const layerWhitelist = new Set(args.find(a => a.startsWith("--layers="))
?.substring("--layers=".length)?.split(",") ?? [])
const layerWhitelist = new Set(
args
.find((a) => a.startsWith("--layers="))
?.substring("--layers=".length)
?.split(",") ?? []
)
const forceReload = args.some((a) => a == "--force")
@ -384,19 +396,19 @@ class LayerOverviewUtils extends Script {
sharedLayers,
recompiledThemes,
forceReload,
themeWhitelist,
themeWhitelist
)
new ValidateThemeEnsemble().convertStrict(
Array.from(sharedThemes.values()).map(th => new LayoutConfig(th, true)))
Array.from(sharedThemes.values()).map((th) => new LayoutConfig(th, true))
)
if (recompiledThemes.length > 0) {
writeFileSync(
"./src/assets/generated/known_layers.json",
JSON.stringify({
layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"),
}),
})
)
}
@ -417,7 +429,7 @@ class LayerOverviewUtils extends Script {
const proto: LayoutConfigJson = JSON.parse(
readFileSync("./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", {
encoding: "utf8",
}),
})
)
const protolayer = <LayerConfigJson>(
proto.layers.filter((l) => l["id"] === "mapcomplete-changes")[0]
@ -434,12 +446,12 @@ class LayerOverviewUtils extends Script {
layers: ScriptUtils.getLayerFiles().map((f) => f.parsed),
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed),
},
ConversionContext.construct([], []),
ConversionContext.construct([], [])
)
for (const [_, theme] of sharedThemes) {
theme.layers = theme.layers.filter(
(l) => Constants.added_by_default.indexOf(l["id"]) < 0,
(l) => Constants.added_by_default.indexOf(l["id"]) < 0
)
}
@ -448,21 +460,19 @@ class LayerOverviewUtils extends Script {
"./src/assets/generated/known_themes.json",
JSON.stringify({
themes: Array.from(sharedThemes.values()),
}),
})
)
}
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
console.error(
"This was a bootstrapping-run. Run generate layeroverview again!"
)
console.error("This was a bootstrapping-run. Run generate layeroverview again!")
}
}
private parseLayer(
doesImageExist: DoesImageExist,
prepLayer: PrepareLayer,
sharedLayerPath: string,
sharedLayerPath: string
): {
raw: LayerConfigJson
parsed: LayerConfig
@ -473,7 +483,7 @@ class LayerOverviewUtils extends Script {
const parsed = parser.convertStrict(sharedLayerPath, context)
const result = AddIconSummary.singleton.convertStrict(
parsed,
context.inOperation("AddIconSummary"),
context.inOperation("AddIconSummary")
)
return { ...result, context }
}
@ -481,7 +491,7 @@ class LayerOverviewUtils extends Script {
private buildLayerIndex(
doesImageExist: DoesImageExist,
forceReload: boolean,
whitelist: Set<string>,
whitelist: Set<string>
): Map<string, LayerConfigJson> {
// First, we expand and validate all builtin layers. These are written to src/assets/generated/layers
// At the same time, an index of available layers is built.
@ -501,7 +511,10 @@ class LayerOverviewUtils extends Script {
for (const sharedLayerPath of ScriptUtils.getLayerPaths()) {
if (whitelist.size > 0) {
const idByPath = sharedLayerPath.split("/").at(-1).split(".")[0]
if (Constants.priviliged_layers.indexOf(<any>idByPath) < 0 && !whitelist.has(idByPath)) {
if (
Constants.priviliged_layers.indexOf(<any>idByPath) < 0 &&
!whitelist.has(idByPath)
) {
continue
}
}
@ -533,17 +546,17 @@ class LayerOverviewUtils extends Script {
console.log(
"Recompiled layers " +
recompiledLayers.join(", ") +
" and skipped " +
skippedLayers.length +
" layers. Detected " +
warningCount +
" warnings",
recompiledLayers.join(", ") +
" and skipped " +
skippedLayers.length +
" layers. Detected " +
warningCount +
" warnings"
)
// We always need the calculated tags of 'usersettings', so we export them separately
this.extractJavascriptCodeForLayer(
state.sharedLayers.get("usersettings"),
"./src/Logic/State/UserSettingsMetaTagging.ts",
"./src/Logic/State/UserSettingsMetaTagging.ts"
)
return sharedLayers
@ -560,8 +573,8 @@ class LayerOverviewUtils extends Script {
private extractJavascriptCode(themeFile: LayoutConfigJson) {
const allCode = [
"import {Feature} from 'geojson'",
"import { ExtraFuncType } from \"../../../Logic/ExtraFunctions\";",
"import { Utils } from \"../../../Utils\"",
'import { ExtraFuncType } from "../../../Logic/ExtraFunctions";',
'import { Utils } from "../../../Utils"',
"export class ThemeMetaTagging {",
" public static readonly themeName = " + JSON.stringify(themeFile.id),
"",
@ -573,8 +586,8 @@ class LayerOverviewUtils extends Script {
allCode.push(
" public metaTaggging_for_" +
id +
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {",
id +
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {"
)
allCode.push(" const {" + ExtraFunctions.types.join(", ") + "} = helperFunctions")
for (const line of code) {
@ -585,10 +598,10 @@ class LayerOverviewUtils extends Script {
if (!isStrict) {
allCode.push(
" Utils.AddLazyProperty(feat.properties, '" +
attributeName +
"', () => " +
expression +
" ) ",
attributeName +
"', () => " +
expression +
" ) "
)
} else {
attributeName = attributeName.substring(0, attributeName.length - 1).trim()
@ -633,7 +646,7 @@ class LayerOverviewUtils extends Script {
const code = l.calculatedTags ?? []
allCode.push(
" public metaTaggging_for_" + l.id + "(feat: {properties: Record<string, string>}) {",
" public metaTaggging_for_" + l.id + "(feat: {properties: Record<string, string>}) {"
)
for (const line of code) {
const firstEq = line.indexOf("=")
@ -643,10 +656,10 @@ class LayerOverviewUtils extends Script {
if (!isStrict) {
allCode.push(
" Utils.AddLazyProperty(feat.properties, '" +
attributeName +
"', () => " +
expression +
" ) ",
attributeName +
"', () => " +
expression +
" ) "
)
} else {
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
@ -671,20 +684,20 @@ class LayerOverviewUtils extends Script {
sharedLayers: Map<string, LayerConfigJson>,
recompiledThemes: string[],
forceReload: boolean,
whitelist: Set<string>,
whitelist: Set<string>
): Map<string, LayoutConfigJson> {
console.log(" ---------- VALIDATING BUILTIN THEMES ---------")
const themeFiles = ScriptUtils.getThemeFiles()
const fixed = new Map<string, LayoutConfigJson>()
const publicLayers = LayerOverviewUtils.publicLayerIdsFrom(
themeFiles.map((th) => th.parsed),
themeFiles.map((th) => th.parsed)
)
const convertState: DesugaringContext = {
sharedLayers,
tagRenderings: this.getSharedTagRenderings(
new DoesImageExist(licensePaths, existsSync),
new DoesImageExist(licensePaths, existsSync)
),
publicLayers,
}
@ -717,15 +730,15 @@ class LayerOverviewUtils extends Script {
LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
const usedLayers = Array.from(
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false),
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
fixed.set(
themeFile.id,
JSON.parse(
readFileSync(LayerOverviewUtils.themePath + themeFile.id + ".json", "utf8"),
),
readFileSync(LayerOverviewUtils.themePath + themeFile.id + ".json", "utf8")
)
)
ScriptUtils.erasableLog("Skipping", themeFile.id)
skippedThemes.push(themeFile.id)
@ -736,23 +749,23 @@ class LayerOverviewUtils extends Script {
new PrevalidateTheme().convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"]),
ConversionContext.construct([themePath], ["PrepareLayer"])
)
try {
themeFile = new PrepareTheme(convertState, {
skipDefaultLayers: true,
}).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"]),
ConversionContext.construct([themePath], ["PrepareLayer"])
)
new ValidateThemeAndLayers(
new DoesImageExist(licensePaths, existsSync, knownTagRenderings),
themePath,
true,
knownTagRenderings,
knownTagRenderings
).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"]),
ConversionContext.construct([themePath], ["PrepareLayer"])
)
if (themeFile.icon.endsWith(".svg")) {
@ -807,16 +820,16 @@ class LayerOverviewUtils extends Script {
.OnEveryLanguage((s) => parse_html(s).textContent).translations,
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
}
}),
})
)
}
console.log(
"Recompiled themes " +
recompiledThemes.join(", ") +
" and skipped " +
skippedThemes.length +
" themes",
recompiledThemes.join(", ") +
" and skipped " +
skippedThemes.length +
" themes"
)
return fixed

View file

@ -16,6 +16,8 @@ import * as crypto from "crypto"
import * as eli from "../src/assets/editor-layer-index.json"
import * as eli_global from "../src/assets/global-raster-layers.json"
import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtils"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8")
@ -271,7 +273,6 @@ async function generateCsp(
}
): Promise<string> {
const apiUrls: string[] = [
"'self'",
...Constants.defaultOverpassUrls,
Constants.countryCoderEndpoint,
Constants.nominatimEndpoint,
@ -282,19 +283,24 @@ async function generateCsp(
SpecialVisualizations.specialVisualizations.forEach((sv) => {
if (typeof sv.needsUrls === "function") {
// Handled below
return
}
apiUrls.push(...(sv.needsUrls ?? []))
})
const usedSpecialVisualisations = ValidationUtils.getSpecialVisualisationsWithArgs(layoutJson)
const usedSpecialVisualisations = [].concat(...layoutJson.layers.map(l => ValidationUtils.getAllSpecialVisualisations(<QuestionableTagRenderingConfigJson[]> (<LayerConfigJson>l).tagRenderings ?? [])))
for (const usedSpecialVisualisation of usedSpecialVisualisations) {
if (typeof usedSpecialVisualisation === "string") {
continue
}
const neededUrls = usedSpecialVisualisation.func.needsUrls ?? []
if (typeof neededUrls === "function") {
apiUrls.push(...neededUrls(usedSpecialVisualisation.args))
let needed: string | string[] = neededUrls(usedSpecialVisualisation.args)
if(typeof needed === "string"){
needed = [needed]
}
apiUrls.push(...needed)
}
}
@ -306,11 +312,14 @@ async function generateCsp(
const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector")
const vectorSources = vectorLayers.map((l) => l.properties.url)
apiUrls.push(...vectorSources)
for (const connectSource of apiUrls.concat(geojsonSources)) {
for (let connectSource of apiUrls.concat(geojsonSources)) {
if (!connectSource) {
continue
}
try {
if(!connectSource.startsWith("http")){
connectSource = "https://"+connectSource
}
const url = new URL(connectSource)
hosts.add("https://" + url.host)
} catch (e) {
@ -340,7 +349,7 @@ async function generateCsp(
"default-src": "'self'",
"child-src": "'self' blob: ",
"img-src": "* data:", // maplibre depends on 'data:' to load
"connect-src": connectSrc.join(" "),
"connect-src": "'self' "+connectSrc.join(" "),
"report-to": "https://report.mapcomplete.org/csp",
"worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob'
"style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours

View file

@ -5,7 +5,6 @@ import { readFileSync, writeFileSync } from "fs"
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import { Utils } from "../src/Utils"
import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig"
/**
* Generates all the files in "Docs/TagInfo". These are picked up by the taginfo project, showing a link to the mapcomplete theme if the key is used

View file

@ -11,9 +11,9 @@
cp config.json config.json.bu &&
cp ./scripts/hetzner/config.json . && # Copy the config _before_ building, as the config might contain some needed URLs
# npm run reset:layeroverview
# npm run test &&
# npm run prepare-deploy &&
npm run reset:layeroverview
npm run test &&
npm run prepare-deploy &&
zip dist.zip -r dist/* &&
mv config.json.bu config.json &&
scp ./scripts/hetzner/config/* hetzner:/root/ &&

View file

@ -30,17 +30,6 @@ t.OnEveryLanguage((txt, ln) => {
return txt
})
const articles = {
/* de: "eine",
es: 'una',
fr: 'une',
it: 'una',
nb_NO: 'en',
nl: 'een',
pt: 'uma',
pt_BR : 'uma',//*/
}
function reorder(object: object, order: string[]) {
const allKeys = new Set<string>(Object.keys(object))
const copy = {}
@ -54,38 +43,6 @@ function reorder(object: object, order: string[]) {
return copy
}
function addArticleToPresets(layerConfig: { presets?: { title: any }[] }) {
/*
if(layerConfig.presets === undefined){
return
}
for (const preset of layerConfig.presets) {
preset.title = new Translation(preset.title, "autofix")
.OnEveryLanguage((txt, lang) => {
let article = articles[lang]
if(lang === "en"){
if(["a","e","u","o","i"].some(vowel => txt.toLowerCase().startsWith(vowel))) {
article = "an"
}else{
article = "a"
}
}
if(article === undefined){
return txt;
}
if(txt.startsWith(article+" ")){
return txt;
}
if(txt.startsWith("an ")){
return txt;
}
return article +" " + txt.toLowerCase();
})
.translations
}
//*/
}
const layerFiles = ScriptUtils.getLayerFiles()
for (const layerFile of layerFiles) {
try {
@ -95,7 +52,6 @@ for (const layerFile of layerFiles) {
ConversionContext.construct([layerFile.path.split("/").at(-1)], ["update legacy"])
)
)
addArticleToPresets(fixed)
const reordered = reorder(fixed, layerAttributesOrder)
writeFileSync(layerFile.path, JSON.stringify(reordered, null, " ") + "\n")
} catch (e) {
@ -110,11 +66,7 @@ for (const themeFile of themeFiles) {
themeFile.parsed,
ConversionContext.construct([themeFile.path.split("/").at(-1)], ["update legacy layer"])
)
for (const layer of fixed.layers) {
if (layer["presets"] !== undefined) {
addArticleToPresets(<any>layer)
}
}
// extractInlineLayer(fixed)
const endsWithNewline = themeFile.raw.at(-1) === "\n"
const ordered = reorder(fixed, themeAttributesOrder)

View file

@ -0,0 +1,72 @@
import Script from "../Script"
import fs from "fs"
import { Feature, FeatureCollection } from "geojson"
import { GeoOperations } from "../../src/Logic/GeoOperations"
import * as os from "os"
// vite-node scripts/velopark/compare.ts -- scripts/velopark/velopark_all_2024-02-14T12\:18\:41.772Z.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\ Fietsenstallingen_OSM_met_velopark_ref.geojson
class Compare extends Script {
compare(veloId: string, osmParking: Feature, veloParking: Feature): {distance: number, ref: string, osmid: string, diffs: {
osm: string, velopark: string, key: string
}[] }{
const osmCenterpoint = GeoOperations.centerpointCoordinates(osmParking)
const veloparkCenterpoint = GeoOperations.centerpointCoordinates(veloParking)
const distance = Math.round(GeoOperations.distanceBetween(osmCenterpoint, veloparkCenterpoint))
const diffs: { osm: string, velopark: string, key: string}[] = []
const allKeys = new Set<string>(Object.keys(osmParking.properties).concat(Object.keys(veloParking.properties)))
for (const key of allKeys) {
if(osmParking.properties[key] === veloParking.properties[key]){
continue
}
if(Number(osmParking.properties[key]) === veloParking.properties[key]){
continue
}
if(veloParking.properties[key] === undefined){
continue
}
diffs.push({
key,
osm: osmParking.properties[key],
velopark: veloParking.properties[key]
})
}
return {
ref: veloId,
osmid: osmParking.properties["@id"],
distance, diffs
}
}
async main(args: string[]): Promise<void> {
let [velopark, osm, key] = args
key ??= "ref:velopark"
const veloparkData: FeatureCollection = JSON.parse(fs.readFileSync(velopark, "utf-8"))
const osmData : FeatureCollection = JSON.parse(fs.readFileSync(osm, "utf-8"))
const veloparkById : Record<string, Feature> = {}
for (const parking of veloparkData.features) {
veloparkById[parking.properties[key]] = parking
}
const diffs = []
for (const parking of osmData.features) {
const veloId = parking.properties[key]
const veloparking = veloparkById[veloId]
if(veloparking === undefined){
console.error("No velopark entry found for", veloId)
continue
}
diffs.push(this.compare(veloId, parking, veloparking))
}
fs.writeFileSync("report_diff.json",JSON.stringify(diffs))
}
constructor() {
super("Compares a velopark geojson with OSM geojson. Usage: `compare velopark.geojson osm.geojson [key-to-compare-on]`. If key-to-compare-on is not given, `ref:velopark` will be used")
}
}
new Compare().run()

View file

@ -0,0 +1,75 @@
import Script from "../Script"
import { Utils } from "../../src/Utils"
import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader"
import fs from "fs"
import { Overpass } from "../../src/Logic/Osm/Overpass"
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
import Constants from "../../src/Models/Constants"
import { ImmutableStore } from "../../src/Logic/UIEventSource"
import { BBox } from "../../src/Logic/BBox"
class VeloParkToGeojson extends Script {
constructor() {
super(
"Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory"
)
}
exportTo(filename: string, features){
fs.writeFileSync(
filename+"_" + new Date().toISOString() + ".geojson",
JSON.stringify(
{
type: "FeatureCollection",
features,
},
null,
" "
)
)
}
async main(args: string[]): Promise<void> {
console.log("Downloading velopark data")
// Download data for NIS-code 1000. 1000 means: all of belgium
const url = "https://www.velopark.be/api/parkings/1000"
const data = <VeloparkData[]>await Utils.downloadJson(url)
const bboxBelgium = new BBox([
[2.51357303225, 49.5294835476],
[6.15665815596, 51.4750237087],
])
const alreadyLinkedQuery = new Overpass(
new RegexTag("ref:velopark", /.+/),
[],
Constants.defaultOverpassUrls[0],
new ImmutableStore(60 * 5),
false
)
const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium)
const seenIds = new Set<string>(
alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"])
)
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
const allVelopark = data.map((f) => VeloparkLoader.convert(f))
this.exportTo("velopark_all", allVelopark)
const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))
const allProperties = new Set<string>()
for (const feature of features) {
Object.keys(feature.properties).forEach((k) => allProperties.add(k))
}
this.exportTo("velopark_noncynced",features)
allProperties.delete("ref:velopark")
for (const feature of features) {
allProperties.forEach((k) => {
delete feature.properties[k]
})
}
this.exportTo("velopark_nonsynced_id_only", features)
}
}
new VeloParkToGeojson().run()