From 5d3f927d7ef8ecc27270677286771236e0805fea Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Thu, 30 Nov 2023 19:22:43 -0500 Subject: [PATCH 01/15] in progress --- .eslintrc.json | 8 +- ADVANCED.md | 5 +- package.json | 80 ++- src/georaster-layer-for-leaflet.ts | 1050 +++++++++++++++------------- src/types/declarations.d.ts | 20 + src/types/index.ts | 3 +- tests/arrayBuffer.html | 7 +- tests/arrayBufferInUTM.html | 5 +- tests/arrows.html | 5 +- tests/cog-error-fallback.html | 3 +- tests/cog-mask.html | 2 +- tests/cog-single-band-canopy.html | 2 +- tests/cog.html | 19 +- tests/color-scale.2.html | 56 +- tests/color-scale.html | 11 +- tests/global-mask-inside.html | 26 +- tests/global-mask-outside.html | 11 +- tests/global.html | 2 +- tests/ifr.html | 2 +- tests/inner-tile-css.html | 13 +- tests/landsat-double-band.html | 2 +- tests/landsat-single-band.html | 2 +- tests/leaflet-latest.html | 2 +- tests/paletted.html | 2 +- tests/projection_3857.html | 2 +- tests/projection_4269.html | 2 +- tests/separated-1.html | 2 +- tests/separated-2.html | 2 +- tests/separated-3.html | 2 +- tests/setup.sh | 6 + tests/simple-2.html | 29 +- tests/simple.html | 2 +- tests/undersampling.html | 5 +- tests/ycbcr.html | 12 +- tsconfig.json | 8 +- 35 files changed, 799 insertions(+), 611 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e8e54a6..5929a36 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,23 +11,19 @@ }, "plugins": ["@typescript-eslint/eslint-plugin", "prettier"], "extends": [ - "prettier", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "eslint-config-prettier/@typescript-eslint" + "plugin:prettier/recommended" ], "ignorePatterns": ["dist", "webpack.config.js"], "rules": { "@typescript-eslint/ban-ts-comment": "warn", "arrow-parens": ["error", "as-needed"], "comma-dangle": ["error", { "objects": "never" }], - "indent": ["error", 2], "linebreak-style": ["error", "unix"], "no-console": "off", "no-multiple-empty-lines": "error", "no-unused-vars": ["error", { "args": "none" }], "no-var": "error", - "object-curly-spacing": ["error", "always", { "objectsInObjects": false }], + "object-curly-spacing": ["error", "always", { "objectsInObjects": true }], "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-rest-params": "error", diff --git a/ADVANCED.md b/ADVANCED.md index 9f9ec25..3bdd260 100644 --- a/ADVANCED.md +++ b/ADVANCED.md @@ -46,4 +46,7 @@ new GeoRasterLayer({ mask: { type: "FeatureCollection", features: [ /* .. */] }, // a GeoJSON for the world's oceans mask_strategy: "outside" }); -``` \ No newline at end of file +``` + +# Turbo Mode +You can enable turbo mode, which will in some cases lead to better performance. However, it is experimental, so should be tested first on your data before using in production. Internally, it uses [proj-turbo](https://github.com/DanielJDufour/proj-turbo). \ No newline at end of file diff --git a/package.json b/package.json index a8472ef..1484f2b 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,13 @@ "clean": "rimraf dist", "b": "npm run build", "build:ts:source": "npx mkdirp ./dist/v3/ts/source/. && cp -r ./src/* ./dist/v3/ts/source/.", - "build:ts:types": "tsc --emitDeclarationOnly", + "build:ts:types": "tsc --emitDeclarationOnly --moduleResolution node --skipLibCheck", "build:babel": "npm run build:babel:esm && npm run build:babel:cjs", "build:babel:esm": "npx babel --extensions \".ts\" --plugins @babel/plugin-proposal-export-namespace-from --plugins @babel/plugin-proposal-optional-chaining --presets @babel/preset-typescript ./src/georaster-layer-for-leaflet.ts --out-dir ./dist/v3/babel/esm", "build:babel:cjs": "npx babel --extensions \".ts\" --plugins @babel/plugin-transform-modules-commonjs --plugins @babel/plugin-proposal-optional-chaining --plugins @babel/plugin-proposal-export-namespace-from --presets @babel/preset-typescript ./src/georaster-layer-for-leaflet.ts --out-dir dist/v3/babel/cjs", "build:webpack:bundle": "WEBPACK_OUTPUT_PATH=\"$PWD/dist/v3/webpack/bundle/\" webpack", "build:webpack:lite": "LITE=true WEBPACK_OUTPUT_PATH=\"$PWD/dist/v3/webpack/lite/\" webpack", + "clean:webpack:bundle": "rimraf $PWD/dist/v3/webpack/bundle", "copy:legacy": "npm run copy:legacy:min && npm run copy:legacy:lite && npm run copy:legacy:cjs && npm run copy:legacy:esm && npm run copy:legacy:types", "copy:legacy:min": "cp ./dist/v3/webpack/bundle/* ./dist/.", "copy:legacy:lite": "cp ./dist/v3/webpack/lite/* ./dist/.", @@ -34,7 +35,7 @@ "copy:legacy:esm": "cp ./dist/v3/babel/esm/georaster-layer-for-leaflet.js ./dist/georaster-layer-for-leaflet.js", "copy:legacy:types": "cp -r ./dist/v3/ts/types/* ./dist/.", "build": "npm run clean && npm run build:ts:source && npm run build:ts:types && npm run build:babel:esm && npm run build:babel:cjs && npm run build:webpack:bundle && npm run build:webpack:lite && npm run copy:legacy", - "dev": "concurrently \"npm run serve\" \"npm run build:webpack:bundle:old -- --watch\" ", + "dev": "concurrently \"npm run serve\" \"npm run clean:webpack:bundle && npm run build:webpack:bundle -- --watch\" ", "f": "npm run format", "format": "prettier --write ./src/georaster-layer-for-leaflet.ts webpack.config.js && npm run fix", "fix": "eslint ./src/georaster-layer-for-leaflet.ts --fix", @@ -67,41 +68,56 @@ }, "homepage": "https://github.com/GeoTIFF/georaster-layer-for-leaflet#readme", "dependencies": { - "@types/geojson": "^7946.0.10", - "@types/node": "^18.7.13", - "chroma-js": "^1.4.1", - "geo-extent": "^0.11.0", - "geocanvas": "^0.3.1", - "pixel-utils": "^0.7.0", - "proj4-fully-loaded": "^0.1.0", - "regenerator-runtime": "^0.13.9", - "reproject-bbox": "^0.4.1", - "snap-bbox": "^0.2.0", - "utm-utils": "^0.1.0" + "@types/geojson": "^7946.0.13", + "bbox-fns": "^0.19.0", + "geo-extent": "^1.3.1", + "georaster-stack": "^0.4.3", + "geotiff-epsg-code": "^0.3.1", + "geotiff-read-bbox": "^2.2.0", + "geowarp": "^1.23.1", + "geowarp-canvas": "link:../../DanielJDufour/geowarp-canvas", + "memoizee": "^0.4.15", + "pixel-utils": "^0.9.0", + "proj4": "^2.9.2", + "proj4-collect": "^0.0.2", + "proj4-fully-loaded": "^0.2.0", + "proj4-merge": "^0.1.1", + "regenerator-runtime": "^0.14.0", + "snap-bbox": "^0.5.0", + "spatial-reference-system": "^0.5.2", + "utm-utils": "^0.6.1", + "xdim": "^1.10.1" }, "devDependencies": { - "@babel/cli": "^7.18.10", - "@babel/core": "^7.18.13", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/preset-env": "^7.18.10", - "@babel/preset-typescript": "^7.18.6", - "@types/chroma-js": "^2.1.4", - "@types/leaflet": "^1.7.11", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "babel-loader": "^8.2.5", - "concurrently": "^5.3.0", + "@babel/cli": "^7.23.4", + "@babel/core": "^7.23.5", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/preset-env": "^7.23.5", + "@babel/preset-typescript": "^7.23.3", + "@types/chroma-js": "^2.4.3", + "@types/leaflet": "^1.9.8", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "babel-loader": "^9.1.3", + "concurrently": "^8.2.2", "envisage": "^0.1.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^7.2.0", - "eslint-plugin-prettier": "^3.4.1", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "fast-max": "^0.4.0", + "fast-min": "^0.3.0", "geojson": "^0.5.0", + "georaster": "^1.6.0", + "leaflet": "^1.9.4", + "mkdirp": "^3.0.1", "null-loader": "^4.0.1", - "prettier": "^2.7.1", - "rimraf": "^3.0.2", - "typescript": "^4.8.2", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" + "prettier": "^3.1.0", + "reproject-geojson": "^0.5.0", + "rimraf": "^5.0.5", + "to-canvas": "^0.2.0", + "typescript": "^5.3.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4" }, "peerDependencies": { "georaster": "*", diff --git a/src/georaster-layer-for-leaflet.ts b/src/georaster-layer-for-leaflet.ts index 57f1109..2b11dae 100644 --- a/src/georaster-layer-for-leaflet.ts +++ b/src/georaster-layer-for-leaflet.ts @@ -1,15 +1,24 @@ +/* global global */ /* global proj4 */ import "regenerator-runtime/runtime.js"; import * as L from "leaflet"; -import chroma from "chroma-js"; -import geocanvas from "geocanvas"; import { rawToRgb } from "pixel-utils"; +import densePolygon from "bbox-fns/dense-polygon.js"; import isUTM from "utm-utils/src/isUTM.js"; import getProjString from "utm-utils/src/getProjString.js"; import type { Coords, DoneCallback, LatLngBounds, LatLngTuple } from "leaflet"; -import proj4FullyLoaded from "proj4-fully-loaded"; + +import proj4collect from "proj4-collect"; +import reprojectGeoJSON from "reproject-geojson"; + +import bboxMerge from "bbox-fns/merge.js"; +import fastMin from "fast-min"; +import fastMax from "fast-max"; import { GeoExtent } from "geo-extent"; +import geowarp_core from "geowarp"; +import geowarp_canvas from "geowarp-canvas"; import snap from "snap-bbox"; +import { GeoRasterStack } from "georaster-stack/web"; import type { CustomCRS, @@ -17,7 +26,6 @@ import type { GeoRasterLayerOptions, GeoRaster, GeoRasterKeys, - GetRasterOptions, DrawTileOptions, Mask, MaskStrategy, @@ -25,13 +33,14 @@ import type { Tile } from "./types"; +declare global {} + const EPSG4326 = 4326; -const PROJ4_SUPPORTED_PROJECTIONS = new Set([3785, 3857, 4269, 4326, 900913, 102113]); -const MAX_NORTHING = 1000; -const MAX_EASTING = 1000; const ORIGIN: LatLngTuple = [0, 0]; -const log = (obj: any) => console.log("[georaster-layer-for-leaflet] ", obj); +const geowarp = geowarp_canvas(geowarp_core); + +const isDefaultCRS = (crs: any) => crs === L.CRS.EPSG3857 || crs.code === "EPSG:3857"; // figure out if simple CRS // even if not created with same instance of LeafletJS @@ -58,12 +67,15 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C keepBuffer: 25, resolution: 2 ** 5, debugLevel: 0, - caching: true + caching: true, + turbo: false }, cache: {}, initialize: function (options: GeoRasterLayerOptions) { + this.proj4 = proj4collect(); + try { if (options.georasters) { this.georasters = options.georasters; @@ -80,39 +92,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C } if (options.resampleMethod) { - this.resampleMethod = options.resampleMethod; - } - - /* - Unpacking values for use later. - We do this in order to increase speed. - */ - const keys = [ - "height", - "width", - "noDataValue", - "palette", - "pixelHeight", - "pixelWidth", - "projection", - "sourceType", - "xmin", - "xmax", - "ymin", - "ymax" - ]; - if (this.georasters.length > 1) { - keys.forEach(key => { - if (this.same(this.georasters, key)) { - this[key] = this.georasters[0][key]; - } else { - throw new Error("all GeoRasters must have the same " + key); - } - }); - } else if (this.georasters.length === 1) { - keys.forEach(key => { - this[key] = this.georasters[0][key]; - }); + if ((options.resampleMethod as any) === "nearest") { + this.resampleMethod = "near"; + } else { + this.resampleMethod = options.resampleMethod; + } } this._cache = { @@ -120,40 +104,45 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C tile: {} }; - this.extent = new GeoExtent([this.xmin, this.ymin, this.xmax, this.ymax], { srs: this.projection }); + this.debugLevel = options.debugLevel; - // used later if simple projection - this.ratio = this.height / this.width; + this.subextents = this.georasters.map( + (g: any) => new GeoExtent([g.xmin, g.ymin, g.xmax, g.ymax], { srs: g.projection }) + ); - this.debugLevel = options.debugLevel; - if (this.debugLevel >= 1) log({ options }); - - if (this.georasters.every((georaster: GeoRaster) => typeof georaster.values === "object")) { - this.rasters = this.georasters.reduce((result: number[][][], georaster: GeoRaster) => { - // added double-check of values to make typescript linter and compiler happy - if (georaster.values) { - result = result.concat(georaster.values); - return result; - } - }, []); - if (this.debugLevel > 1) console.log("this.rasters:", this.rasters); - } + // normalize all extents to EPSG:4326 and combine them + this.extent = new GeoExtent(bboxMerge(this.subextents.map((extent: any) => extent.reproj(4326).bbox)), { + srs: 4326 + }); - if (options.mask) { - if (typeof options.mask === "string") { - this.mask = fetch(options.mask).then(r => r.json()) as Promise; - } else if (typeof options.mask === "object") { - this.mask = Promise.resolve(options.mask); - } + const max_height = Math.max.apply( + null, + this.georasters.map((it: any) => it.height) + ); + const max_width = Math.max.apply( + null, + this.georasters.map((it: any) => it.width) + ); + this.simpleExtent = new GeoExtent([0, 0, max_width, max_height]); - // default mask srs is the EPSG:4326 projection used by GeoJSON - this.mask_srs = options.mask_srs || "EPSG:4326"; + if (this.debugLevel >= 1) { + console.log("[georaster-layer-for-leaflet] ", { options }); } - this.mask_strategy = (options.mask_strategy || "outside") as MaskStrategy; + this.initialize_mask(options); - this.chroma = chroma; - this.scale = chroma.scale(); + this.turbo = options.turbo || false; + + this.stack = GeoRasterStack.init({ + // flatten results, so it appears as if all the bands + // are from the same raster + flat: true, + sources: this.georasters, + + debugLevel: this.debugLevel, + method: this.resampleMethod, + turbo: this.turbo + }); // could probably replace some day with a simple // (for let k in options) { this.options[k] = options[k]; } @@ -172,10 +161,48 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C throw "you must pass in a pixelValuesToColorFn if you are combining rasters"; } + this._all_values_in_memory = this.georasters.every( + (georaster: GeoRaster) => typeof georaster.values === "object" + ); + // total number of bands across all georasters this.numBands = this.georasters.reduce((total: number, g: GeoRaster) => total + g.numberOfRasters, 0); if (this.debugLevel > 1) console.log("this.numBands:", this.numBands); + // if we have pre-computed stats, save them, so we can use them for linear stretching later + if ( + this.georasters.every( + (g: any) => + Array.isArray(g.mins) && + g.mins.length === g.numberOfRasters && + g.mins.every((it: number | null) => typeof it === "number") && + Array.isArray(g.maxs) && + g.maxs.length === g.numberOfRasters && + g.maxs.every((it: number | null) => typeof it === "number") + ) + ) { + this.stats = { + mins: [], + maxs: [] + }; + + this.georasters.map((georaster: any) => { + const numBands = georaster.numberOfRasters; + if (georaster.mins.length === numBands) { + this.stats.mins = this.stats.mins.concat(georaster.mins); + } else { + this.stats.mins = this.stats.mins.concat(new Array(numBands).fill(null)); + } + if (georaster.maxs.length === numBands) { + this.stats.maxs = this.stats.maxs.concat(georaster.maxs); + } else { + this.stats.maxs = this.stats.maxs.concat(new Array(numBands).fill(null)); + } + this.stats.ranges = zip(this.stats.mins, this.stats.maxs).map(([min, max]) => max - min); + }); + console.log("this.stats:", this.stats); + } + // in-case we want to track dynamic/running stats of all pixels fetched this.currentStats = { mins: new Array(this.numBands), @@ -183,6 +210,17 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C ranges: new Array(this.numBands) }; + // if don't have custom band arithmetic, set one based on the palette + if ( + !this.options.pixelValuesToColorFn && + !this.options.customDrawFunction && + this.georasters.length === 1 && + Array.isArray(this.georasters[0].palette) + ) { + const palette = this.georasters[0].palette; + this.options.pixelValuesToColorFn = (values: Number[]) => palette[values[0] as number]; + } + // using single-band raster as grayscale // or mapping 2 or 3 rasters to rgb bands if ( @@ -223,11 +261,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C return resolve(true); }); } catch (error) { - console.error("ERROR initializing GeoTIFFLayer", error); + console.error("ERROR initializing GeoRasterLayer", error); } }, - onAdd: function (map) { + onAdd: function (map: any) { if (!this.options.maxZoom) { // maxZoom is needed to display the tiles in the correct order over the zIndex between the zoom levels // https://github.com/Leaflet/Leaflet/blob/2592967aa6bd392db0db9e58dab840054e2aa291/src/layer/tile/GridLayer.js#L375C21-L375C21 @@ -237,89 +275,63 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C L.GridLayer.prototype.onAdd.call(this, map); }, - getRasters: function (options: GetRasterOptions) { - const { - innerTileTopLeftPoint, - heightOfSampleInScreenPixels, - widthOfSampleInScreenPixels, - zoom, - numberOfSamplesAcross, - numberOfSamplesDown, - ymax, - xmin - } = options; - if (this.debugLevel >= 1) console.log("starting getRasters with options:", options); - - // called if georaster was constructed from URL and we need to get - // data separately for each tile - // aka 'COG mode' - - /* - This function takes in coordinates in the rendered image inner tile and - returns the y and x values in the original raster - */ - const rasterCoordsForTileCoords = (h: number, w: number): { x: number; y: number } | null => { - const xInMapPixels = innerTileTopLeftPoint.x + w * widthOfSampleInScreenPixels; - const yInMapPixels = innerTileTopLeftPoint.y + h * heightOfSampleInScreenPixels; - - const mapPoint = L.point(xInMapPixels, yInMapPixels); - if (this.debugLevel >= 1) log({ mapPoint }); - - const { lat, lng } = this.getMap().unproject(mapPoint, zoom); - - if (this.projection === EPSG4326) { - return { - y: Math.round((ymax - lat) / this.pixelHeight), - x: Math.round((lng - xmin) / this.pixelWidth) - }; - } else if (this.getProjector()) { - /* source raster doesn't use latitude and longitude, - so need to reproject point from lat/long to projection of raster - */ - const [x, y] = this.getProjector().inverse([lng, lat]); - if (x === Infinity || y === Infinity) { - if (this.debugLevel >= 1) console.error("projector converted", [lng, lat], "to", [x, y]); - } - return { - y: Math.round((ymax - y) / this.pixelHeight), - x: Math.round((x - xmin) / this.pixelWidth) - }; + initialize_mask: function (options: any) { + if (options.mask && options.mask !== "auto") { + if (typeof options.mask === "string") { + this.mask = fetch(options.mask).then(r => r.json()) as Promise; + } else if (typeof options.mask === "object") { + this.mask = Promise.resolve(options.mask); + } + this.mask_srs = options.mask_srs || "EPSG:4326"; + } else if (options.mask === "auto") { + const projections = new Set(this.georasters.map((it: any) => it.projection)); + if (projections.size === 1) { + this.mask = Promise.resolve({ + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: this.subextents + .map((ext: any) => ext.unwrap()) + .flat() + .map((ext: any) => densePolygon(ext.bbox, { density: 100 })) + } + }); + this.mask_srs = Array.from(projections)[0]; } else { - return null; + this.mask = Promise.resolve({ + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: this.subextents + .map((ext: any) => ext.unwrap()) + .flat() + .map((ext: any) => ext.reproj(4326, { density: "high" })) + .map((ext: any) => densePolygon(ext.bbox, { density: 100 })) + } + }); + this.mask_srs = "EPSG:4326"; } - }; + } + }, - // careful not to flip min_y/max_y here - const topLeft = rasterCoordsForTileCoords(0, 0); - const bottomRight = rasterCoordsForTileCoords(numberOfSamplesDown, numberOfSamplesAcross); - - const getValuesOptions = { - bottom: bottomRight?.y, - height: numberOfSamplesDown, - left: topLeft?.x, - right: bottomRight?.x, - top: topLeft?.y, - width: numberOfSamplesAcross - }; + getProjDef: function (proj: number | string) { + if (isUTM(proj)) return getProjString(proj); + if (typeof proj === "number") proj = "EPSG:" + proj; + if (proj in this.proj4.defs) return proj; + if ("EPSG:" + proj in this.proj4.defs) return "EPSG:" + proj; + throw new Error("[georaster-layer-for-leaflet] unsupported projection:" + proj); + }, - if (!Object.values(getValuesOptions).every(it => it !== undefined && isFinite(it))) { - console.error("getRasters failed because not all values are finite:", getValuesOptions); - } else { - // !note: The types need confirmation - SFR 2021-01-20 - return Promise.all( - this.georasters.map((georaster: GeoRaster) => - georaster.getValues({ ...getValuesOptions, resampleMethod: this.resampleMethod || "nearest" }) - ) - ).then(valuesByGeoRaster => - valuesByGeoRaster.reduce((result: number[][][], values) => { - result = result.concat(values as number[][]); - return result; - }, []) - ); + getProjector: function (_from: number | string, _to: number | string) { + if (!this.isSupportedProjection(_from)) { + throw Error("[georaster-layer-for-leaflet] unsupported projection: " + _from); } + if (!this.isSupportedProjection(_to)) throw Error("[georaster-layer-for-leaflet] unsupported projection: " + _to); + return this.proj4(this.getProjDef(_from), this.getProjDef(_to)); }, createTile: function (coords: Coords, done: DoneCallback) { + console.log("starting createTile with coords:", coords); /* This tile is the square piece of the Leaflet map that we draw on */ const tile = L.DomUtil.create("canvas", "leaflet-tile") as HTMLCanvasElement; @@ -331,6 +343,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const context = tile.getContext("2d"); + const { x, y, z } = coords; + tile.setAttribute("data-x", x.toString()); + tile.setAttribute("data-y", y.toString()); + tile.setAttribute("data-z", z.toString()); + // note that we aren't setting the tile height or width here // drawTile dynamically sets the width and padding based on // how much the georaster takes up the tile area @@ -359,18 +376,45 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C drawTile: function ({ tile, coords, context, done, resolution }: DrawTileOptions) { try { + const start_draw_tile = performance.now(); const { debugLevel = 0 } = this; + const timed = debugLevel >= 1; + if (debugLevel >= 2) console.log("starting drawTile with", { tile, coords, context, done }); let error: Error; - const { z: zoom } = coords; - // stringified hash of tile coordinates for caching purposes - const cacheKey = [coords.x, coords.y, coords.z].join(","); + const { x, y, z } = coords; + + const cacheKey = [z, x, y].join("/"); + + if (this.options._valid_tiles && !this.options._valid_tiles.includes(cacheKey)) return; + + // over-ride default log with tile coordinate info + const log = (...rest: any[]) => { + if (rest.length === 1 && typeof rest[0] === "object" && Object.keys(rest[0]).length === 1) { + const obj = rest[0]; + const key = Object.keys(obj)[0]; + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] ${key}: `, obj[key]); + } else { + console.log(`[georaster-layer-for-leaflet] [${cacheKey}]`, ...rest); + } + }; + if (debugLevel >= 2) log({ cacheKey }); + if (this.debugLevel >= 4) { + try { + // L.geoJSON(this.extent.asGeoJSON({ density: 1000 }), { style: { color: "#0F0", fillOpacity: 0 } }).addTo( + // this.getMap() + // ); + } catch (error) { + console.error(error); + } + } + const mapCRS = this.getMapCRS(); if (debugLevel >= 2) log({ mapCRS }); @@ -378,48 +422,70 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C if (debugLevel >= 2) log({ inSimpleCRS }); // Unpacking values for increased speed - const { rasters, xmin, xmax, ymin, ymax } = this; + const { xmin, xmax, ymin, ymax } = this; const rasterHeight = this.height; const rasterWidth = this.width; const extentOfLayer = new GeoExtent(this.getBounds(), { srs: inSimpleCRS ? "simple" : 4326 }); - if (debugLevel >= 2) log({ extentOfLayer }); + if (debugLevel >= 2) log(`extentOfLayer: ${extentOfLayer.js}`); const pixelHeight = inSimpleCRS ? extentOfLayer.height / rasterHeight : this.pixelHeight; const pixelWidth = inSimpleCRS ? extentOfLayer.width / rasterWidth : this.pixelWidth; if (debugLevel >= 2) log({ pixelHeight, pixelWidth }); - // these values are used, so we don't try to sample outside of the raster - const { xMinOfLayer, xMaxOfLayer, yMinOfLayer, yMaxOfLayer } = this; const boundsOfTile = this._tileCoordsToBounds(coords); if (debugLevel >= 2) log({ boundsOfTile }); - const { code } = mapCRS; - if (debugLevel >= 2) log({ code }); + const map_crs_code = mapCRS.code; + if (debugLevel >= 2) log("map_crs_code:", map_crs_code); const extentOfTile = new GeoExtent(boundsOfTile, { srs: inSimpleCRS ? "simple" : 4326 }); - if (debugLevel >= 2) log({ extentOfTile }); + if (debugLevel >= 2) log(`extentOfTile: ${extentOfTile.js}`); // create blue outline around tiles if (debugLevel >= 4) { if (!this._cache.tile[cacheKey]) { this._cache.tile[cacheKey] = L.rectangle(extentOfTile.leafletBounds, { fillOpacity: 0 }) .addTo(this.getMap()) - .bindTooltip(cacheKey, { direction: "center", permanent: true }); + .bindTooltip(`z:${z}
x:${x}
y:${y}`, { direction: "center", permanent: true }); } } - const extentOfTileInMapCRS = inSimpleCRS ? extentOfTile : extentOfTile.reproj(code); - if (debugLevel >= 2) log({ extentOfTileInMapCRS }); + const extentOfTileInMapCRS = inSimpleCRS ? extentOfTile : extentOfTile.reproj(map_crs_code); + if (debugLevel >= 2) + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] extentOfTileInMapCRS = ${extentOfTileInMapCRS.js}`); + + if ( + !inSimpleCRS && + !this.subextents.some((extent: any) => extentOfTileInMapCRS.overlaps(extent, { strict: false })) + ) { + if (debugLevel >= 2) { + console.log( + `[georaster-layer-for-leaflet] [${cacheKey}] subextents = ${this.subextents + .map((e: any) => e.js) + .join(", ")}` + ); + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] tile and georaster don't overlap`); + } + return; + } - let extentOfInnerTileInMapCRS = extentOfTileInMapCRS.crop(inSimpleCRS ? extentOfLayer : this.extent); if (debugLevel >= 2) console.log( - "[georaster-layer-for-leaflet] extentOfInnerTileInMapCRS", - extentOfInnerTileInMapCRS.reproj(inSimpleCRS ? "simple" : 4326) + `[georaster-layer-for-leaflet] [${cacheKey}] this.subextents:`, + this.subextents.map(({ js }: any) => js) ); - if (debugLevel >= 2) log({ coords, extentOfInnerTileInMapCRS, extent: this.extent }); - // create blue outline around tiles + const cropline = inSimpleCRS ? extentOfLayer : this.extent; + let extentOfInnerTileInMapCRS = extentOfTileInMapCRS.crop(cropline); + if (debugLevel >= 2) log(`extentOfInnerTileInMapCRS: ${extentOfInnerTileInMapCRS.js}`); + + if (extentOfInnerTileInMapCRS === null) { + if (debugLevel >= 2) + console.log(`[georaster-layer-for-leaflet] failed to crop ${extentOfTileInMapCRS.js} by ${cropline.js}`); + return; + } + + // create red outline around inner tiles if (debugLevel >= 4) { if (!this._cache.innerTile[cacheKey]) { const ext = inSimpleCRS ? extentOfInnerTileInMapCRS : extentOfInnerTileInMapCRS.reproj(4326); @@ -433,64 +499,68 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const widthOfScreenPixelInMapCRS = extentOfTileInMapCRS.width / this.tileWidth; const heightOfScreenPixelInMapCRS = extentOfTileInMapCRS.height / this.tileHeight; + // const defaultCanvasHeight = Math.max(256, this.resolution || 256); + // const defaultCanvasWidth = Math.max(256, this.resolution || 256); + // const widthOfCanvasPixelInMapCRS = extentOfTileInMapCRS.width / defaultCanvasHeight; + // const heightOfCanvasPixelInMapCRS = extentOfTileInMapCRS.height / defaultCanvasWidth; if (debugLevel >= 3) log({ heightOfScreenPixelInMapCRS, widthOfScreenPixelInMapCRS }); - // expand tile sampling area to align with raster pixels - const oldExtentOfInnerTileInRasterCRS = inSimpleCRS - ? extentOfInnerTileInMapCRS - : extentOfInnerTileInMapCRS.reproj(this.projection); - const snapped = snap({ - bbox: oldExtentOfInnerTileInRasterCRS.bbox, - // pad xmax and ymin of container to tolerate ceil() and floor() in snap() - container: inSimpleCRS - ? [ - extentOfLayer.xmin, - extentOfLayer.ymin - 0.25 * pixelHeight, - extentOfLayer.xmax + 0.25 * pixelWidth, - extentOfLayer.ymax - ] - : [xmin, ymin - 0.25 * pixelHeight, xmax + 0.25 * pixelWidth, ymax], - debug: debugLevel >= 2, - origin: inSimpleCRS ? [extentOfLayer.xmin, extentOfLayer.ymax] : [xmin, ymax], - scale: [pixelWidth, -pixelHeight] // negative because origin is at ymax - }); - const extentOfInnerTileInRasterCRS = new GeoExtent(snapped.bbox_in_coordinate_system, { - srs: inSimpleCRS ? "simple" : this.projection - }); + let numberOfSamplesAcross = 256; + let numberOfSamplesDown = 256; + + if (this.options.alignSamples) { + // align tile sampling area to raster pixels (by expanding extent to tile boundaries) + // while also cropping the tile the layer extent + const oldExtentOfInnerTileInRasterCRS = inSimpleCRS + ? extentOfInnerTileInMapCRS + : extentOfInnerTileInMapCRS.reproj(this.projection); + const snapped = snap({ + bbox: oldExtentOfInnerTileInRasterCRS.bbox, + // pad xmax and ymin of container to tolerate ceil() and floor() in snap() + container: inSimpleCRS + ? [ + extentOfLayer.xmin, + extentOfLayer.ymin - 0.25 * pixelHeight, + extentOfLayer.xmax + 0.25 * pixelWidth, + extentOfLayer.ymax + ] + : [xmin, ymin - 0.25 * pixelHeight, xmax + 0.25 * pixelWidth, ymax], + debug: debugLevel >= 2, + origin: inSimpleCRS ? [extentOfLayer.xmin, extentOfLayer.ymax] : [xmin, ymax], + precise: false, // use numbers, not numerical strings + scale: [pixelWidth, -pixelHeight] // negative because origin is at ymax + }); + const extentOfInnerTileInRasterCRS = new GeoExtent(snapped.bbox_in_coordinate_system, { + srs: inSimpleCRS ? "simple" : this.projection + }); - const gridbox = snapped.bbox_in_grid_cells; - const snappedSamplesAcross = Math.abs(gridbox[2] - gridbox[0]); - const snappedSamplesDown = Math.abs(gridbox[3] - gridbox[1]); - const rasterPixelsAcross = Math.ceil(oldExtentOfInnerTileInRasterCRS.width / pixelWidth); - const rasterPixelsDown = Math.ceil(oldExtentOfInnerTileInRasterCRS.height / pixelHeight); - const layerCropExtent = inSimpleCRS ? extentOfLayer : this.extent; - const recropTileOrig = oldExtentOfInnerTileInRasterCRS.crop(layerCropExtent); // may be null - let maxSamplesAcross = 1; - let maxSamplesDown = 1; - if (recropTileOrig !== null) { - const recropTileProj = inSimpleCRS ? recropTileOrig : recropTileOrig.reproj(code); - const recropTile = recropTileProj.crop(extentOfTileInMapCRS); - if (recropTile !== null) { - maxSamplesAcross = Math.ceil(resolution * (recropTile.width / extentOfTileInMapCRS.width)); - maxSamplesDown = Math.ceil(resolution * (recropTile.height / extentOfTileInMapCRS.height)); + const gridbox = snapped.bbox_in_grid_cells; + const snappedSamplesAcross = Math.abs(gridbox[2] - gridbox[0]); + const snappedSamplesDown = Math.abs(gridbox[3] - gridbox[1]); + const rasterPixelsAcross = Math.ceil(oldExtentOfInnerTileInRasterCRS.width / pixelWidth); + const rasterPixelsDown = Math.ceil(oldExtentOfInnerTileInRasterCRS.height / pixelHeight); + const layerCropExtent = inSimpleCRS ? extentOfLayer : this.extent; + const recropTileOrig = oldExtentOfInnerTileInRasterCRS.crop(layerCropExtent); // may be null + + let maxSamplesAcross = 1; + let maxSamplesDown = 1; + if (recropTileOrig !== null) { + const recropTileProj = inSimpleCRS ? recropTileOrig : recropTileOrig.reproj(map_crs_code); + const recropTile = recropTileProj.crop(extentOfTileInMapCRS); + if (recropTile !== null) { + maxSamplesAcross = Math.ceil(resolution * (recropTile.width / extentOfTileInMapCRS.width)); + maxSamplesDown = Math.ceil(resolution * (recropTile.height / extentOfTileInMapCRS.height)); + } } - } - const overdrawTileAcross = rasterPixelsAcross < maxSamplesAcross; - const overdrawTileDown = rasterPixelsDown < maxSamplesDown; - const numberOfSamplesAcross = overdrawTileAcross ? snappedSamplesAcross : maxSamplesAcross; - const numberOfSamplesDown = overdrawTileDown ? snappedSamplesDown : maxSamplesDown; + const overdrawTileAcross = rasterPixelsAcross < maxSamplesAcross; + const overdrawTileDown = rasterPixelsDown < maxSamplesDown; + numberOfSamplesAcross = overdrawTileAcross ? snappedSamplesAcross : maxSamplesAcross; + numberOfSamplesDown = overdrawTileDown ? snappedSamplesDown : maxSamplesDown; - if (debugLevel >= 3) - console.log( - "[georaster-layer-for-leaflet] extent of inner tile before snapping " + - extentOfInnerTileInMapCRS.reproj(inSimpleCRS ? "simple" : 4326).bbox.toString() - ); - - // Reprojecting the bounding box back to the map CRS would expand it - // (unless the projection is purely scaling and translation), - // so instead just extend the old map bounding box proportionately. - { + // Reprojecting the bounding box back to the map CRS would expand it + // (unless the projection is purely scaling and translation), + // so instead just extend the old map bounding box proportionately. const oldrb = new GeoExtent(oldExtentOfInnerTileInRasterCRS.bbox); const newrb = new GeoExtent(extentOfInnerTileInRasterCRS.bbox); const oldmb = new GeoExtent(extentOfInnerTileInMapCRS.bbox); @@ -510,6 +580,26 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const newbox = [oldmb.xmin + n0, oldmb.ymin + n1, oldmb.xmax + n2, oldmb.ymax + n3]; extentOfInnerTileInMapCRS = new GeoExtent(newbox, { srs: extentOfInnerTileInMapCRS.srs }); } + } else { + // even if we aren't doing the more advacned sample alignment above + // we should still factor in the resolution when determing the resolution of the sampled rasters + // for example, if the inner tile only takes up 10% of the total tile container space, + // we shouldn't sample 256 times across + numberOfSamplesAcross = Math.ceil(resolution * (extentOfInnerTileInMapCRS.width / extentOfTileInMapCRS.width)); + numberOfSamplesDown = Math.ceil(resolution * (extentOfInnerTileInMapCRS.height / extentOfTileInMapCRS.height)); + + log(`Math.ceil(${resolution} * (${extentOfInnerTileInMapCRS.width} / ${extentOfTileInMapCRS.width}))`); + if (debugLevel >= 2) + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] numberOfSamplesAcross: ${numberOfSamplesAcross}`); + if (debugLevel >= 2) + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] numberOfSamplesDown: ${numberOfSamplesDown}`); + } + + if (debugLevel >= 3) { + console.log( + "[georaster-layer-for-leaflet] extent of inner tile before snapping " + + extentOfInnerTileInMapCRS.reproj(inSimpleCRS ? "simple" : 4326).bbox.toString() + ); } // create outline around raster pixels @@ -524,11 +614,12 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C } } - if (debugLevel >= 3) - console.log( - "[georaster-layer-for-leaflet] extent of inner tile after snapping " + - extentOfInnerTileInMapCRS.reproj(inSimpleCRS ? "simple" : 4326).bbox.toString() + if (debugLevel >= 3) { + log( + "extent of inner tile after snapping", + extentOfInnerTileInMapCRS.reproj(inSimpleCRS ? "simple" : 4326).bbox ); + } // Note that the snapped "inner" tile may extend beyond the original tile, // in which case the padding values will be negative. @@ -547,6 +638,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const innerTileWidth = this.tileWidth - padding.left - padding.right; if (debugLevel >= 3) log({ innerTileHeight, innerTileWidth }); + if (innerTileHeight === 0 || innerTileWidth === 0) { + if (debugLevel >= 2) log("returning early because the tile will be invisible"); + return; + } + if (debugLevel >= 4) { const xMinOfInnerTileInMapCRS = extentOfTileInMapCRS.xmin + padding.left * widthOfScreenPixelInMapCRS; const yMinOfInnerTileInMapCRS = extentOfTileInMapCRS.ymin + padding.bottom * heightOfScreenPixelInMapCRS; @@ -564,6 +660,9 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const canvasHeight = this.tileHeight - canvasPadding.top - canvasPadding.bottom; const canvasWidth = this.tileWidth - canvasPadding.left - canvasPadding.right; + tile.setAttribute("data-extent", extentOfTile.bbox); + tile.setAttribute("data-zxy", cacheKey); + // set padding and size of canvas tile tile.style.paddingTop = canvasPadding.top + "px"; tile.style.paddingRight = canvasPadding.right + "px"; @@ -580,11 +679,10 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C // set how large to display each sample in screen pixels const heightOfSampleInScreenPixels = innerTileHeight / numberOfSamplesDown; - const heightOfSampleInScreenPixelsInt = Math.ceil(heightOfSampleInScreenPixels); const widthOfSampleInScreenPixels = innerTileWidth / numberOfSamplesAcross; - const widthOfSampleInScreenPixelsInt = Math.ceil(widthOfSampleInScreenPixels); + if (debugLevel >= 3) console.log("heightOfSampleInScreenPixels:" + heightOfSampleInScreenPixels + "px"); + if (debugLevel >= 3) console.log("widthOfSampleInScreenPixels:" + widthOfSampleInScreenPixels + "px"); - const map = this.getMap(); const tileSize = this.getTileSize(); // this converts tile coordinates (how many tiles down and right) @@ -596,51 +694,73 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const innerTileTopLeftPoint = { x: xLeftOfInnerTile, y: yTopOfInnerTile }; if (debugLevel >= 4) log({ innerTileTopLeftPoint }); + if (timed) log(`pre-processing took ${performance.now() - start_draw_tile}ms`); + // render asynchronously so tiles show up as they finish instead of all at once (which blocks the UI) setTimeout(async () => { try { - let tileRasters: number[][][] | null = null; - if (!rasters) { - tileRasters = await this.getRasters({ - innerTileTopLeftPoint, - heightOfSampleInScreenPixels, - widthOfSampleInScreenPixels, - zoom, - pixelHeight, - pixelWidth, - numberOfSamplesAcross, - numberOfSamplesDown, - ymax, - xmin + const startReadRasters = timed ? performance.now() : 0; + const stack = await this.stack; + const { data: tileRasters }: { data: number[][][] } = await stack.read({ + extent: extentOfInnerTileInMapCRS, + size: [numberOfSamplesAcross, numberOfSamplesDown] + }); + if (timed) + console.log( + `[georaster-layer-for-leaflet] [${cacheKey}] reading rasters took: ${ + performance.now() - startReadRasters + }ms` + ); + if (this.options.onReadRasters) { + this.options.onReadRasters({ + data: tileRasters, + height: numberOfSamplesDown, + width: numberOfSamplesAcross }); - if (tileRasters && this.calcStats) { - const { noDataValue } = this; - for (let bandIndex = 0; bandIndex < tileRasters.length; bandIndex++) { - let min = this.currentStats.mins[bandIndex]; - let max = this.currentStats.maxs[bandIndex]; - const band = tileRasters[bandIndex]; - for (let rowIndex = 0; rowIndex < band.length; rowIndex++) { - const row = band[rowIndex]; - for (let columnIndex = 0; columnIndex < row.length; columnIndex++) { - const value = row[columnIndex]; - if (value !== noDataValue) { - if (min === undefined || value < min) { - min = value; - } - if (max === undefined || value > max) { - max = value; - } + } + + if (debugLevel >= 3) log("tileRasters:", tileRasters); + + if (this.calcStats) { + const start_calc_stats = debugLevel >= 1 ? performance.now() : 0; + const { noDataValue } = this; + for (let bandIndex = 0; bandIndex < tileRasters.length; bandIndex++) { + let min = this.currentStats.mins[bandIndex]; + let max = this.currentStats.maxs[bandIndex]; + const band = tileRasters[bandIndex]; + for (let rowIndex = 0; rowIndex < band.length; rowIndex++) { + const row = band[rowIndex]; + for (let columnIndex = 0; columnIndex < row.length; columnIndex++) { + const value = row[columnIndex]; + if (value !== noDataValue) { + if (min === undefined || value < min) { + min = value; + // invalidate cache because previous tiles used less accurate stats + this._cache = { innerTile: {}, tile: {} }; + } + if (max === undefined || value > max) { + max = value; + // invalidate cache because previous tiles used less accurate stats + this._cache = { innerTile: {}, tile: {} }; + const tiles = this.getActiveTiles(); + + // redraw old tiles + tiles.forEach((tile: Tile) => { + const { coords, el } = tile; + // this.drawTile({ tile: el, coords, context: el.getContext("2d") }); + }); + if (debugLevel >= 1) console.log("redrew tiles"); } } } - this.currentStats.mins[bandIndex] = min; - this.currentStats.maxs[bandIndex] = max; - this.currentStats.ranges[bandIndex] = max - min; } + this.currentStats.mins[bandIndex] = min; + this.currentStats.maxs[bandIndex] = max; + this.currentStats.ranges[bandIndex] = max - min; } if (this._dynamic) { + const rawToRgbFn = (rawToRgb as any).default || rawToRgb; try { - const rawToRgbFn = (rawToRgb as any).default || rawToRgb; this.rawToRgb = rawToRgbFn({ format: "string", flip: this.currentStats.mins.length === 1 ? true : false, @@ -648,120 +768,117 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C round: true }); } catch (error) { - console.error(error); + console.error("[georaster-layer-for-leaflet] error creating rawToRgb"); } } + if (timed) log(`updating statistics took ${performance.now() - start_calc_stats}ms`); } + const start_ycbcr = timed ? performance.now() : 0; await this.checkIfYCbCr; - - for (let h = 0; h < numberOfSamplesDown; h++) { - const yCenterInMapPixels = yTopOfInnerTile + (h + 0.5) * heightOfSampleInScreenPixels; - const latWestPoint = L.point(xLeftOfInnerTile, yCenterInMapPixels); - const { lat } = map.unproject(latWestPoint, zoom); - if (lat > yMinOfLayer && lat < yMaxOfLayer) { - const yInTilePixels = Math.round(h * heightOfSampleInScreenPixels) + Math.min(padding.top, 0); - - let yInRasterPixels = 0; - if (inSimpleCRS || this.projection === EPSG4326) { - yInRasterPixels = Math.floor((yMaxOfLayer - lat) / pixelHeight); - } - - for (let w = 0; w < numberOfSamplesAcross; w++) { - const latLngPoint = L.point( - xLeftOfInnerTile + (w + 0.5) * widthOfSampleInScreenPixels, - yCenterInMapPixels - ); - const { lng: xOfLayer } = map.unproject(latLngPoint, zoom); - if (xOfLayer > xMinOfLayer && xOfLayer < xMaxOfLayer) { - let xInRasterPixels = 0; - if (inSimpleCRS || this.projection === EPSG4326) { - xInRasterPixels = Math.floor((xOfLayer - xMinOfLayer) / pixelWidth); - } else if (this.getProjector()) { - const inverted = this.getProjector().inverse({ x: xOfLayer, y: lat }); - const yInSrc = inverted.y; - yInRasterPixels = Math.floor((ymax - yInSrc) / pixelHeight); - if (yInRasterPixels < 0 || yInRasterPixels >= rasterHeight) continue; - - const xInSrc = inverted.x; - xInRasterPixels = Math.floor((xInSrc - xmin) / pixelWidth); - if (xInRasterPixels < 0 || xInRasterPixels >= rasterWidth) continue; - } - let values = null; - if (tileRasters) { - // get value from array specific to this tile - values = tileRasters.map(band => band[h][w]); - } else if (rasters) { - // get value from array with data for entire raster - values = rasters.map((band: number[][]) => { - return band[yInRasterPixels][xInRasterPixels]; - }); - } else { - done && done(Error("no rasters are available for, so skipping value generation")); - return; - } - - // x-axis coordinate of the starting point of the rectangle representing the raster pixel - const x = Math.round(w * widthOfSampleInScreenPixels) + Math.min(padding.left, 0); - - // y-axis coordinate of the starting point of the rectangle representing the raster pixel - const y = yInTilePixels; - - // how many real screen pixels does a pixel of the sampled raster take up - const width = widthOfSampleInScreenPixelsInt; - const height = heightOfSampleInScreenPixelsInt; - - if (this.options.customDrawFunction) { - this.options.customDrawFunction({ - values, - context, - x, - y, - width, - height, - rasterX: xInRasterPixels, - rasterY: yInRasterPixels, - sampleX: w, - sampleY: h, - sampledRaster: tileRasters - }); - } else { - const color = this.getColor(values); - if (color && context) { - context.fillStyle = color; - context.fillRect(x, y, width, height); - } - } - } - } + if (timed) log(`checking if YCbCr took ${performance.now() - start_ycbcr}ms`); + + const mask = await Promise.resolve(this.mask); + const { pixelValuesToColorFn } = this.options; + + // paint the sampled data onto the canvas with and band math expressions applicable + // note: don't need forward or inverse because tileRasters is already warped to map projection + // note: we don't need to provide in_bbox and out_bbox because same + // - the following aren't applicable when drawing on a canvas: out_array_types, out_no_data, out_layout + // - we don't currently use out_bands and read_bands, but could probably allow users to use that + // don't need to provide out_pixel_depth because geowarp-canvas takes care of that + // - taken care of by geowapr-canvas: out_pixel_depth, out_height, out_width, method + // don't need to do round: true because our pixel-utils expr function takes care of that + // have to provide in_srs and out_srs in order to support clipping + const cutline_forward = mask ? this.getProjector(this.mask_srs, map_crs_code).forward : undefined; + + if (this.debugLevel >= 4 && inSimpleCRS === false && mask) { + try { + const geojson = await reprojectGeoJSON(mask, { from: this.mask_srs, to: 4326 }); + L.geoJSON(geojson, { style: { color: "#AAA", fillOpacity: 0 } }).addTo(this.getMap()); + } catch (error) { + console.error(error); } } - if (this.mask) { - if (inSimpleCRS) { - console.warn("[georaster-layer-for-leaflet] mask is not supported when using simple projection"); + const theoretical_min = this.calcStats ? fastMin(this.currentStats.mins) : undefined; + const theoretical_max = this.calcStats ? fastMax(this.currentStats.maxs) : undefined; + const in_stats = (() => { + if (this.stats) { + return zip(this.stats.mins, this.stats.maxs).map(([min, max]) => ({ min, max })); + } else if (this.calcStats && this.currentStats) { + return zip(this.currentStats.mins, this.currentStats.maxs).map(([min, max]) => ({ min, max })); } else { - this.mask.then((mask: Mask) => { - geocanvas.maskCanvas({ - canvas: tile, - // eslint-disable-next-line camelcase - canvas_bbox: extentOfInnerTileInMapCRS.bbox, // need to support simple projection too - // eslint-disable-next-line camelcase - canvas_srs: 3857, // default map crs, need to support simple - mask, - // eslint-disable-next-line camelcase - mask_srs: this.mask_srs, - strategy: this.mask_strategy // hide everything inside or outside the mask - }); - }); + return undefined; } - } - - tile.style.visibility = "visible"; // set to default + })(); + const draw = !this.options.customDrawFunction + ? undefined + : ({ + // deprecating rasterX and rasterY + context, + pixel, + rect: [x, y, width, height], + sample: [sampleX, sampleY] = [undefined, undefined] + }: { + context: any; + pixel: number[]; + rect: [number, number, number, number]; + sample: [number, number] | [undefined, undefined] | undefined; + }): void => { + this.options.customDrawFunction({ + values: pixel, + context, + x, + y, + width, + height, + sampleX, + sampleY, + sampledRaster: tileRasters + }); + }; + const expr = pixelValuesToColorFn + ? ({ pixel }: { pixel: number[] }) => pixelValuesToColorFn(pixel) + : undefined; + geowarp({ + plugins: ["canvas"], // activate geowarp-canvas plugin + cutline: mask, + cutline_forward, + cutline_strategy: this.mask_strategy, + cutline_srs: this.mask_srs, + debug_level: debugLevel - 1, + in_bbox: extentOfInnerTileInMapCRS.bbox, + in_data: tileRasters, + in_height: numberOfSamplesDown, + in_layout: "[band][row][column]", + in_srs: map_crs_code, + in_stats, + in_width: numberOfSamplesAcross, + out_bbox: extentOfInnerTileInMapCRS.bbox, + out_canvas: tile, + out_resolution: [1, 1], + out_srs: map_crs_code, + draw, + draw_strategy: "canvas", + method: "near", // this is separate from the resampleMethod that does the actual reprojection + theoretical_min, + theoretical_max, + expr, + turbo: this.options.turbo ?? false, + skip_no_data_strategy: true // don't bother trying to render pixels with no data values + }); + tile.style.visibility = "visible"; } catch (e: any) { console.error(e); error = e; } + + if (timed) + console.log( + `[georaster-layer-for-leaflet] [${cacheKey}] creating tile took ${performance.now() - start_draw_tile}ms` + ); + done && done(error, tile); }, 0); @@ -830,7 +947,23 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C }, _isValidTile: function (coords: Coords) { + // console.log("_isValidTile from ", coords) const crs = this.getMapCRS(); + const bounds = this.getBounds(); + + if (!bounds) { + return true; + } + + const { x, y, z } = coords; + + const boundsOfTile = new GeoExtent(this._tileCoordsToBounds(coords)); + + if (isSimpleCRS(crs)) { + // if not within the original confines of the earth return false + // we don't want wrapping if using Simple CRS + return this.simpleExtent.overlaps(boundsOfTile); + } if (!crs.infinite) { // don't load tile if it's out of bounds and not wrapped @@ -843,26 +976,14 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C } } - const bounds = this.getBounds(); - - if (!bounds) { + // check if any raster's extent overlaps the given tile coordinates + // we check in both the srs of the georaster extents and the tile extent + // because sometimes reprojection acts weird around world boundaries + // it's better to try to render empty tiles than miss rendering tiles we should + if (this.subextents.some((extent: any) => extent.overlaps(boundsOfTile))) { return true; } - const { x, y, z } = coords; - - // not sure what srs should be here when simple crs - const layerExtent = new GeoExtent(bounds, { srs: 4326 }); - - const boundsOfTile = this._tileCoordsToBounds(coords); - - // check given tile coordinates - if (layerExtent.overlaps(boundsOfTile)) return true; - - // if not within the original confines of the earth return false - // we don't want wrapping if using Simple CRS - if (isSimpleCRS(crs)) return false; - // width of the globe in tiles at the given zoom level const width = Math.pow(2, z); @@ -870,48 +991,17 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const leftCoords = L.point(x - width, y) as Coords; leftCoords.z = z; const leftBounds = this._tileCoordsToBounds(leftCoords); - if (layerExtent.overlaps(leftBounds)) return true; + if (this.subextents.some((extent: any) => extent.overlaps(leftBounds))) return true; // check one world to the right const rightCoords = L.point(x + width, y) as Coords; rightCoords.z = z; const rightBounds = this._tileCoordsToBounds(rightCoords); - if (layerExtent.overlaps(rightBounds)) return true; + if (this.subextents.some((extent: any) => extent.overlaps(rightBounds))) return true; return false; }, - getColor: function (values: number[]): string | undefined { - if (this.options.pixelValuesToColorFn) { - return this.options.pixelValuesToColorFn(values); - } else { - const numberOfValues = values.length; - const haveDataForAllBands = values.every(value => value !== undefined && value !== this.noDataValue); - if (haveDataForAllBands) { - if (numberOfValues == 1) { - const value = values[0]; - if (this.palette) { - const [r, g, b, a] = this.palette[value]; - return `rgba(${r},${g},${b},${a / 255})`; - } else if (this.georasters[0].mins) { - const { mins, ranges } = this.georasters[0]; - return this.scale((values[0] - mins[0]) / ranges[0]).hex(); - } else if (this.currentStats.mins) { - const min = this.currentStats.mins[0]; - const range = this.currentStats.ranges[0]; - return this.scale((values[0] - min) / range).hex(); - } - } else if (numberOfValues === 2) { - return `rgb(${values[0]},${values[1]},0)`; - } else if (numberOfValues === 3) { - return `rgb(${values[0]},${values[1]},${values[2]})`; - } else if (numberOfValues === 4) { - return `rgba(${values[0]},${values[1]},${values[2]},${values[3] / 255})`; - } - } - } - }, - /** * Redraws the active map tiles updating the pixel values using the supplie callback */ @@ -958,113 +1048,67 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C return tiles.filter(tile => this._isValidTile(tile.coords)); }, - isSupportedProjection: function () { - if (this._isSupportedProjection === undefined) { - const projection = this.projection; - if (isUTM(projection)) { - this._isSupportedProjection = true; - } else if (PROJ4_SUPPORTED_PROJECTIONS.has(projection)) { - this._isSupportedProjection = true; - } else if (typeof proj4FullyLoaded === "function" && `EPSG:${projection}` in proj4FullyLoaded.defs) { - this._isSupportedProjection = true; - } else if ( - typeof proj4 === "function" && - typeof proj4.defs !== "undefined" && - `EPSG:${projection}` in proj4.defs - ) { - this._isSupportedProjection = true; - } else { - this._isSupportedProjection = false; - } - } - return this._isSupportedProjection; - }, - - getProjectionString: function (projection: number) { - if (isUTM(projection)) { - return getProjString(projection); - } - return `EPSG:${projection}`; + isSupportedProjection: function (proj: number | string) { + return isUTM(proj) || proj in this.proj4.defs || `EPSG:${proj}` in this.proj4.defs; }, initBounds: function (options: GeoRasterLayerOptions) { if (!options) options = this.options; + + const maxEasting = Math.max(...this.georasters.map((georaster: any) => georaster.width)); + const maxNorthing = Math.max(...this.georasters.map((georaster: any) => georaster.height)); + const maxValue = Math.max(maxEasting, maxNorthing); + const aspect_ratio = this.width / this.height; + + // want a little padding, so all tiles appear when fit bounds + // const maxBounds = Math.round(maxValue * 0.5); + const maxBounds = maxValue; + if (!this._bounds) { - const { debugLevel, height, width, projection, xmin, xmax, ymin, ymax } = this; - // check if map using Simple CRS - if (isSimpleCRS(this.getMapCRS())) { - if (height === width) { - this._bounds = L.latLngBounds([ORIGIN, [MAX_NORTHING, MAX_EASTING]]); - } else if (height > width) { - this._bounds = L.latLngBounds([ORIGIN, [MAX_NORTHING, MAX_EASTING / this.ratio]]); - } else if (width > height) { - this._bounds = L.latLngBounds([ORIGIN, [MAX_NORTHING * this.ratio, MAX_EASTING]]); + const map_crs = this.getMapCRS(); + if (isSimpleCRS(map_crs)) { + if (maxEasting === maxNorthing) { + this._bounds = L.latLngBounds([ORIGIN, [maxBounds, maxBounds]]); + } else if (maxNorthing > maxEasting) { + this._bounds = L.latLngBounds([ORIGIN, [maxBounds, maxBounds * aspect_ratio]]); + } else if (maxEasting > maxNorthing) { + this._bounds = L.latLngBounds([ORIGIN, [maxBounds / aspect_ratio, maxBounds]]); } - } else if (projection === EPSG4326) { - if (debugLevel >= 1) console.log(`georaster projection is in ${EPSG4326}`); - const minLatWest = L.latLng(ymin, xmin); - const maxLatEast = L.latLng(ymax, xmax); - this._bounds = L.latLngBounds(minLatWest, maxLatEast); - } else if (this.getProjector()) { - if (debugLevel >= 1) console.log("projection is UTM or supported by proj4"); - const bottomLeft = this.getProjector().forward({ x: xmin, y: ymin }); - const minLatWest = L.latLng(bottomLeft.y, bottomLeft.x); - const topRight = this.getProjector().forward({ x: xmax, y: ymax }); - const maxLatEast = L.latLng(topRight.y, topRight.x); - this._bounds = L.latLngBounds(minLatWest, maxLatEast); + } else if (isDefaultCRS(map_crs)) { + const bboxes_in_map_crs = this.subextents.map((extent: any) => { + try { + return extent.reproj(4326, { quiet: false }).bbox; + } catch (error) { + throw "GeoRasterLayer ran into an issue reprojecting. Try adding the projection definition to your global proj4."; + } + }); + const [xmin, ymin, xmax, ymax] = bboxMerge(bboxes_in_map_crs); + this._bounds = L.latLngBounds([ + [ymin, xmin], + [ymax, xmax] + ]); } else { - if (typeof proj4FullyLoaded !== "function") { - throw `You are using the lite version of georaster-layer-for-leaflet, which does not support rasters with the projection ${projection}. Please try using the default build or add the projection definition to your global proj4.`; - } else { - throw `GeoRasterLayer does not provide built-in support for rasters with the projection ${projection}. Add the projection definition to your global proj4.`; - } + // set bounds in crs of map + // maybe need to not rely on GeoExtent.reproj and instead use bbox-fns reproj with getProjector result + const { code } = map_crs; + const bboxes_in_map_crs = this.subextents.map((extent: any) => { + try { + return extent.reproj(code, { quiet: false }).bbox; + } catch (error) { + throw "GeoRasterLayer ran into an issue reprojecting. Try adding the projection definition to your global proj4."; + } + }); + const [xmin, ymin, xmax, ymax] = bboxMerge(bboxes_in_map_crs); + this._bounds = L.bounds([ + [xmin, ymin], + [xmax, ymax] + ]); } - // these values are used so we don't try to sample outside of the raster - this.xMinOfLayer = this._bounds.getWest(); - this.xMaxOfLayer = this._bounds.getEast(); - this.yMaxOfLayer = this._bounds.getNorth(); - this.yMinOfLayer = this._bounds.getSouth(); - + // not sure if/why this is necessary options.bounds = this._bounds; - } - }, - getProjector: function () { - if (this.isSupportedProjection()) { - if (!proj4FullyLoaded && !proj4) { - throw "proj4 must be found in the global scope in order to load a raster that uses this projection"; - } - if (!this._projector) { - const projString = this.getProjectionString(this.projection); - if (this.debugLevel >= 1) log({ projString }); - let proj4Lib; - if (projString.startsWith("EPSG")) { - if (typeof proj4 === "function" && typeof proj4.defs === "function" && projString in proj4.defs) { - proj4Lib = proj4; - } else if ( - typeof proj4FullyLoaded === "function" && - typeof proj4FullyLoaded.defs === "function" && - projString in proj4FullyLoaded.defs - ) { - proj4Lib = proj4FullyLoaded; - } else { - throw "[georaster-layer-for-leaflet] projection not found in proj4 instance"; - } - } else { - if (typeof proj4 === "function") { - proj4Lib = proj4; - } else if (typeof proj4FullyLoaded === "function") { - proj4Lib = proj4FullyLoaded; - } else { - throw "[georaster-layer-for-leaflet] projection not found in proj4 instance"; - } - } - this._projector = proj4Lib(projString, `EPSG:${EPSG4326}`); - - if (this.debugLevel >= 1) console.log("projector set"); - } - return this._projector; + if (this.debugLevel >= 1) console.log("bounds were intialized to:", this._bounds); } }, diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts index 4e95ebb..71f8117 100644 --- a/src/types/declarations.d.ts +++ b/src/types/declarations.d.ts @@ -1,6 +1,26 @@ declare module "utm-utils/src/isUTM.js"; declare module "utm-utils/src/getProjString.js"; declare module "proj4-fully-loaded"; +declare module "bbox-fns"; +declare module "bbox-fns/dense-polygon.js"; +declare module "bbox-fns/merge.js"; +declare module "bbox-fns/shift.js"; +declare module "bbox-fns/split.js"; +declare module "bbox-fns/union.js"; +declare module "bbox-fns/unwrap.js"; declare module "geocanvas"; declare module "geo-extent"; +declare module "georaster-stack/web"; +declare module "geotiff"; +declare module "geotiff-read-bbox"; +declare module "geowarp"; +declare module "geowarp-canvas"; +declare module "reproject-bbox/pluggable.js"; declare module "snap-bbox"; +declare module "to-canvas"; +declare module "to-canvas/to-canvas.min.js"; +declare module "geotiff-tile-web-worker"; +declare module "proj4"; +declare module "proj4-collect"; +declare module "proj4-merge"; +declare module "reproject-geojson"; diff --git a/src/types/index.ts b/src/types/index.ts index a429a10..10b1803 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,7 +15,7 @@ export type SimplePoint = { y: number; }; -export type Mask = string | Feature | FeatureCollection | Polygon | MultiPolygon; +export type Mask = string | Feature | FeatureCollection | Polygon | MultiPolygon | "auto"; interface GeoRasterLayerOptions_CommonOptions extends GridLayerOptions { resolution?: number | { [key: number]: number }; @@ -27,6 +27,7 @@ interface GeoRasterLayerOptions_CommonOptions extends GridLayerOptions { mask?: Mask; mask_srs?: string | number; mask_strategy?: MaskStrategy; + turbo?: boolean | undefined; updateWhenIdle?: boolean; // inherited from LeafletJS updateWhenZooming?: boolean; // inherited from LeafletJS keepBuffer?: number; // inherited from LeafletJS diff --git a/tests/arrayBuffer.html b/tests/arrayBuffer.html index 2a61ef3..4331d29 100644 --- a/tests/arrayBuffer.html +++ b/tests/arrayBuffer.html @@ -18,7 +18,7 @@ - + - + - + - + - + - + - + + + diff --git a/tests/color-scale.2.html b/tests/color-scale.2.html index 65b8127..5ae42a5 100644 --- a/tests/color-scale.2.html +++ b/tests/color-scale.2.html @@ -18,11 +18,16 @@ - + + diff --git a/tests/color-scale.html b/tests/color-scale.html index 2984dcf..f285967 100644 --- a/tests/color-scale.html +++ b/tests/color-scale.html @@ -18,7 +18,7 @@ - + - + diff --git a/tests/global-mask-outside.html b/tests/global-mask-outside.html index a6daf85..1b87881 100644 --- a/tests/global-mask-outside.html +++ b/tests/global-mask-outside.html @@ -19,7 +19,7 @@ - + - + - + + - + + - + - + - + - + - + - + - + - + - + - + diff --git a/tests/simple.html b/tests/simple.html index d5a6be0..8a22ea7 100644 --- a/tests/simple.html +++ b/tests/simple.html @@ -20,7 +20,7 @@ - + - + + - + + diff --git a/tsconfig.json b/tsconfig.json index 8b08faf..75acde0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ // "incremental": true, /* Enable incremental compilation */ "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "es2020" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "lib": ["es2017", "es7", "es6", "dom"] /* Specify library files to be included in the compilation. */, + "lib": ["es2020", "es2017", "es7", "es6", "dom"] /* Specify library files to be included in the compilation. */, // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ @@ -19,7 +19,7 @@ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - "removeComments": false /* Do not emit comments to output. */, + "removeComments": true /* Do not emit comments to output. */, // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ @@ -48,7 +48,7 @@ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": ["types"] /* List of folders to include type definitions from. */, - "types": ["node"] /* Type declaration files to be included in compilation. */, + "types": [] /* Type declaration files to be included in compilation. */, "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ @@ -68,6 +68,6 @@ // "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "include": ["./src/**/*"], + "include": ["./src/*", "./src/**/*"], "exclude": [".pnpm", "node_modules", "dist"] } From 1545ad75e8e56ccdfe85b49cde379441362ba5d4 Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Sun, 3 Dec 2023 19:22:24 -0500 Subject: [PATCH 02/15] fixed redrawing --- package.json | 6 +- src/georaster-layer-for-leaflet.ts | 83 ++++++++++---- tests/color-scale.2.html | 49 --------- tests/color-scale.html | 13 +-- tests/dynamic_threshold_ndvi.html | 108 ------------------- tests/dynamic_threshold_ndvi_no_options.html | 105 ------------------ tests/geowarp-cog.html | 44 -------- tests/global-mask-inside.html | 6 +- tests/landsat-single-band.html | 2 +- 9 files changed, 72 insertions(+), 344 deletions(-) delete mode 100644 tests/dynamic_threshold_ndvi.html delete mode 100644 tests/dynamic_threshold_ndvi_no_options.html delete mode 100644 tests/geowarp-cog.html diff --git a/package.json b/package.json index 1484f2b..cd145af 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@types/geojson": "^7946.0.13", "bbox-fns": "^0.19.0", "geo-extent": "^1.3.1", - "georaster-stack": "^0.4.3", + "georaster-stack": "^0.4.6", "geotiff-epsg-code": "^0.3.1", "geotiff-read-bbox": "^2.2.0", "geowarp": "^1.23.1", @@ -101,8 +101,8 @@ "babel-loader": "^9.1.3", "concurrently": "^8.2.2", "envisage": "^0.1.0", - "eslint": "^8.54.0", - "eslint-config-prettier": "^9.0.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", "fast-max": "^0.4.0", "fast-min": "^0.3.0", diff --git a/src/georaster-layer-for-leaflet.ts b/src/georaster-layer-for-leaflet.ts index 2b11dae..6b8d669 100644 --- a/src/georaster-layer-for-leaflet.ts +++ b/src/georaster-layer-for-leaflet.ts @@ -312,6 +312,7 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C this.mask_srs = "EPSG:4326"; } } + this.mask_strategy = options.mask_strategy; }, getProjDef: function (proj: number | string) { @@ -354,6 +355,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const coordsKey = this._tileCoordsToKey(coords); const resolution = this._getResolution(coords.z); + if (resolution === undefined) throw new Error("[georaster-layer-for-leaflet] resolution is undefined"); + + // saving resolution, which we will need later if/when redrawing + (tile as any).resolution = resolution; + const key = `${coordsKey}:${resolution}`; const doneCb = (error?: Error, tile?: HTMLElement): void => { done(error, tile); @@ -381,7 +387,7 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const timed = debugLevel >= 1; - if (debugLevel >= 2) console.log("starting drawTile with", { tile, coords, context, done }); + if (debugLevel >= 2) console.log("starting drawTile with", { tile, coords, context, done, resolution }); let error: Error; @@ -390,6 +396,10 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C const cacheKey = [z, x, y].join("/"); + if (isNaN(resolution)) { + throw new Error(`[georaster-layer-for-leafler] [${cacheKey}] resolution isNaN`); + } + if (this.options._valid_tiles && !this.options._valid_tiles.includes(cacheKey)) return; // over-ride default log with tile coordinate info @@ -595,6 +605,12 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C console.log(`[georaster-layer-for-leaflet] [${cacheKey}] numberOfSamplesDown: ${numberOfSamplesDown}`); } + if (isNaN(numberOfSamplesAcross)) { + throw new Error( + `[georaster-layer-for-leaflet [${cacheKey}] numberOfSamplesAcross is NaN when resolution=${resolution} and extentOfInnerTileInMapCRS.width=${extentOfInnerTileInMapCRS.width} and extentOfTileInMapCRS.width=${extentOfTileInMapCRS.width}` + ); + } + if (debugLevel >= 3) { console.log( "[georaster-layer-for-leaflet] extent of inner tile before snapping " + @@ -701,16 +717,26 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C try { const startReadRasters = timed ? performance.now() : 0; const stack = await this.stack; + const stack_size = [numberOfSamplesAcross, numberOfSamplesDown]; const { data: tileRasters }: { data: number[][][] } = await stack.read({ extent: extentOfInnerTileInMapCRS, - size: [numberOfSamplesAcross, numberOfSamplesDown] + size: stack_size }); - if (timed) - console.log( - `[georaster-layer-for-leaflet] [${cacheKey}] reading rasters took: ${ - performance.now() - startReadRasters - }ms` + + if (tileRasters === undefined) { + throw new Error( + `tileRasters is undefined when extent is ${extentOfInnerTileInMapCRS.js} and size is ${JSON.stringify([ + numberOfSamplesAcross, + numberOfSamplesDown + ])}` ); + } + + if (timed) { + const durationReadRasters = performance.now() - startReadRasters; + console.log(`[georaster-layer-for-leaflet] [${cacheKey}] reading rasters took: ${durationReadRasters}ms`); + } + if (this.options.onReadRasters) { this.options.onReadRasters({ data: tileRasters, @@ -724,6 +750,7 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C if (this.calcStats) { const start_calc_stats = debugLevel >= 1 ? performance.now() : 0; const { noDataValue } = this; + const original_ranges: number[] = Array.from(this.currentStats.ranges); for (let bandIndex = 0; bandIndex < tileRasters.length; bandIndex++) { let min = this.currentStats.mins[bandIndex]; let max = this.currentStats.maxs[bandIndex]; @@ -735,21 +762,9 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C if (value !== noDataValue) { if (min === undefined || value < min) { min = value; - // invalidate cache because previous tiles used less accurate stats - this._cache = { innerTile: {}, tile: {} }; } if (max === undefined || value > max) { max = value; - // invalidate cache because previous tiles used less accurate stats - this._cache = { innerTile: {}, tile: {} }; - const tiles = this.getActiveTiles(); - - // redraw old tiles - tiles.forEach((tile: Tile) => { - const { coords, el } = tile; - // this.drawTile({ tile: el, coords, context: el.getContext("2d") }); - }); - if (debugLevel >= 1) console.log("redrew tiles"); } } } @@ -758,6 +773,34 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C this.currentStats.maxs[bandIndex] = max; this.currentStats.ranges[bandIndex] = max - min; } + + let redraw = false; + for (let bandIndex = 0; bandIndex < tileRasters.length; bandIndex++) { + const old_range = original_ranges[bandIndex]; + const new_range = this.currentStats.ranges[bandIndex]; + const diff_range = new_range - old_range; + const percentage_change = diff_range / old_range; + const threshold = 1 / 256; + if (percentage_change > threshold) { + redraw = true; + break; + } + } + + if (redraw) { + if (debugLevel >= 1) console.log("[georaster-layer-for-leaflet] redrawing tiles"); + // invalidate cache because previous tiles used less accurate stats + this._cache = { innerTile: {}, tile: {} }; + const tiles = this.getActiveTiles(); + + // redraw old tiles + tiles.forEach((tile: Tile) => { + const { coords, el } = tile; + this.drawTile({ tile: el, coords, context: el.getContext("2d"), resolution: (el as any).resolution }); + }); + if (debugLevel >= 1) console.log("[georaster-layer-for-leaflet] finished redrawing tiles"); + } + if (this._dynamic) { const rawToRgbFn = (rawToRgb as any).default || rawToRgb; try { @@ -1031,7 +1074,7 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C tiles.forEach((tile: Tile) => { const { coords, el } = tile; - this.drawTile({ tile: el, coords, context: el.getContext("2d") }); + this.drawTile({ tile: el, coords, context: el.getContext("2d"), resolution: (el as any).resolution }); }); if (debugLevel >= 1) console.log("Finished updating active tile colours"); return this; diff --git a/tests/color-scale.2.html b/tests/color-scale.2.html index 5ae42a5..33c40fc 100644 --- a/tests/color-scale.2.html +++ b/tests/color-scale.2.html @@ -23,11 +23,7 @@ - - - - - - - - diff --git a/tests/dynamic_threshold_ndvi_no_options.html b/tests/dynamic_threshold_ndvi_no_options.html deleted file mode 100644 index d2de9a6..0000000 --- a/tests/dynamic_threshold_ndvi_no_options.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - -
-
- -
- - - - - - - - - diff --git a/tests/geowarp-cog.html b/tests/geowarp-cog.html deleted file mode 100644 index 08a34fe..0000000 --- a/tests/geowarp-cog.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - -
- - - - - - - diff --git a/tests/global-mask-inside.html b/tests/global-mask-inside.html index 52c4193..bc3c1ee 100644 --- a/tests/global-mask-inside.html +++ b/tests/global-mask-inside.html @@ -38,7 +38,7 @@ parseGeoraster(window.location.origin + "/tests/spam2010V1r1_global_H_WHEA_A.tif").then(async function (georaster) { console.log("georaster:", georaster); var layer = new GeoRasterLayer({ - debugLevel: 10, + debugLevel: 0, georaster: georaster, resolution: Number(new URL(window.location.href).searchParams.get("resolution") || 512), pixelValuesToColorFn: function (values) { @@ -47,8 +47,8 @@ } if (values[0] > 200) return "brown"; }, - // mask: "./usa.geojson", - // mask_strategy: "inside" + mask: "./usa.geojson", + mask_strategy: "inside" }); layer.addTo(map); }); diff --git a/tests/landsat-single-band.html b/tests/landsat-single-band.html index 8f2a1ca..a8c32f2 100644 --- a/tests/landsat-single-band.html +++ b/tests/landsat-single-band.html @@ -32,7 +32,7 @@ parseGeoraster(url_to_geotiff_file).then(function (georaster) { console.log("georaster:", georaster); const layer = new GeoRasterLayer({ - debugLevel: 0, + debugLevel: 10, georaster, resolution: 512 }); From 938ac8f4569356c9a9e8feab2ab6b577222b718c Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Wed, 6 Dec 2023 21:15:42 -0500 Subject: [PATCH 03/15] updated deps --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index cd145af..c6ace8d 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "geotiff-epsg-code": "^0.3.1", "geotiff-read-bbox": "^2.2.0", "geowarp": "^1.23.1", - "geowarp-canvas": "link:../../DanielJDufour/geowarp-canvas", + "geowarp-canvas": "^0.0.1", "memoizee": "^0.4.15", "pixel-utils": "^0.9.0", "proj4": "^2.9.2", @@ -96,8 +96,8 @@ "@babel/preset-typescript": "^7.23.3", "@types/chroma-js": "^2.4.3", "@types/leaflet": "^1.9.8", - "@typescript-eslint/eslint-plugin": "^6.13.1", - "@typescript-eslint/parser": "^6.13.1", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", "babel-loader": "^9.1.3", "concurrently": "^8.2.2", "envisage": "^0.1.0", @@ -115,7 +115,7 @@ "reproject-geojson": "^0.5.0", "rimraf": "^5.0.5", "to-canvas": "^0.2.0", - "typescript": "^5.3.2", + "typescript": "^5.3.3", "webpack": "^5.89.0", "webpack-cli": "^5.1.4" }, From 83508bc8e6a41ab96dcdd8389bb5dce0065ff7b7 Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Wed, 6 Dec 2023 21:26:25 -0500 Subject: [PATCH 04/15] updated tests --- src/georaster-layer-for-leaflet.ts | 16 +++++++++------- tests/arrayBuffer.html | 4 ++-- tests/cog-single-band-canopy.html | 1 + tests/file-input.html | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/georaster-layer-for-leaflet.ts b/src/georaster-layer-for-leaflet.ts index 6b8d669..32b030d 100644 --- a/src/georaster-layer-for-leaflet.ts +++ b/src/georaster-layer-for-leaflet.ts @@ -200,7 +200,6 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C } this.stats.ranges = zip(this.stats.mins, this.stats.maxs).map(([min, max]) => max - min); }); - console.log("this.stats:", this.stats); } // in-case we want to track dynamic/running stats of all pixels fetched @@ -332,7 +331,6 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C }, createTile: function (coords: Coords, done: DoneCallback) { - console.log("starting createTile with coords:", coords); /* This tile is the square piece of the Leaflet map that we draw on */ const tile = L.DomUtil.create("canvas", "leaflet-tile") as HTMLCanvasElement; @@ -591,18 +589,17 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C extentOfInnerTileInMapCRS = new GeoExtent(newbox, { srs: extentOfInnerTileInMapCRS.srs }); } } else { - // even if we aren't doing the more advacned sample alignment above + // even if we aren't doing the more advanced sample alignment above // we should still factor in the resolution when determing the resolution of the sampled rasters // for example, if the inner tile only takes up 10% of the total tile container space, // we shouldn't sample 256 times across numberOfSamplesAcross = Math.ceil(resolution * (extentOfInnerTileInMapCRS.width / extentOfTileInMapCRS.width)); numberOfSamplesDown = Math.ceil(resolution * (extentOfInnerTileInMapCRS.height / extentOfTileInMapCRS.height)); - log(`Math.ceil(${resolution} * (${extentOfInnerTileInMapCRS.width} / ${extentOfTileInMapCRS.width}))`); - if (debugLevel >= 2) + if (debugLevel >= 2) { console.log(`[georaster-layer-for-leaflet] [${cacheKey}] numberOfSamplesAcross: ${numberOfSamplesAcross}`); - if (debugLevel >= 2) console.log(`[georaster-layer-for-leaflet] [${cacheKey}] numberOfSamplesDown: ${numberOfSamplesDown}`); + } } if (isNaN(numberOfSamplesAcross)) { @@ -806,7 +803,12 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C try { this.rawToRgb = rawToRgbFn({ format: "string", - flip: this.currentStats.mins.length === 1 ? true : false, + flip: + typeof this.options.flip === "boolean" + ? this.options.flip + : this.currentStats.mins.length === 1 + ? true + : false, ranges: zip(this.currentStats.mins, this.currentStats.maxs), round: true }); diff --git a/tests/arrayBuffer.html b/tests/arrayBuffer.html index 4331d29..df4fada 100644 --- a/tests/arrayBuffer.html +++ b/tests/arrayBuffer.html @@ -39,8 +39,8 @@ parseGeoraster(arrayBuffer).then(function (georaster) { console.log("georaster:", georaster); var layer = new GeoRasterLayer({ - debugLevel: 1, - georaster: georaster, + debugLevel: 0, + georaster, resolution: 512, resampleMethod: "near" }); diff --git a/tests/cog-single-band-canopy.html b/tests/cog-single-band-canopy.html index 0044880..05bce4c 100644 --- a/tests/cog-single-band-canopy.html +++ b/tests/cog-single-band-canopy.html @@ -34,6 +34,7 @@ console.log("georaster:", georaster); const layer = new GeoRasterLayer({ debugLevel: 4, + flip: false, georaster, resolution: 512 }); diff --git a/tests/file-input.html b/tests/file-input.html index d6f163d..729dff8 100644 --- a/tests/file-input.html +++ b/tests/file-input.html @@ -30,7 +30,7 @@

Load a GeoTIFF File

- + + + + + + + + + + + \ No newline at end of file From 1cade6bacf0df6a3c2ce08f7aac3eca71e20c74f Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Tue, 23 Jan 2024 21:48:03 -0500 Subject: [PATCH 14/15] 4.1.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eed7fd..e200573 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "georaster-layer-for-leaflet", - "version": "4.0.0-1", + "version": "4.1.0-0", "description": "Display GeoTIFFs and soon other types of raster on your Leaflet Map", "type": "module", "main": "dist/v3/webpack/bundle/georaster-layer-for-leaflet.min.js", From c4a78310960a7e51a8a56371c1fc0f3e9c38b8f2 Mon Sep 17 00:00:00 2001 From: DanielJDufour Date: Sun, 4 Feb 2024 11:16:47 -0500 Subject: [PATCH 15/15] improved backwards compatibility --- package.json | 19 ++++++------- src/georaster-layer-for-leaflet.ts | 45 ++++++++++++++++++++++++------ tests/arrayBuffer.html | 2 +- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index e200573..82850b7 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ }, "homepage": "https://github.com/GeoTIFF/georaster-layer-for-leaflet#readme", "dependencies": { - "@types/geojson": "^7946.0.13", + "@types/geojson": "^7946.0.14", "bbox-fns": "^0.20.2", "fast-max": "^0.5.1", "fast-min": "^0.4.0", @@ -78,7 +78,7 @@ "geotiff-epsg-code": "^0.3.1", "geotiff-read-bbox": "^2.2.0", "geowarp": "^1.26.1", - "geowarp-canvas": "^0.1.0", + "geowarp-canvas": "^0.2.0", "memoizee": "^0.4.15", "pixel-utils": "^0.9.0", "proj4": "^2.10.0", @@ -92,17 +92,16 @@ "xdim": "^1.10.1" }, "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.7", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.23.9", "@babel/plugin-transform-export-namespace-from": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/preset-env": "^7.23.8", + "@babel/preset-env": "^7.23.9", "@babel/preset-typescript": "^7.23.3", - "@types/chroma-js": "^2.4.3", "@types/leaflet": "^1.9.8", - "@typescript-eslint/eslint-plugin": "^6.19.1", - "@typescript-eslint/parser": "^6.19.1", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", "babel-loader": "^9.1.3", "concurrently": "^8.2.2", "envisage": "^0.1.0", @@ -114,12 +113,12 @@ "leaflet": "^1.9.4", "mkdirp": "^3.0.1", "null-loader": "^4.0.1", - "prettier": "^3.2.4", + "prettier": "^3.2.5", "reproject-geojson": "^0.5.0", "rimraf": "^5.0.5", "to-canvas": "^0.2.0", "typescript": "^5.3.3", - "webpack": "^5.89.0", + "webpack": "^5.90.1", "webpack-cli": "^5.1.4" }, "peerDependencies": { diff --git a/src/georaster-layer-for-leaflet.ts b/src/georaster-layer-for-leaflet.ts index 0446c3c..9e3b454 100644 --- a/src/georaster-layer-for-leaflet.ts +++ b/src/georaster-layer-for-leaflet.ts @@ -335,7 +335,7 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C this.mask_strategy = options.mask_strategy; }, - getProjDef: function (proj: number | string) { + getProjectionString: function (proj: number | string) { if (isUTM(proj)) return getProjString(proj); if (typeof proj === "number") proj = "EPSG:" + proj; if (proj in this.proj4.defs) return proj; @@ -343,12 +343,20 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C throw new Error("[georaster-layer-for-leaflet] unsupported projection:" + proj); }, - getProjector: function (_from: number | string, _to: number | string) { + getProjector: function (_from: number | string, _to: number | string = "EPSG:4326") { + if (_from === null || _from === undefined) { + if (new Set(this.georasters.map((georaster: any) => georaster.projection)).size !== 1) { + throw new Error("[georaster-layer-for-leaflet] getProjector called without a _from and georasters don't all have the same projection"); + } else { + _from = this.georasters[0].projection; + } + } + if (!this.isSupportedProjection(_from)) { throw Error("[georaster-layer-for-leaflet] unsupported projection: " + _from); } if (!this.isSupportedProjection(_to)) throw Error("[georaster-layer-for-leaflet] unsupported projection: " + _to); - return this.proj4(this.getProjDef(_from), this.getProjDef(_to)); + return this.proj4(this.getProjectionString(_from), this.getProjectionString(_to)); }, createTile: function (coords: Coords, done: DoneCallback) { @@ -566,7 +574,14 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C // const defaultCanvasWidth = Math.max(256, this.resolution || 256); // const widthOfCanvasPixelInMapCRS = extentOfTileInMapCRS.width / defaultCanvasHeight; // const heightOfCanvasPixelInMapCRS = extentOfTileInMapCRS.height / defaultCanvasWidth; - if (debugLevel >= 3) log({ heightOfScreenPixelInMapCRS, widthOfScreenPixelInMapCRS }); + if (debugLevel >= 3) + console.log( + `[georaster-layer-for-leaflet] [${cacheKey}] heightOfScreenPixelInMapCRS: ${heightOfScreenPixelInMapCRS}` + ); + if (debugLevel >= 3) + console.log( + `[georaster-layer-for-leaflet] [${cacheKey}] widthOfScreenPixelInMapCRS: ${widthOfScreenPixelInMapCRS}` + ); // even if we aren't doing the more advanced sample alignment above // we should still factor in the resolution when determing the resolution of the sampled rasters @@ -579,10 +594,13 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C resolution * (extentOfInnerTileInMapCRS.height / extentOfTileInMapCRS.height) ); - const skew = map_crs_code === ("EPSG:" + this.georasters[0].projection) ? [0, 0] : measureSkew( - [this.georasters[0].xmin, this.georasters[0].ymin, this.georasters[0].xmax, this.georasters[0].ymax], - this.getProjector(this.georasters[0].projection, map_crs_code).forward - ); + const skew = + map_crs_code === "EPSG:" + this.georasters[0].projection + ? [0, 0] + : measureSkew( + [this.georasters[0].xmin, this.georasters[0].ymin, this.georasters[0].xmax, this.georasters[0].ymax], + this.getProjector(this.georasters[0].projection, map_crs_code).forward + ); if (debugLevel >= 2) console.log(`[georaster-layer-for-leaflet] [${cacheKey}] skew:`, skew); if ( @@ -978,7 +996,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C theoretical_max, expr, turbo: this.options.turbo ?? false, - skip_no_data_strategy: "any" // don't bother trying to render pixels with no data values + skip_no_data_strategy: "any", // don't bother trying to render pixels with no data values + before_warp: (options: any) => { + // provide backwards compatability + this.getColor = (pixel: number[]) => options.expr({ pixel }); + } }); tile.style.visibility = "visible"; } catch (e: any) { @@ -1146,6 +1168,11 @@ const GeoRasterLayer: (new (options: GeoRasterLayerOptions) => any) & typeof L.C return false; }, + // provided for backwards compatability + getColor: function (values: number[]): string | undefined { + throw new Error("[georaster-layer-for-leaflet] please call getColor after creating at least one tile"); + }, + /** * Redraws the active map tiles updating the pixel values using the supplie callback */ diff --git a/tests/arrayBuffer.html b/tests/arrayBuffer.html index df4fada..6f2b551 100644 --- a/tests/arrayBuffer.html +++ b/tests/arrayBuffer.html @@ -38,7 +38,7 @@ .then(function (arrayBuffer) { parseGeoraster(arrayBuffer).then(function (georaster) { console.log("georaster:", georaster); - var layer = new GeoRasterLayer({ + layer = new GeoRasterLayer({ debugLevel: 0, georaster, resolution: 512,