MapComplete/src/Models/RasterLayers.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

331 lines
12 KiB
TypeScript
Raw Normal View History

2023-10-10 01:52:02 +02:00
import { Feature, Polygon } from "geojson"
import * as globallayers from "../assets/global-raster-layers.json"
2024-08-14 11:41:57 +02:00
import * as bingJson from "../assets/bing.json"
2023-10-10 01:52:02 +02:00
import { BBox } from "../Logic/BBox"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
2023-10-10 01:52:02 +02:00
import { GeoOperations } from "../Logic/GeoOperations"
import { EliCategory, RasterLayerProperties } from "./RasterLayerProperties"
import { Utils } from "../Utils"
2024-08-14 13:53:56 +02:00
export type EditorLayerIndex = (Feature<Polygon, EditorLayerIndexProperties> & RasterLayerPolygon)[]
export class AvailableRasterLayers {
private static _editorLayerIndex: EditorLayerIndex = undefined
2024-08-14 13:53:56 +02:00
private static _editorLayerIndexStore: UIEventSource<EditorLayerIndex> =
new UIEventSource<EditorLayerIndex>(undefined)
public static async editorLayerIndex(): Promise<EditorLayerIndex> {
2024-08-14 13:53:56 +02:00
if (AvailableRasterLayers._editorLayerIndex !== undefined) {
return AvailableRasterLayers._editorLayerIndex
}
console.debug("Downloading ELI")
2024-08-14 13:53:56 +02:00
const eli = await Utils.downloadJson<{ features: EditorLayerIndex }>(
"./assets/data/editor-layer-index.json"
)
this._editorLayerIndex = eli.features.filter((l) => l.properties.id !== "Bing")
this._editorLayerIndexStore.set(this._editorLayerIndex)
return this._editorLayerIndex
}
public static globalLayers: ReadonlyArray<RasterLayerPolygon> = globallayers.layers
.filter(
(properties) =>
properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/
)
.map(
(properties) =>
<RasterLayerPolygon>{
type: "Feature",
properties,
geometry: BBox.global.asGeometry()
}
)
2024-08-14 13:53:56 +02:00
public static bing = <RasterLayerPolygon>bingJson
2023-10-10 01:52:02 +02:00
public static readonly osmCartoProperties: RasterLayerProperties = {
id: "osm",
name: "OpenStreetMap",
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright"
2023-10-10 01:52:02 +02:00
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap"
2023-10-10 01:52:02 +02:00
}
2023-10-10 01:52:02 +02:00
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry()
2023-10-10 01:52:02 +02:00
}
2024-03-25 04:17:13 +01:00
/**
* The default background layer that any theme uses which does not explicitly define a background
*/
2024-04-13 02:40:21 +02:00
public static readonly defaultBackgroundLayer: RasterLayerPolygon =
AvailableRasterLayers.globalLayers.find((l) => {
return l.properties.id === "protomaps.sunny"
})
2024-08-14 13:53:56 +02:00
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>,
enableBing?: Store<boolean>
): { store: Store<RasterLayerPolygon[]> } {
const store = { store: undefined }
Utils.AddLazyProperty(store, "store", () =>
AvailableRasterLayers._layersAvailableAt(location, enableBing)
)
return store
}
private static _layersAvailableAt(
location: Store<{ lon: number; lat: number }>,
enableBing?: Store<boolean>
2023-10-10 01:52:02 +02:00
): Store<RasterLayerPolygon[]> {
this.editorLayerIndex() // start the download
2023-10-10 01:52:02 +02:00
const availableLayersBboxes = Stores.ListStabilized(
2024-08-14 13:53:56 +02:00
location.mapD(
(loc) => {
const eli = AvailableRasterLayers._editorLayerIndexStore.data
if (!eli) {
return []
}
const lonlat: [number, number] = [loc.lon, loc.lat]
return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat))
},
[AvailableRasterLayers._editorLayerIndexStore]
)
2023-10-10 01:52:02 +02:00
)
return Stores.ListStabilized(
availableLayersBboxes.map(
(eliPolygons) => {
const loc = location.data
const lonlat: [number, number] = [loc.lon, loc.lat]
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) {
return true // global ELI-layer
}
return GeoOperations.inside(lonlat, eliPolygon)
})
matching.unshift(AvailableRasterLayers.osmCarto)
if (enableBing?.data) {
matching.push(AvailableRasterLayers.bing)
2023-10-10 01:52:02 +02:00
}
matching.push(...AvailableRasterLayers.globalLayers)
2024-06-16 16:06:26 +02:00
if (
!matching.some(
(l) =>
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id
)
) {
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
}
return matching
},
[enableBing]
)
2023-10-10 01:52:02 +02:00
)
}
}
export class RasterLayerUtils {
2023-10-10 01:52:02 +02:00
/**
* Selects, from the given list of available rasterLayerPolygons, a rasterLayer.
* This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available).
* Returns 'undefined' if no such layer is available
* @param available
* @param preferredCategory
* @param ignoreLayer
* @param skipLayers Skip the first N layers
2023-10-10 01:52:02 +02:00
*/
public static SelectBestLayerAccordingTo(
available: RasterLayerPolygon[],
preferredCategory: string,
ignoreLayer?: RasterLayerPolygon,
skipLayers: number = 0
2023-10-10 01:52:02 +02:00
): RasterLayerPolygon {
2024-08-23 13:13:41 +02:00
const inCategory = available.filter((l) => l.properties.category === preferredCategory)
const best: RasterLayerPolygon[] = inCategory.filter((l) => l.properties.best)
const others: RasterLayerPolygon[] = inCategory.filter((l) => !l.properties.best)
let all = best.concat(others)
2024-08-23 13:13:41 +02:00
console.log(
"Selected layers are:",
all.map((l) => l.properties.id)
)
if (others.length > skipLayers) {
all = all.slice(skipLayers)
2023-10-08 17:38:28 +02:00
}
2024-08-23 13:13:41 +02:00
return all.find((l) => l !== ignoreLayer)
}
}
export type RasterLayerPolygon = Feature<Polygon, RasterLayerProperties>
/**
* Information about a raster tile layer
*
* Based on the spec here https://github.com/osmlab/editor-layer-index/blob/gh-pages/schema.json
* which was then converted with http://borischerny.com/json-schema-to-typescript-browser/
*/
export interface EditorLayerIndexProperties extends RasterLayerProperties {
/**
2023-10-10 01:52:02 +02:00
* The name of the imagery source
*/
readonly name: string
/**
* Whether the imagery name should be translated
*/
readonly i18n?: boolean
readonly type:
| "tms"
| "wms"
| "bing"
| "scanex"
| "wms_endpoint"
| "wmts"
| "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */
/**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/
readonly category?: EliCategory
2023-10-10 01:52:02 +02:00
/**
* A URL template for imagery tiles
*/
readonly url: string
2023-10-10 01:52:02 +02:00
readonly min_zoom?: number
readonly max_zoom?: number
/**
* explicit/implicit permission by the owner for use in OSM
*/
readonly permission_osm?: "explicit" | "implicit" | "no"
/**
* A URL for the license or permissions for the imagery
*/
readonly license_url?: string
/**
* A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
*/
readonly privacy_policy_url?: string | boolean
/**
* A unique identifier for the source; used in imagery_used changeset tag
*/
readonly id: string
/**
* A short English-language description of the source
*/
readonly description?: string
/**
* The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
*/
readonly country_code?: string
/**
* Whether this imagery should be shown in the default world-wide menu
*/
readonly default?: boolean
/**
* Whether this imagery is the best source for the region
*/
readonly best?: boolean
/**
* The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly start_date?: string
/**
* The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
*/
readonly end_date?: string
/**
* HTTP header to check for information if the tile is invalid
*/
readonly no_tile_header?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "^.*$".
*/
[k: string]: string[] | null
}
/**
* 'true' if tiles are transparent and can be overlaid on another source
*/
readonly overlay?: boolean & string
readonly available_projections?: string[]
readonly attribution?: {
readonly url?: string
readonly text?: string
readonly html?: string
readonly required?: boolean
}
/**
* A URL for an image, that can be displayed in the list of imagery layers next to the name
*/
readonly icon?: string
/**
* A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
*/
readonly eula?: string
/**
* A URL for an image, that is displayed in the mapview for attribution
*/
readonly "logo-image"?: string
/**
* Customized text for the terms of use link (default is "Background Terms of Use")
*/
readonly "terms-of-use-text"?: string
/**
* Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
*/
readonly "no-tile-checksum"?: string
/**
* header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
*/
readonly "metadata-header"?: string
/**
* Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
*/
readonly "valid-georeference"?: boolean
/**
* Size of individual tiles delivered by a TMS service
*/
readonly "tile-size"?: number
/**
* Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
*/
readonly "mod-tile-features"?: string
/**
* HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
*/
readonly "custom-http-headers"?: {
readonly "header-name"?: string
readonly "header-value"?: string
2023-09-28 23:50:27 +02:00
}
2023-10-10 01:52:02 +02:00
/**
* Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
*/
readonly "default-layers"?: {
layer?: {
"layer-name"?: string
"layer-style"?: string
[k: string]: unknown
}
[k: string]: unknown
}[]
/**
* format to use when connecting tile server (when using WMS_ENDPOINT type)
*/
readonly format?: string
/**
* If `true` transparent tiles will be requested from WMS server
*/
readonly transparent?: boolean & string
/**
* minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
*/
readonly "minimum-tile-expire"?: number
}