forked from MapComplete/MapComplete
Scripts(community_index): create script to update community index files, create weekly data maintenance script
This commit is contained in:
parent
36a9b49c66
commit
7bddaa7d4c
11 changed files with 412 additions and 141 deletions
31
.forgejo/workflows/update_community_index.yml
Normal file
31
.forgejo/workflows/update_community_index.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: Weekly data updates
|
||||
on:
|
||||
schedule:
|
||||
-cron: "* * * * 1"
|
||||
|
||||
jobs:
|
||||
deploy_on_hetzner_single:
|
||||
runs-on: [ ubuntu-latest, hetzner-access ]
|
||||
|
||||
runs-on: [ ubuntu-latest, hetzner-access ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: install deps
|
||||
run: npm ci
|
||||
shell: bash
|
||||
|
||||
- name: update community index files
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir community-index
|
||||
npm run download:community-index -- community-index/
|
||||
zip community-index.zip community-index/*
|
||||
scp community-index.zip hetzner:data/
|
|
@ -119,6 +119,7 @@
|
|||
"download:editor-layer-index": "vite-node scripts/downloadEli.ts",
|
||||
"download:stats": "vite-node scripts/GenerateSeries.ts",
|
||||
"download:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/",
|
||||
"download:community-index": "vite-node scripts/downloadCommunityIndex.ts -- /tmp/test",
|
||||
"weblate:add-upstream": "git remote add weblate https://translate.mapcomplete.org/git/mapcomplete/core/ ; git remote update weblate",
|
||||
"weblate:fix": "npm run weblate:add-upstream && git merge weblate/master && git rebase origin/master && git push origin master",
|
||||
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:themes",
|
||||
|
|
116
scripts/downloadCommunityIndex.ts
Normal file
116
scripts/downloadCommunityIndex.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import Script from "./Script"
|
||||
import { CommunityResource } from "../src/Logic/Web/CommunityIndex"
|
||||
import { Utils } from "../src/Utils"
|
||||
import { FeatureCollection, MultiPolygon, Polygon } from "geojson"
|
||||
import { writeFileSync } from "fs"
|
||||
import { GeoOperations } from "../src/Logic/GeoOperations"
|
||||
import { Tiles } from "../src/Models/TileRange"
|
||||
import ScriptUtils from "./ScriptUtils"
|
||||
|
||||
class DownloadCommunityIndex extends Script {
|
||||
|
||||
constructor() {
|
||||
super("Updates the community index")
|
||||
}
|
||||
|
||||
printHelp() {
|
||||
console.log("Arguments are:\noutputdirectory")
|
||||
}
|
||||
|
||||
private static targetZoomlevel: number = 6
|
||||
private static upstreamUrl: string = "https://raw.githubusercontent.com/osmlab/osm-community-index/main/dist/"
|
||||
|
||||
/**
|
||||
* Prunes away unnecessary fields from a CommunityResource
|
||||
* @private
|
||||
*/
|
||||
private static stripResource(r: Readonly<CommunityResource>): CommunityResource {
|
||||
return {
|
||||
id: r.id,
|
||||
languageCodes: r.languageCodes,
|
||||
account: r.account,
|
||||
type: r.type,
|
||||
resolved: {
|
||||
name: r.resolved.name,
|
||||
description: r.resolved.description,
|
||||
url: r.resolved.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static stripResourcesObj(resources: Readonly<Record<string, Readonly<CommunityResource>>>) {
|
||||
const stripped: Record<string, CommunityResource> = {}
|
||||
for (const k in resources) {
|
||||
stripped[k] = DownloadCommunityIndex.stripResource(resources[k])
|
||||
}
|
||||
return stripped
|
||||
}
|
||||
|
||||
public static async update(targetDirectory: string) {
|
||||
const data = await Utils.downloadJson<FeatureCollection<Polygon | MultiPolygon, {
|
||||
resources: Record<string, CommunityResource>,
|
||||
nameEn: string,
|
||||
id: string
|
||||
}>>(DownloadCommunityIndex.upstreamUrl + "completeFeatureCollection.json"
|
||||
)
|
||||
const features = data.features
|
||||
const global = features.find(
|
||||
f => f.id === "Q2"
|
||||
)
|
||||
const globalProperties = DownloadCommunityIndex.stripResourcesObj(global.properties.resources)
|
||||
writeFileSync(targetDirectory + "/global.json", JSON.stringify(globalProperties), "utf8")
|
||||
console.log("Written global properties")
|
||||
|
||||
const types = new Set<string>()
|
||||
for (const f of features) {
|
||||
const res = f.properties.resources
|
||||
for (const k in res) {
|
||||
types.add(res[k].type)
|
||||
}
|
||||
}
|
||||
for (const type of types) {
|
||||
const url = `${DownloadCommunityIndex.upstreamUrl}img/${type}.svg`
|
||||
await ScriptUtils.DownloadFileTo(url, `${targetDirectory}/${type}.svg`)
|
||||
}
|
||||
const local = features.filter(f => f.id !== "Q2")
|
||||
const spread = GeoOperations.spreadIntoBboxes(local, DownloadCommunityIndex.targetZoomlevel)
|
||||
let written = 0
|
||||
let skipped = 0
|
||||
writeFileSync(targetDirectory + "local.geojson", JSON.stringify({ type: "FeatureCollection", features: local }))
|
||||
for (const tileIndex of spread.keys()) {
|
||||
const features = spread.get(tileIndex)
|
||||
const clipped = GeoOperations.clipAllInBox(features, tileIndex)
|
||||
if (clipped.length === 0) {
|
||||
skipped++
|
||||
features.push(Tiles.asGeojson(tileIndex))
|
||||
writeFileSync(`${targetDirectory + tileIndex}_skipped.geojson`, JSON.stringify({
|
||||
type: "FeatureCollection", features
|
||||
}))
|
||||
continue
|
||||
}
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
const path = `${targetDirectory}/tile_${z}_${x}_${y}.geojson`
|
||||
clipped.forEach((f) => {
|
||||
delete f.bbox
|
||||
})
|
||||
writeFileSync(path, JSON.stringify({ type: "FeatureCollection", features: clipped }), "utf8")
|
||||
written++
|
||||
console.log(`Written tile ${path}`)
|
||||
}
|
||||
console.log(`Created ${written} tiles, skipped ${skipped}`)
|
||||
}
|
||||
|
||||
|
||||
async main(args: string[]): Promise<void> {
|
||||
const path = args[0]
|
||||
if (!path) {
|
||||
this.printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
await DownloadCommunityIndex.update(path)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
new DownloadCommunityIndex().run()
|
|
@ -1,13 +1,12 @@
|
|||
import * as fs from "fs"
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import * as readline from "readline"
|
||||
import ScriptUtils from "./ScriptUtils"
|
||||
import { Utils } from "../Utils"
|
||||
import Script from "./Script"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Feature } from "geojson"
|
||||
import { features } from "monaco-editor/esm/metadata"
|
||||
|
||||
/**
|
||||
* This script slices a big newline-delimeted geojson file into tiled geojson
|
||||
|
@ -96,34 +95,15 @@ class Slice extends Script {
|
|||
features: Feature[],
|
||||
tileIndex: number,
|
||||
outputDirectory: string,
|
||||
doSlice: boolean,
|
||||
doClip: boolean,
|
||||
handled: number,
|
||||
maxNumberOfTiles: number
|
||||
) {
|
||||
): boolean {
|
||||
if (doClip) {
|
||||
features = GeoOperations.clipAllInBox(features, tileIndex)
|
||||
}
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
const path = `${outputDirectory}/tile_${z}_${x}_${y}.geojson`
|
||||
const box = BBox.fromTileIndex(tileIndex)
|
||||
if (doSlice) {
|
||||
features = Utils.NoNull(
|
||||
features.map((f) => {
|
||||
const bbox = box.asGeoJson({})
|
||||
const properties = {
|
||||
...f.properties,
|
||||
id: (f.properties?.id ?? "") + "_" + z + "_" + x + "_" + y,
|
||||
}
|
||||
|
||||
if (GeoOperations.completelyWithin(bbox, <any>f)) {
|
||||
bbox.properties = properties
|
||||
return bbox
|
||||
}
|
||||
const intersection = GeoOperations.intersect(f, box.asGeoJson({}))
|
||||
if (intersection) {
|
||||
intersection.properties = properties
|
||||
}
|
||||
return intersection
|
||||
})
|
||||
)
|
||||
}
|
||||
features.forEach((f) => {
|
||||
delete f.bbox
|
||||
})
|
||||
|
@ -177,7 +157,7 @@ class Slice extends Script {
|
|||
}
|
||||
console.log("Using directory ", outputDirectory)
|
||||
|
||||
let allFeatures: any[]
|
||||
let allFeatures: Feature[]
|
||||
if (inputFile.endsWith(".geojson")) {
|
||||
console.log("Detected geojson")
|
||||
allFeatures = await this.readFeaturesFromGeoJson(inputFile)
|
||||
|
@ -202,18 +182,16 @@ class Slice extends Script {
|
|||
}
|
||||
const maxNumberOfTiles = Math.pow(2, zoomlevel) * Math.pow(2, zoomlevel)
|
||||
let handled = 0
|
||||
StaticFeatureSource.fromGeojson(allFeatures).features.addCallbackAndRun((feats) => {
|
||||
GeoOperations.slice(zoomlevel, feats).forEach((tileData, tileIndex) => {
|
||||
handled = handled + 1
|
||||
this.handleTileData(
|
||||
tileData,
|
||||
tileIndex,
|
||||
outputDirectory,
|
||||
doSlice,
|
||||
handled,
|
||||
maxNumberOfTiles
|
||||
)
|
||||
})
|
||||
GeoOperations.slice(zoomlevel, features).forEach((tileData, tileIndex) => {
|
||||
handled = handled + 1
|
||||
this.handleTileData(
|
||||
tileData,
|
||||
tileIndex,
|
||||
outputDirectory,
|
||||
doSlice,
|
||||
handled,
|
||||
maxNumberOfTiles
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ export default class SaveFeatureSourceToLocalStorage {
|
|||
this.storage = storage
|
||||
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
|
||||
features.features.addCallbackAndRunD((features) => {
|
||||
const sliced = GeoOperations.slice(zoomlevel, features)
|
||||
const sliced = GeoOperations.spreadIntoBboxes(features, zoomlevel)
|
||||
|
||||
sliced.forEach((features, tileIndex) => {
|
||||
let tileSaver = singleTileSavers.get(tileIndex)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BBox } from "./BBox"
|
||||
import * as turf from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord, Polygon } from "@turf/turf"
|
||||
import {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
|
@ -9,13 +9,13 @@ import {
|
|||
MultiLineString,
|
||||
MultiPolygon,
|
||||
Point,
|
||||
Polygon,
|
||||
Position,
|
||||
Position
|
||||
} from "geojson"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Utils } from "../Utils"
|
||||
import { NearestPointOnLine } from "@turf/nearest-point-on-line"
|
||||
;("use strict")
|
||||
|
||||
("use strict")
|
||||
|
||||
export class GeoOperations {
|
||||
private static readonly _earthRadius = 6378137
|
||||
|
@ -29,7 +29,7 @@ export class GeoOperations {
|
|||
"behind",
|
||||
"sharp_left",
|
||||
"left",
|
||||
"slight_left",
|
||||
"slight_left"
|
||||
] as const
|
||||
private static reverseBearing = {
|
||||
N: 0,
|
||||
|
@ -47,7 +47,7 @@ export class GeoOperations {
|
|||
W: 270,
|
||||
WNW: 292.5,
|
||||
NW: 315,
|
||||
NNW: 337.5,
|
||||
NNW: 337.5
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,8 +61,8 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
public static intersect(
|
||||
f0: Feature<Polygon | MultiPolygon>,
|
||||
f1: Feature<Polygon | MultiPolygon>
|
||||
f0: Readonly<Feature<Polygon | MultiPolygon>>,
|
||||
f1: Readonly<Feature<Polygon | MultiPolygon>>
|
||||
): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.intersect(f0, f1)
|
||||
}
|
||||
|
@ -309,7 +309,7 @@ export class GeoOperations {
|
|||
bufferSizeInMeter: number
|
||||
): Feature<Polygon | MultiPolygon> | FeatureCollection<Polygon | MultiPolygon> {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: "kilometers",
|
||||
units: "kilometers"
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -325,9 +325,9 @@ export class GeoOperations {
|
|||
[lon0, lat],
|
||||
[lon0, lat0],
|
||||
[lon, lat0],
|
||||
[lon, lat],
|
||||
],
|
||||
},
|
||||
[lon, lat]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,9 +368,9 @@ export class GeoOperations {
|
|||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
coordinates: way.geometry.coordinates[0]
|
||||
},
|
||||
properties: way.properties,
|
||||
properties: way.properties
|
||||
}
|
||||
}
|
||||
if (way.geometry.type === "MultiPolygon") {
|
||||
|
@ -378,9 +378,9 @@ export class GeoOperations {
|
|||
type: "Feature",
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
coordinates: way.geometry.coordinates[0]
|
||||
},
|
||||
properties: way.properties,
|
||||
properties: way.properties
|
||||
}
|
||||
}
|
||||
if (way.geometry.type === "LineString") {
|
||||
|
@ -512,6 +512,8 @@ export class GeoOperations {
|
|||
/**
|
||||
* Given a list of features, will construct a map of slippy map tile-indices.
|
||||
* Features of which the BBOX overlaps with the corresponding slippy map tile are added to the corresponding array
|
||||
*
|
||||
* Also @see clipAllInBox
|
||||
* @param features
|
||||
* @param zoomlevel
|
||||
*/
|
||||
|
@ -535,6 +537,33 @@ export class GeoOperations {
|
|||
return perBbox
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of features, returns a new list of features so that the features are clipped into the given tile-index.
|
||||
* Note: IDs are rewritten
|
||||
* Also @see spreadIntoBBoxes
|
||||
*/
|
||||
public static clipAllInBox(features: ReadonlyArray<Readonly<Feature>>, tileIndex: number): Feature[] {
|
||||
const bbox = Tiles.asGeojson(tileIndex)
|
||||
const newFeatures: Feature[] = []
|
||||
for (const f of features) {
|
||||
const intersectionParts = GeoOperations.clipWith(f, bbox)
|
||||
for (let i = 0; i < intersectionParts.length; i++) {
|
||||
const intersectionPart = intersectionParts[i]
|
||||
let id = (f.properties?.id ?? "") + "_" + tileIndex
|
||||
if (i > 0) {
|
||||
id += "_part_" + i
|
||||
}
|
||||
const properties = {
|
||||
...f.properties,
|
||||
id
|
||||
}
|
||||
intersectionPart.properties = properties
|
||||
newFeatures.push(intersectionPart)
|
||||
}
|
||||
}
|
||||
return Utils.NoNull(newFeatures)
|
||||
}
|
||||
|
||||
public static toGpx(
|
||||
locations:
|
||||
| Feature<LineString>
|
||||
|
@ -558,8 +587,8 @@ export class GeoOperations {
|
|||
properties: {},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: p,
|
||||
},
|
||||
coordinates: p
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -575,7 +604,7 @@ export class GeoOperations {
|
|||
trackPoints.push(trkpt)
|
||||
}
|
||||
const header =
|
||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||
return (
|
||||
header +
|
||||
"\n<name>" +
|
||||
|
@ -614,7 +643,7 @@ export class GeoOperations {
|
|||
trackPoints.push(trkpt)
|
||||
}
|
||||
const header =
|
||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||
return (
|
||||
header +
|
||||
"\n<name>" +
|
||||
|
@ -640,7 +669,7 @@ export class GeoOperations {
|
|||
|
||||
const copy = {
|
||||
...feature,
|
||||
geometry: { ...feature.geometry },
|
||||
geometry: { ...feature.geometry }
|
||||
}
|
||||
let coordinates: [number, number][]
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
@ -698,8 +727,8 @@ export class GeoOperations {
|
|||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [a, b],
|
||||
},
|
||||
coordinates: [a, b]
|
||||
}
|
||||
},
|
||||
distanceMeter,
|
||||
{ units: "meters" }
|
||||
|
@ -736,17 +765,26 @@ export class GeoOperations {
|
|||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(
|
||||
feature: Feature,
|
||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon>
|
||||
feature: Readonly<Feature>,
|
||||
possiblyEnclosingFeature: Readonly<Feature<Polygon | MultiPolygon>>
|
||||
): boolean {
|
||||
if (feature.geometry.type === "MultiPolygon") {
|
||||
const polygons = feature.geometry.coordinates.map(coordinates =>
|
||||
<Feature<Polygon>>{
|
||||
type: "Feature", geometry: {
|
||||
type: "Polygon", coordinates
|
||||
}
|
||||
})
|
||||
return !polygons.some(polygon => !booleanWithin(polygon, possiblyEnclosingFeature))
|
||||
}
|
||||
return booleanWithin(feature, possiblyEnclosingFeature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an intersection between two features.
|
||||
* One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
|
||||
* One or multiple new feature are returned based on 'toSplit', which'll have a geometry that is completely withing boundary
|
||||
*/
|
||||
public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] {
|
||||
public static clipWith(toSplit: Readonly<Feature>, boundary: Readonly<Feature<Polygon>>): Feature[] {
|
||||
if (toSplit.geometry.type === "Point") {
|
||||
const p = <Feature<Point>>toSplit
|
||||
if (GeoOperations.inside(<[number, number]>p.geometry.coordinates, boundary)) {
|
||||
|
@ -757,9 +795,9 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (toSplit.geometry.type === "LineString") {
|
||||
const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary)
|
||||
const kept = []
|
||||
for (const f of splitup.features) {
|
||||
const splitup: Feature<LineString>[] = turf.lineSplit(<Feature<LineString>>toSplit, boundary).features
|
||||
const kept: Feature[] = []
|
||||
for (const f of splitup) {
|
||||
if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) {
|
||||
continue
|
||||
}
|
||||
|
@ -787,7 +825,24 @@ export class GeoOperations {
|
|||
return kept
|
||||
}
|
||||
if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") {
|
||||
|
||||
const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary)
|
||||
if (splitup === null) {
|
||||
// No intersection found.
|
||||
// Either: the boundary is contained fully in 'toSplit', 'toSplit' is contained fully in 'boundary' or they are unrelated at all
|
||||
if (GeoOperations.completelyWithin(toSplit, boundary)) {
|
||||
return [toSplit]
|
||||
}
|
||||
if (GeoOperations.completelyWithin(boundary, <Feature<Polygon | MultiPolygon>>toSplit)) {
|
||||
return [{
|
||||
type: "Feature",
|
||||
properties: { ...toSplit.properties },
|
||||
geometry: boundary.geometry,
|
||||
bbox: boundary.bbox
|
||||
}]
|
||||
}
|
||||
return []
|
||||
}
|
||||
splitup.properties = { ...toSplit.properties }
|
||||
return [splitup]
|
||||
}
|
||||
|
@ -864,32 +919,6 @@ export class GeoOperations {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs all tiles where features overlap with and puts those features in them.
|
||||
* Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with
|
||||
* @param zoomlevel
|
||||
* @param features
|
||||
*/
|
||||
public static slice(zoomlevel: number, features: Feature[]): Map<number, Feature[]> {
|
||||
const tiles = new Map<number, Feature[]>()
|
||||
|
||||
for (const feature of features) {
|
||||
const bbox = BBox.get(feature)
|
||||
Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => {
|
||||
const i = Tiles.tile_index(zoomlevel, x, y)
|
||||
|
||||
let tiledata = tiles.get(i)
|
||||
if (tiledata === undefined) {
|
||||
tiledata = []
|
||||
tiles.set(i, tiledata)
|
||||
}
|
||||
tiledata.push(feature)
|
||||
})
|
||||
}
|
||||
|
||||
return tiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linestring object based on the outer ring of the given polygon
|
||||
*
|
||||
|
@ -905,8 +934,8 @@ export class GeoOperations {
|
|||
properties: p.properties,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: p.geometry.coordinates[0],
|
||||
},
|
||||
coordinates: p.geometry.coordinates[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -934,7 +963,7 @@ export class GeoOperations {
|
|||
console.debug("SPlitting way", feature.properties.id)
|
||||
result.push({
|
||||
...feature,
|
||||
geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) },
|
||||
geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) }
|
||||
})
|
||||
coors = coors.slice(0, i + 1)
|
||||
break
|
||||
|
@ -943,7 +972,7 @@ export class GeoOperations {
|
|||
}
|
||||
result.push({
|
||||
...feature,
|
||||
geometry: { ...feature.geometry, coordinates: coors },
|
||||
geometry: { ...feature.geometry, coordinates: coors }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1117,8 +1146,8 @@ export class GeoOperations {
|
|||
properties: multiLineStringFeature.properties,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: coors[0],
|
||||
},
|
||||
coordinates: coors[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
@ -1126,8 +1155,8 @@ export class GeoOperations {
|
|||
properties: multiLineStringFeature.properties,
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates: coors,
|
||||
},
|
||||
coordinates: coors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
33
src/Logic/Web/CommunityIndex.ts
Normal file
33
src/Logic/Web/CommunityIndex.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Various tools and types to work with the community index (https://openstreetmap.community/; https://github.com/osmlab/osm-community-index)
|
||||
*/
|
||||
|
||||
|
||||
export interface CommunityResource {
|
||||
/**
|
||||
* A unique identifier for the resource
|
||||
* "pattern": "^[-_.A-Za-z0-9]+$"
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Type of community resource (thus: platform)
|
||||
*/
|
||||
type: string,
|
||||
/**
|
||||
* included and excluded locations for this item
|
||||
* See location-conflation documentation for compatible values: https://github.com/rapideditor/location-conflation#readme
|
||||
*/
|
||||
locationSet?,
|
||||
|
||||
/** Array of ISO-639-1 (2 letter) or ISO-639-3 (3 letter) codes in lowercase
|
||||
* */
|
||||
languageCodes?: string[]
|
||||
/**
|
||||
* Resource account string, required for some resource types
|
||||
*/
|
||||
account?: string
|
||||
|
||||
resolved?: { url: string, name: string, description: string } & Record<string, string>
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { BBox } from "../Logic/BBox"
|
||||
import { Feature, Polygon } from "geojson"
|
||||
|
||||
export interface TileRange {
|
||||
xstart: number
|
||||
|
@ -80,6 +81,17 @@ export class Tiles {
|
|||
return [z, x, index % factor]
|
||||
}
|
||||
|
||||
static asGeojson(index: number): Feature<Polygon>;
|
||||
static asGeojson(x: number, y: number, z: number): Feature<Polygon>;
|
||||
static asGeojson(zIndex: number, x?: number, y?: number): Feature<Polygon> {
|
||||
let z = zIndex
|
||||
if (x === undefined) {
|
||||
[z, x, y] = Tiles.tile_from_index(zIndex)
|
||||
}
|
||||
const bounds = Tiles.tile_bounds_lon_lat(z, x, y)
|
||||
return new BBox(bounds).asGeoJson()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return x, y of the tile containing (lat, lon) on the given zoom level
|
||||
*/
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
import ContactLink from "./ContactLink.svelte"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Translations from "../i18n/Translations"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import type { Feature, Geometry, GeometryCollection } from "@turf/turf"
|
||||
import type { FeatureCollection, Polygon } from "geojson"
|
||||
import type { CommunityResource } from "../../Logic/Web/CommunityIndex"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let location: Store<{ lat: number; lon: number }>
|
||||
const tileToFetch: Store<string> = location.mapD((l) => {
|
||||
|
@ -20,7 +22,10 @@
|
|||
>([])
|
||||
|
||||
tileToFetch.addCallbackAndRun(async (url) => {
|
||||
const data = await Utils.downloadJsonCached(url, 24 * 60 * 60)
|
||||
const data = await Utils.downloadJsonCached<FeatureCollection<Polygon, {
|
||||
nameEn: string,
|
||||
resources: Record<string, CommunityResource>
|
||||
}>>(url, 24 * 60 * 60)
|
||||
if (data === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -29,15 +34,13 @@
|
|||
|
||||
const filteredResources = resources.map(
|
||||
(features) =>
|
||||
features.filter((f) => {
|
||||
return GeoOperations.inside([location.data.lon, location.data.lat], f)
|
||||
}),
|
||||
features.filter((f) => GeoOperations.inside([location.data.lon, location.data.lat], f)),
|
||||
[location]
|
||||
)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ToSvelte construct={t.intro} />
|
||||
<Tr t={t.intro} />
|
||||
{#each $filteredResources as feature}
|
||||
<ContactLink country={feature.properties} />
|
||||
{/each}
|
||||
|
|
|
@ -3,23 +3,18 @@
|
|||
// The _properties_ of a community feature
|
||||
import Locale from "../i18n/Locale.js"
|
||||
import Translations from "../i18n/Translations"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import * as native from "../../assets/language_native.json"
|
||||
import { TypedTranslation } from "../i18n/Translation"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { CommunityResource } from "../../Logic/Web/CommunityIndex"
|
||||
|
||||
const availableTranslationTyped: TypedTranslation<{ native: string }> =
|
||||
Translations.t.communityIndex.available
|
||||
const availableTranslation = availableTranslationTyped.OnEveryLanguage((s, ln) =>
|
||||
s.replace("{native}", native[ln] ?? ln)
|
||||
)
|
||||
export let country: { resources; nameEn: string }
|
||||
let resources: {
|
||||
id: string
|
||||
resolved: Record<string, string>
|
||||
languageCodes: string[]
|
||||
type: string
|
||||
}[] = []
|
||||
export let country: { resources: Record<string, CommunityResource>; nameEn: string }
|
||||
let resources: CommunityResource[] = []
|
||||
$: resources = Array.from(Object.values(country?.resources ?? {}))
|
||||
|
||||
const language = Locale.language
|
||||
|
|
|
@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"
|
|||
describe("GeoOperations", () => {
|
||||
describe("calculateOverlap", () => {
|
||||
it("should not give too much overlap (regression test)", () => {
|
||||
const polyGrb = {
|
||||
const polyGrb: Feature<Polygon> = <any>{
|
||||
type: "Feature",
|
||||
properties: {
|
||||
osm_id: "25189153",
|
||||
|
@ -37,7 +37,7 @@ describe("GeoOperations", () => {
|
|||
"_now:date": "2021-12-05",
|
||||
"_now:datetime": "2021-12-05 21:51:40",
|
||||
"_loaded:date": "2021-12-05",
|
||||
"_loaded:datetime": "2021-12-05 21:51:40",
|
||||
"_loaded:datetime": "2021-12-05 21:51:40"
|
||||
},
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
|
@ -50,21 +50,21 @@ describe("GeoOperations", () => {
|
|||
[3.24329779999996, 50.837435399999855],
|
||||
[3.2431881000000504, 50.83740090000025],
|
||||
[3.243152699999997, 50.83738980000017],
|
||||
[3.2431059999999974, 50.83730270000021],
|
||||
],
|
||||
],
|
||||
[3.2431059999999974, 50.83730270000021]
|
||||
]
|
||||
]
|
||||
},
|
||||
id: "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37",
|
||||
_lon: 3.2432137000000116,
|
||||
_lat: 50.83736194999996,
|
||||
bbox: {
|
||||
minLat: 50.83728850000007,
|
||||
maxLat: 50.837435399999855,
|
||||
maxLon: 3.2433214000000254,
|
||||
minLat: 50.83728850000007,
|
||||
minLon: 3.2431059999999974,
|
||||
},
|
||||
minLon: 3.2431059999999974
|
||||
}
|
||||
}
|
||||
const polyHouse = {
|
||||
const polyHouse: Feature<Polygon> = <any>{
|
||||
type: "Feature",
|
||||
id: "way/594963177",
|
||||
properties: {
|
||||
|
@ -95,7 +95,7 @@ describe("GeoOperations", () => {
|
|||
"_loaded:date": "2021-12-05",
|
||||
"_loaded:datetime": "2021-12-05 21:51:39",
|
||||
_surface: "93.32785810484549",
|
||||
"_surface:ha": "0",
|
||||
"_surface:ha": "0"
|
||||
},
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
|
@ -108,9 +108,9 @@ describe("GeoOperations", () => {
|
|||
[3.2431691, 50.8374252],
|
||||
[3.2430936, 50.837401],
|
||||
[3.243046, 50.8374112],
|
||||
[3.2429993, 50.8373243],
|
||||
],
|
||||
],
|
||||
[3.2429993, 50.8373243]
|
||||
]
|
||||
]
|
||||
},
|
||||
_lon: 3.2430937,
|
||||
_lat: 50.83736395,
|
||||
|
@ -118,8 +118,8 @@ describe("GeoOperations", () => {
|
|||
maxLat: 50.8374252,
|
||||
maxLon: 3.2431881,
|
||||
minLat: 50.8373027,
|
||||
minLon: 3.2429993,
|
||||
},
|
||||
minLon: 3.2429993
|
||||
}
|
||||
}
|
||||
|
||||
const p0 = turf.polygon(polyGrb.geometry.coordinates)
|
||||
|
@ -145,11 +145,11 @@ describe("GeoOperations", () => {
|
|||
[3.218560377159008, 51.21499687768525],
|
||||
[3.2207456783268356, 51.21499687768525],
|
||||
[3.2207456783268356, 51.21600586532159],
|
||||
[3.218560377159008, 51.21600586532159],
|
||||
],
|
||||
[3.218560377159008, 51.21600586532159]
|
||||
]
|
||||
],
|
||||
type: "Polygon",
|
||||
},
|
||||
type: "Polygon"
|
||||
}
|
||||
}
|
||||
const line: Feature<LineString> = {
|
||||
type: "Feature",
|
||||
|
@ -157,10 +157,10 @@ describe("GeoOperations", () => {
|
|||
geometry: {
|
||||
coordinates: [
|
||||
[3.218405371672816, 51.21499091846559],
|
||||
[3.2208408127450525, 51.21560173433727],
|
||||
[3.2208408127450525, 51.21560173433727]
|
||||
],
|
||||
type: "LineString",
|
||||
},
|
||||
type: "LineString"
|
||||
}
|
||||
}
|
||||
const result = GeoOperations.clipWith(line, bbox)
|
||||
expect(result.length).to.equal(1)
|
||||
|
@ -168,10 +168,83 @@ describe("GeoOperations", () => {
|
|||
const clippedLine = (<Feature<LineString>>result[0]).geometry.coordinates
|
||||
const expCoordinates = [
|
||||
[3.2185604, 51.215029800031594],
|
||||
[3.2207457, 51.21557787977764],
|
||||
[3.2207457, 51.21557787977764]
|
||||
]
|
||||
|
||||
expect(clippedLine).to.deep.equal(expCoordinates)
|
||||
})
|
||||
it("clipWith should contain the full feature if it is fully contained", () => {
|
||||
const bbox: Feature<Polygon> = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
coordinates: [
|
||||
[
|
||||
[
|
||||
2.1541744759711037,
|
||||
51.73994420687188
|
||||
],
|
||||
[
|
||||
2.1541744759711037,
|
||||
50.31129074222787
|
||||
],
|
||||
[
|
||||
4.53247037641421,
|
||||
50.31129074222787
|
||||
],
|
||||
[
|
||||
4.53247037641421,
|
||||
51.73994420687188
|
||||
],
|
||||
[
|
||||
2.1541744759711037,
|
||||
51.73994420687188
|
||||
]
|
||||
]
|
||||
],
|
||||
type: "Polygon"
|
||||
}
|
||||
}
|
||||
const content: Feature<Polygon> = {
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
2.8900597545854225,
|
||||
50.9035099487991
|
||||
],
|
||||
[
|
||||
3.4872999807053873,
|
||||
50.74856284865993
|
||||
],
|
||||
[
|
||||
3.9512276563531543,
|
||||
50.947206170675486
|
||||
],
|
||||
[
|
||||
3.897902636163167,
|
||||
51.25526892606362
|
||||
],
|
||||
[
|
||||
3.188679867646016,
|
||||
51.24525576870511
|
||||
], [
|
||||
2.8900597545854225,
|
||||
50.9035099487991
|
||||
]
|
||||
]
|
||||
],
|
||||
"type": "Polygon"
|
||||
}
|
||||
}
|
||||
const clipped = GeoOperations.clipWith(content, bbox)
|
||||
expect(clipped.length).to.equal(1)
|
||||
|
||||
const clippedReverse = GeoOperations.clipWith(bbox, content)
|
||||
expect(clippedReverse.length).to.equal(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue