Skip to content

Commit

Permalink
Merge branch 'main' into add-configParameters-leafletMaxZoom
Browse files Browse the repository at this point in the history
  • Loading branch information
glughi authored Oct 22, 2024
2 parents a019cdd + 65d1101 commit 676d1d8
Show file tree
Hide file tree
Showing 39 changed files with 626 additions and 44 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#### next release (8.7.8)

- Add support for Cloud Optimised Geotiff (cog) in Cesium mode. Currently supports EPSG 4326 and 3857. There is experimental support for other projections but performance might suffer and there could be other issues.
- Fix `Workbench.collapseAll()` and `Workbench.expandAll()` for References.
- Add to the "doZoomTo" function the case of an imagery layer with imageryProvider.rectangle
- Add "leafletMaxZoom" to configParameters so that the maxZoom of the Leaflet viewer can be changed.
- [The next improvement]

Expand Down
1 change: 0 additions & 1 deletion buildprocess/createKarmaBaseConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = function (config) {
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine"],
listenAddress: "::",

// list of files / patterns to load in the browser
files: [
Expand Down
5 changes: 5 additions & 0 deletions lib/Core/getDataType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ const builtinRemoteDataTypes: RemoteDataType[] = [
value: "json",
name: "core.dataType.json"
},
{
value: "cog",
name: "core.dataType.cog"
},
{
value: "i3s",
name: "core.dataType.i3s"
Expand Down Expand Up @@ -191,6 +195,7 @@ const builtinLocalDataTypes: LocalDataType[] = [
name: "core.dataType.shp",
extensions: ["zip"]
}

// Add next builtin local upload type
];

Expand Down
4 changes: 2 additions & 2 deletions lib/Map/DragPoints/CesiumDragPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ var CesiumDragPoints = function (terria, pointMovedCallback) {

/**
* Whether user is currently dragging point.
* @type {Bool}
* @type {Boolean}
*/
this._dragInProgress = false;

Expand Down Expand Up @@ -153,7 +153,7 @@ CesiumDragPoints.prototype.destroy = function () {

/**
* Enable or disable camera motion, so that the user can drag a point rather than dragging the map.
* @param {Bool} state True to enable and false to disable camera motion.
* @param {Boolean} state True to enable and false to disable camera motion.
* @private
*/
CesiumDragPoints.prototype._setCameraMotion = function (state) {
Expand Down
2 changes: 1 addition & 1 deletion lib/Map/DragPoints/LeafletDragPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var LeafletDragPoints = function (terria, pointMovedCallback) {

/**
* Whether user is currently dragging point.
* @type {Bool}
* @type {Boolean}
*/
this._dragInProgress = false;

Expand Down
1 change: 1 addition & 0 deletions lib/Map/Leaflet/LeafletScene.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CesiumEvent from "terriajs-cesium/Source/Core/Event";
import L from "leaflet";

export default class LeafletScene {
readonly map: L.Map;
Expand Down
2 changes: 1 addition & 1 deletion lib/ModelMixins/TableMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function TableMixin<T extends AbstractConstructor<BaseType>>(Base: T) {
}

return {
name: (this.name || this.uniqueId)!,
name: name,
file: new Blob([csvString])
};
}
Expand Down
243 changes: 243 additions & 0 deletions lib/Models/Catalog/CatalogItems/CogCatalogItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import i18next from "i18next";
import {
computed,
makeObservable,
observable,
onBecomeObserved,
onBecomeUnobserved,
runInAction
} from "mobx";
import {
GeographicTilingScheme,
WebMercatorTilingScheme
} from "terriajs-cesium";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import type TIFFImageryProvider from "terriajs-tiff-imagery-provider";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import MappableMixin, { MapItem } from "../../../ModelMixins/MappableMixin";
import CogCatalogItemTraits from "../../../Traits/TraitsClasses/CogCatalogItemTraits";
import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import StratumOrder from "../../Definition/StratumOrder";
import Terria from "../../Terria";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";

/**
* Loadable stratum for overriding CogCatalogItem traits
*/
class CogLoadableStratum extends LoadableStratum(CogCatalogItemTraits) {
static stratumName = "cog-loadable-stratum";

constructor(readonly model: CogCatalogItem) {
super();
makeObservable(this);
}

duplicateLoadableStratum(model: BaseModel): this {
return new CogLoadableStratum(model as CogCatalogItem) as this;
}

@computed
get shortReport(): string | undefined {
return this.model.terria.currentViewer.type === "Leaflet"
? // Warn for 2D mode
i18next.t("models.commonModelErrors.3dTypeIn2dMode", this)
: this.model._imageryProvider?.tilingScheme &&
// Show warning for experimental reprojection freature if not using EPSG 3857 or 4326
isCustomTilingScheme(this.model._imageryProvider?.tilingScheme)
? i18next.t("models.cogCatalogItem.experimentalReprojectionWarning", this)
: undefined;
}

@computed
get rectangle(): StratumFromTraits<RectangleTraits> | undefined {
const rectangle = this.model._imageryProvider?.rectangle;
if (!rectangle) {
return;
}

const { west, south, east, north } = rectangle;
return {
west: CesiumMath.toDegrees(west),
south: CesiumMath.toDegrees(south),
east: CesiumMath.toDegrees(east),
north: CesiumMath.toDegrees(north)
};
}
}

StratumOrder.addLoadStratum(CogLoadableStratum.stratumName);

/**
* Creates a Cloud Optimised Geotiff catalog item.
*
* Currently it can render EPSG 4326/3857 COG files. There is experimental
* support for other projections, however it is less performant and could have
* unknown issues.
*/
export default class CogCatalogItem extends MappableMixin(
CatalogMemberMixin(CreateModel(CogCatalogItemTraits))
) {
static readonly type = "cog";

/**
* Private imageryProvider instance. This is set once forceLoadMapItems is
* called.
*/
@observable
_imageryProvider: TIFFImageryProvider | undefined;

/**
* The reprojector function to use for reprojecting non native projections
*
* Exposed here as instance variable for stubbing in specs.
*/
reprojector = reprojector;

get type() {
return CogCatalogItem.type;
}

constructor(
id: string | undefined,
terria: Terria,
sourceReference?: BaseModel | undefined
) {
super(id, terria, sourceReference);
makeObservable(this);
this.strata.set(
CogLoadableStratum.stratumName,
new CogLoadableStratum(this)
);

// Destroy the imageryProvider when `mapItems` is no longer consumed. This
// is so that the webworkers and other resources created by the
// imageryProvider can be freed. Ideally, there would be a more explicit
// `destroy()` method in Terria life-cycle so that we don't have to rely on
// mapItems becoming observed or unobserved.
onBecomeUnobserved(this, "mapItems", () => {
if (this._imageryProvider) {
this._imageryProvider.destroy();
this._imageryProvider = undefined;
}
});

// Re-create the imageryProvider if `mapItems` is consumed again after we
// destroyed it
onBecomeObserved(this, "mapItems", () => {
if (!this._imageryProvider && !this.isLoadingMapItems) {
this.loadMapItems(true);
}
});
}

protected async forceLoadMapItems(): Promise<void> {
if (!this.url) {
return;
}
const url = proxyCatalogItemUrl(this, this.url);
const imageryProvider = await this.createImageryProvider(url);
runInAction(() => {
this._imageryProvider = imageryProvider;
});
}

@computed get mapItems(): MapItem[] {
const imageryProvider = this._imageryProvider;
if (!imageryProvider) {
return [];
}

return [
{
show: this.show,
alpha: this.opacity,
// The 'requestImage' method in Cesium's ImageryProvider has a return type that is stricter than necessary.
// In our custom ImageryProvider, we return ImageData, which is also a valid return type.
// However, since the current Cesium type definitions do not reflect this flexibility, we use a TypeScript ignore comment ('@ts-ignore')
// to suppress the type checking error. This is a temporary solution until the type definitions in Cesium are updated to accommodate ImageData.
// @ts-expect-error - The return type of 'requestImage' method in our custom ImageryProvider can be ImageData, which is not currently allowed in Cesium's type definitions, but is fine.
imageryProvider,
clippingRectangle: this.cesiumRectangle
}
];
}

/**
* Create TIFFImageryProvider for the given url.
*/
private async createImageryProvider(
url: string
): Promise<TIFFImageryProvider> {
// lazy load the imagery provider, only when needed
const [{ default: TIFFImageryProvider }, { default: proj4 }] =
await Promise.all([
import("terriajs-tiff-imagery-provider"),
import("proj4-fully-loaded")
]);

return runInAction(() =>
TIFFImageryProvider.fromUrl(url, {
credit: this.credit,
tileSize: this.tileSize,
maximumLevel: this.maximumLevel,
minimumLevel: this.minimumLevel,
enablePickFeatures: this.allowFeaturePicking,
hasAlphaChannel: this.hasAlphaChannel,
// used for reprojecting from an unknown projection to 4326/3857
// note that this is experimental and could be slow as it runs on the main thread
projFunc: this.reprojector(proj4),
// make sure we omit `undefined` options so as not to override the library defaults
renderOptions: omitUndefined({
nodata: this.renderOptions.nodata,
convertToRGB: this.renderOptions.convertToRGB,
resampleMethod: this.renderOptions.resampleMethod
})
})
);
}
}

/**
* Function returning a custom reprojector
*/
function reprojector(proj4: any) {
return (code: number) => {
if (![4326, 3857, 900913].includes(code)) {
try {
const prj = proj4("EPSG:4326", `EPSG:${code}`);
if (prj)
return {
project: prj.forward,
unproject: prj.inverse
};
} catch (e) {
console.error(e);
}
}
};
}

/**
* Returns true if the tilingScheme is custom
*/
function isCustomTilingScheme(tilingScheme: Object) {
// The upstream library defines a TIFFImageryTillingScheme but it is not
// exported so we have to check if it is not one of the standard Cesium
// tiling schemes. Also, because TIFFImageryTillingScheme derives from
// WebMercatorTilingScheme, we cannot simply do an `instanceof` check, we
// compare the exact constructor instead.
return (
tilingScheme.constructor !== WebMercatorTilingScheme &&
tilingScheme.constructor !== GeographicTilingScheme
);
}

function omitUndefined(obj: Object) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value !== undefined)
);
}
9 changes: 2 additions & 7 deletions lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,11 @@ class FeatureServerStratum extends LoadableStratum(
item: ArcGisFeatureServerCatalogItem
): Promise<FeatureServerStratum> {
if (item.url === undefined) {
/* TODO: Should this be returned? */
/* eslint-disable-next-line no-new */
new FeatureServerStratum(item, undefined, undefined);
return new FeatureServerStratum(item, undefined, undefined);
}
const metaUrl = buildMetadataUrl(item);
const featureServer = await loadJson(metaUrl);

const stratum = new FeatureServerStratum(item, featureServer, undefined);

return stratum;
return new FeatureServerStratum(item, featureServer, undefined);
}

@computed
Expand Down
10 changes: 8 additions & 2 deletions lib/Models/Catalog/registerCatalogMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import WebProcessingServiceCatalogFunctionJob from "./Ows/WebProcessingServiceCa
import WebProcessingServiceCatalogGroup from "./Ows/WebProcessingServiceCatalogGroup";
import SdmxJsonCatalogGroup from "./SdmxJson/SdmxJsonCatalogGroup";
import SdmxJsonCatalogItem from "./SdmxJson/SdmxJsonCatalogItem";
import CogCatalogItem from "./CatalogItems/CogCatalogItem";

export default function registerCatalogMembers() {
CatalogMemberFactory.register(CatalogGroup.type, CatalogGroup);
Expand Down Expand Up @@ -241,6 +242,7 @@ export default function registerCatalogMembers() {
UrlTemplateImageryCatalogItem
);
CatalogMemberFactory.register(AssImpCatalogItem.type, AssImpCatalogItem);
CatalogMemberFactory.register(CogCatalogItem.type, CogCatalogItem);

UrlToCatalogMemberMapping.register(
matchesExtension("csv"),
Expand Down Expand Up @@ -287,6 +289,10 @@ export default function registerCatalogMembers() {
matchesExtension("zip"),
ShapefileCatalogItem.type
);
UrlToCatalogMemberMapping.register(
matchesExtension("tif", "tiff", "geotiff"),
CogCatalogItem.type
);

// These items work by trying to match a URL, then loading the data. If it fails, they move on.
UrlToCatalogMemberMapping.register(
Expand Down Expand Up @@ -412,8 +418,8 @@ function matchesUrl(regex: RegExp) {
return /./.test.bind(regex);
}

export function matchesExtension(extension: string) {
const regex = new RegExp("\\." + extension + "$", "i");
export function matchesExtension(...extensions: string[]) {
const regex = new RegExp("\\.(" + extensions.join("|") + ")$", "i");
return function (url: string) {
return Boolean(url.match(regex));
};
Expand Down
Loading

0 comments on commit 676d1d8

Please sign in to comment.