diff --git a/.eslintrc b/.eslintrc index fba8a521806..d30c30a8de4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -87,7 +87,7 @@ /* Variables */ "no-label-var": 1, "no-unused-vars": [ - 1, + "warn", { "vars": "local", "args": "none" @@ -115,18 +115,21 @@ "rules": { // @TODO: revise these rules "@typescript-eslint/consistent-type-assertions": "error", + "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "ignoreRestSiblings": true } ], "@typescript-eslint/ban-ts-comment": "warn", "@typescript-eslint/no-loss-of-precision": "warn", - "@typescript-eslint/no-unsafe-declaration-merging": "warn", - "react-hooks/exhaustive-deps": "warn", + "@typescript-eslint/no-unsafe-declaration-merging": "error", + "react-hooks/exhaustive-deps": "error", "react/prop-types": "warn" } } diff --git a/CHANGES.md b/CHANGES.md index 7e62558090c..fad35007f83 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,61 @@ # Change Log -#### next release (8.7.4) +#### next release (8.7.8) + +- Fix the layout of the story builder and the item search tool. +- 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 +- [The next improvement] + +#### 8.7.7 - 2024-10-01 + +- **Breaking changes:** + + - Remove RollbarErrorServiceProvder + - Error services now instantiated externally to terriajs + +- Fix remaining lint warnings +- Augment cesium types and start using import instead of require in ts files +- Update to sass 1.79.1 +- Add option to import assets from Cesium ion through the Add data panel. Use map config parameter "cesiumIonOAuth2ApplicationID" to enable the feature. + +#### 8.7.6 - 2024-08-22 + +- Add I3SCatalogItem + - getFeaturesFromPickResult now async to handle I3SNode.loadFields() + - extract common style logic to new Cesium3dTilesStyleMixin.ts +- Set default value for date and datetime WPS fields only when the field is marked as required. +- Fix Sass deprecation warnings (declarations after nested blocks) +- Fix legend shown for WMS difference output item +- Add `diffItemProperties` trait to override properties of WSM difference output item. Useful for customizing feature info template strings etc. +- Add Proj4 definition for EPSG:8059 +- Upgrade to terriajs-cesium 8.0.1. +- Re-enable terrain splitting. +- Add support for ArcGis ImageServer - this includes + - Support for "dynamic" `exportImage` endpoint (using `102100` wkid) + - Support for web mercator and wgs84 precached tiles + - Basic support for raster functions - a dropdown is rendered in the workbench for custom raster functions + - Traits to configure `bandIds` and `renderingRule` +- Increase `maxRefreshIntervals` from 1000 to 10000 for `WebMapServiceCatalogItem` and `ArcGisMapServerCatalogItem`. +- Add `nextDiscreteJulianDate` helper computed value to `DiscretelyTimeVaryingMixin` +- Add `EPSG:7899` to `Proj4Definitions` + +#### 8.7.5 - 2024-06-26 + +- TSify some `js` and `jsx` files and provide `.d.ts` ambient type files for a few others. This is so that running `tsc` on an external project that imports Terria code will typecheck successfully. +- Upgraded a bunch of d3 dependencies for fixing security errors. +- Show rectangle selector for WPS bounding box parameter +- Fix `store` and `status` values send in WPS Execute request. +- Add docs for `modelDimensions` + +#### 8.7.4 - 2024-06-07 - Fix position of draggable point after moving. - Allow to modify `lookupCookie` for i18next +- Fix `getFeatureProperties` (in `FeatureInfoSection`) failing due to bad JSON parsing of nested strings. +- The `TableFeatureInfoStratum` default `featureInfoTemplate` will now not show `_id_` (internal Terria feature ID) in feature info +- Fix bug in FilterSection #### 8.7.3 - 2024-05-28 @@ -17,6 +69,7 @@ #### 8.7.2 - 2024-05-14 +- Add NumberParameterEditor to enable WPS AllowedValues Ranges to be set and use DefaultValue - Feature info template has access to activeStyle of item having TableTraits. - Updated a few dependencies to fix security warnings: `underscore`, `visx`, `shpjs`, `resolve-uri-loader`, `svg-sprite-loader` - Allow related maps UI strings to be translated. Translation support for related maps content is not included. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f541f7cb5e..4b93711ae78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ Please get involved in code review. It's a great way to expand your knowledge of - Verify the code builds successfully and there are no lint warnings (run `gulp`). - Check basic functionality of the map, such as enabling data sources in both 2D and 3D. - Verify that all specs pass. If anything you did might be browser-dependent, you should run the specs in all the major supported browsers. -- Review the code itself for quality of implementation and consistency with coding conventions. Until we have our own coding conventions, we can [use Cesium's](https://github.com/AnalyticalGraphicsInc/cesium/wiki/JavaScript-Coding-Conventions). +- Review the code itself for quality of implementation and consistency with coding conventions. Until we have our own coding conventions, we use [Cesium's](https://github.com/CesiumGS/cesium/blob/master/Documentation/Contributors/CodingGuide/README.md). Reviewers are welcome to make minor edits to a pull request (e.g. fixing typos) before merging it. If a reviewer makes larger changes, someone else - maybe the original author of the pull request - should take a look at the changes before the entire pull request is merged. diff --git a/README.md b/README.md index 9ab810111a4..28673722478 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ See [Getting Started](https://docs.terria.io/guide/getting-started/) in the [Doc #### Related components -- **[TerriaMapStatic](https://github.com/terriajs/terriamapstatic)**, a pre-built version of TerriaMap, which can be deployed as a static HTML website, such as on Github Pages. +- **[TerriaMapStatic](https://github.com/terriajs/terriamap-static)**, a pre-built version of TerriaMap, which can be deployed as a static HTML website, such as on Github Pages. ### Big Thanks diff --git a/architecture/0002-require-instead-of-import.md b/architecture/0002-require-instead-of-import.md index 2a2cf30afbb..3b366be2c41 100644 --- a/architecture/0002-require-instead-of-import.md +++ b/architecture/0002-require-instead-of-import.md @@ -4,7 +4,7 @@ Date: 2020-08-07 recorded, decision made earlier ## Status -Accepted +Superseded by [0012-do-not-use-require-for-cesium-in-ts](./0012-do-not-use-require-for-cesium-in-ts.md) on 2024-09-28 ## Context diff --git a/architecture/0010-never-support-ie11.md b/architecture/0010-never-support-ie11.md index 63952b27c49..42804bc538d 100644 --- a/architecture/0010-never-support-ie11.md +++ b/architecture/0010-never-support-ie11.md @@ -4,7 +4,7 @@ Date: 2022-05-12 ## Status -Proposed +Accepted ## Context diff --git a/architecture/0011-configurable-search-providers.md b/architecture/0011-configurable-search-providers.md index 2779eaf5a97..48d49467e15 100644 --- a/architecture/0011-configurable-search-providers.md +++ b/architecture/0011-configurable-search-providers.md @@ -4,7 +4,7 @@ Date: 2021-01-19 ## Status -Proposed +Accepted ## Context diff --git a/architecture/0012-do-not-use-require-for-cesium-in-ts.md b/architecture/0012-do-not-use-require-for-cesium-in-ts.md new file mode 100644 index 00000000000..c77f67cbaef --- /dev/null +++ b/architecture/0012-do-not-use-require-for-cesium-in-ts.md @@ -0,0 +1,34 @@ +# 2. Using `imports` instead of `require` + type casting statements + +Date: 2024-09-28 + +## Status + +Proposed + +## Context + +A decision was made in [0002-require-instead-of-import](./0002-require-instead-of-import.md) to use non-exported terriajs-cesium features using `require` + type casting statements due to inability to augment namespace and enum imports. + +Recently we found a new way to augment terriajs-cesium typescript definition in case of enums and namespaces. When we directly augment `terriajs-module` typescript will properly augment the namespaces and enums can be augmented as namespaces making everything works as should on type level. + +```ts +declare module terriajs-cesium { + namespace FeatureDetection { + function isChrome(): boolean; + function isEdge(): boolean; + function isInternetExplorer(): boolean; + function isFirefox(): boolean; + function internetExplorerVersion(): number[]; + function chromeVersion(): number[]; + } + + namespace Axis { + function fromName(name: string): number; + } +} +``` + +## Decission + +Augment `terriajs-cesium` type definition diff --git a/buildprocess/configureWebpack.js b/buildprocess/configureWebpack.js index 964deee41f0..57978564483 100644 --- a/buildprocess/configureWebpack.js +++ b/buildprocess/configureWebpack.js @@ -32,6 +32,7 @@ function configureWebpack( ".webpack.js", ".web.js", ".js", + ".mjs", ".ts", ".tsx" ]; @@ -111,6 +112,14 @@ function configureWebpack( }) }); + // Some packages exports an .mjs file for ESM imports. + // This rule instructs webpack to import mjs modules correctly. + config.module.rules.push({ + test: /\.mjs$/, + include: /node_modules/, + type: "javascript/auto" + }); + const zipJsDir = path.dirname(require.resolve("@zip.js/zip.js/package.json")); config.module.rules.push({ diff --git a/buildprocess/createKarmaBaseConfig.js b/buildprocess/createKarmaBaseConfig.js index 8a70af79e13..3ccf1ab5d69 100644 --- a/buildprocess/createKarmaBaseConfig.js +++ b/buildprocess/createKarmaBaseConfig.js @@ -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: [ diff --git a/buildprocess/directory-loader.js b/buildprocess/directory-loader.js deleted file mode 100644 index 35b142447fa..00000000000 --- a/buildprocess/directory-loader.js +++ /dev/null @@ -1,67 +0,0 @@ -var loaderUtils = require("loader-utils"); -var path = require("path"); - -module.exports = DirectoryLoaderPlugin; - -function DirectoryLoaderPlugin(paths) { - this.paths = paths; -} - -DirectoryLoaderPlugin.prototype.apply = function (resolver) { - var optPaths = this.paths; - console.log("A" + optPaths); - - resolver.plugin("directory", function (request, callback) { - var dirPath = this.join(request.path, request.request); - var dirName = dirPath.substr( - dirPath.lastIndexOf(path.sep) + path.sep.length - ); - - if (optPaths && optPaths.indexOf(dirPath) >= 0) { - this.fileSystem.stat( - dirPath, - function (err, stat) { - if (err || !stat || !stat.isDirectory()) { - callback.log && - callback.log( - request.path + - " doesn't exist or is not a directory (directory named)" - ); - return callback(); - } - - try { - request.resolved = true; - callback.log = function () { - console.log(arguments); - }; - console.log(callback); - return callback(null, null); - } catch (e) { - console.error(e); - throw e; - } - }.bind(this) - ); - } - //} else { - callback(); - //} - }); -}; - -DirectoryLoaderPlugin.loader = function (content) { - //this.cacheable && this.cacheable(); - //if(!this.emitFile) throw new Error("emitFile is required from module system"); - //var query = loaderUtils.parseQuery(this.query); - //var url = loaderUtils.interpolateName(this, query.name || "[hash].[ext]", { - // context: query.context || this.options.context, - // content: content, - // regExp: query.regExp - //}); - //this.emitFile(url, content); - //return "module.exports = __webpack_public_path__ + " + JSON.stringify(url) + ";"; - console.log("blah"); - return " hello "; -}; -DirectoryLoaderPlugin.loader.raw = true; diff --git a/doc/connecting-to-data/customizing-data-appearance/img/model-dim.jpeg b/doc/connecting-to-data/customizing-data-appearance/img/model-dim.jpeg new file mode 100644 index 00000000000..d21ef6f4ae5 Binary files /dev/null and b/doc/connecting-to-data/customizing-data-appearance/img/model-dim.jpeg differ diff --git a/doc/connecting-to-data/customizing-data-appearance/model-dimensions.md b/doc/connecting-to-data/customizing-data-appearance/model-dimensions.md new file mode 100644 index 00000000000..04cd37778a8 --- /dev/null +++ b/doc/connecting-to-data/customizing-data-appearance/model-dimensions.md @@ -0,0 +1,114 @@ +# Model Dimensions + +Model dimensions can be used to define dropdown menus in the workbench that update model JSON - think of it as a dropdown that patches the model JSON with a new value. This is useful for changing the appearance of a dataset, such as changing the colourMap for a CSV. + +## Basic example + +For example - this renders a new drop down called "Color" that changes the `colorPalette` trait - [test link](http://ci.terria.io/main/#clean&start={%22initSources%22%3A[{%22homeCamera%22%3A{%22north%22%3A-8%2C%22east%22%3A158%2C%22south%22%3A-45%2C%22west%22%3A109}%2C%22workbench%22%3A[%22test%22]%2C%22catalog%22%3A[{%22type%22%3A%22csv%22%2C%22url%22%3A%22test%2FNSW_LGA_NEXIS_201212.csv%22%2C%22name%22%3A%22NSWLGANEXIS2012%22%2C%22id%22%3A%22test%22%2C%22modelDimensions%22%3A[{%22id%22%3A%22cols%22%2C%22name%22%3A%22Color%22%2C%22selectedId%22%3A%22Red%22%2C%22options%22%3A[{%22id%22%3A%22Red%22%2C%22value%22%3A{%22defaultStyle%22%3A{%22color%22%3A{%22colorPalette%22%3A%22Reds%22}}}}%2C{%22id%22%3A%22Blue%22%2C%22value%22%3A{%22defaultStyle%22%3A{%22color%22%3A{%22colorPalette%22%3A%22Blues%22}}}}]}]}]}]}) + +```json +{ + "type": "csv", + "url": "test/NSW_LGA_NEXIS_201212.csv", + "name": "NSW LGA NEXIS 2012", + "modelDimensions": [ + { + "id": "cols", + "name": "Color", + "selectedId": "Red", + "options": [ + { + "id": "Red", + "value": { + "defaultStyle": { + "color": { + "colorPalette": "Reds" + } + } + } + }, + { + "id": "Blue", + "value": { + "defaultStyle": { + "color": { + "colorPalette": "Blues" + } + } + } + } + ] + } + ] +} +``` + + + +## Example with Mustache template + +Model dimensions also supports the use of [Mustache templates](https://mustache.github.io/) - this means you can refer to other parts of the model JSON in the value of the model dimension. For example, this changes the `colorPalette` trait: + +```json +{ + "type": "csv", + "url": "test/NSW_LGA_NEXIS_201212.csv", + "name": "NSW LGA NEXIS 2012", + "modelDimensions": [ + { + "id": "Cols", + "selectedId": "Red", + "options": [ + { + "id": "Red", + "value": { + "defaultStyle": { + "color": { + "colorPalette": "{{modelDimensions.0.selectedId}}" + } + } + } + }, + { + "id": "Blue", + "value": { + "defaultStyle": { + "color": { + "colorPalette": "{{modelDimensions.0.selectedId}}" + } + } + } + } + ] + } + ] +} +``` + +This is a silly example - but the template approach means you can have "interaction" between `modelDimensions` + +```json +{ + ..., + "modelDimensions": [ + { + "id": "some-dim", + "selectedId": "some-option", + "options": [ + { + "id": "some-option", + "value": { + "url": "https://example.com/{{modelDimensions.0.selectedId}}-and-{{modelDimensions.1.selectedId}}.geojson" + } + }, + ... + ] + } + ], + ... +} +``` + +## Default `selectedId` + +It is important to note that the `selectedId` does not apply/update the model JSON when the model is first loaded. So it is best to make sure that the `selectedId` matches your default model JSON. diff --git a/doc/connecting-to-data/customizing-data-appearance/overview.md b/doc/connecting-to-data/customizing-data-appearance/overview.md index ac7897c91fd..827ad2c03a3 100644 --- a/doc/connecting-to-data/customizing-data-appearance/overview.md +++ b/doc/connecting-to-data/customizing-data-appearance/overview.md @@ -15,3 +15,4 @@ This section explains how you can use such a file to improve the look of your da - [Customizing the Appearance of Tabular Data](./tabular-data.md) - [Customizing the Appearance of Imagery Data](./imagery-data.md) - [Customizing the Feature Info Template](./feature-info-template.md) +- [Using Model Dimensions to customise the Workbench](./model-dimensions.md) diff --git a/doc/customizing/client-side-config.md b/doc/customizing/client-side-config.md index e146abe98aa..598b9789a90 100644 --- a/doc/customizing/client-side-config.md +++ b/doc/customizing/client-side-config.md @@ -181,10 +181,10 @@ Configuration of items to appear in the search bar ### ErrorServiceOptions -| Name | Required | Type | Default | Description | -| ------------- | -------- | ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| provider | yes | **string** | `undefined` | A string identifying the error service provider to use. Currently only `rollbar` is supported. | -| configuration | no | **any** | `undefined` | The configuration object to pass as constructor parameters to the error service provider instance. See the [provider implementation](https://github.com/TerriaJS/terriajs/blob/main/lib/Models/ErrorServiceProviders/) for supported configuration parameters. | +| Name | Required | Type | Default | Description | +| ------------- | -------- | ---------- | ----------- | ---------------------------------------------------------------------- | +| provider | no | **string** | `undefined` | A string identifying the error service provider. | +| configuration | no | **any** | `undefined` | The configuration object to be used when initializing an ErrorService. | **Example** diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index dfd28b71452..8319b66925f 100644 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -31,6 +31,7 @@ nav: - Imagery Data: connecting-to-data/customizing-data-appearance/imagery-data.md - Tabular Data: connecting-to-data/customizing-data-appearance/tabular-data.md - Feature Information Template: connecting-to-data/customizing-data-appearance/feature-info-template.md + - Model Dimensions: connecting-to-data/customizing-data-appearance/model-dimensions.md - Catalog Functions: connecting-to-data/catalog-functions.md - Catalog References: connecting-to-data/catalog-references.md - Catalog Type Details: diff --git a/gulpfile.js b/gulpfile.js index b2eb80bf41d..06c2d286cd4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,7 +54,7 @@ gulp.task("lint", function (done) { "--ignore-pattern", "lib/ThirdParty", "--max-warnings", - "264" // TODO: Bring this back to 0 + "0" ]); done(); diff --git a/lib/Charts/BarChart.js b/lib/Charts/BarChart.js deleted file mode 100644 index fb7ffbd20dd..00000000000 --- a/lib/Charts/BarChart.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; - -import d3Sync from "./d3Sync"; - -import BaseChart from "./BaseChart"; - -const barWidth = 6; - -class BarChart extends BaseChart { - render(chart, chartData, renderContext) { - const { scales, state, chartTransition } = renderContext; - const chartDataSets = state.data.filter( - (data) => data.renderer instanceof BarChart - ); - const offset = - (barWidth * chartDataSets.length) / 2 - - chartDataSets.indexOf(chartData) * barWidth; - const sx = scales.x, - sy = scales.y[chartData.units], - color = chartData.color || "white"; - - // preprocess for negative data; rect does not seem to like negative values. - const points = chartData.points.map((p) => { - const point = { - x: sx(p.x) - offset, - y: sy(p.y), - dx: barWidth, - dy: sy(0) - sy(p.y) - }; - if (point.dy < 0) { - point.y += point.dy; - point.dy *= -1; - } - return point; - }); - - d3Sync( - chart, - points, - "rect", - (rect) => - rect - .attr("x", (p) => p.x) - .attr("y", (p) => p.y) - .attr("width", (p) => p.dx) - .attr("height", (p) => p.dy) - .style("fill", color) - .style("stroke", "none"), - chartTransition - ); - } - - getXpadding(chartData, allChartData, allRenderers) { - return ( - (barWidth * - allRenderers.filter((renderer) => renderer instanceof BarChart) - .length) / - 2 - ); - } -} - -module.exports = BarChart; diff --git a/lib/Charts/BaseChart.js b/lib/Charts/BaseChart.js deleted file mode 100644 index 1728a37b168..00000000000 --- a/lib/Charts/BaseChart.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -import TerriaError from "../Core/TerriaError"; - -class BaseChart { - constructor() { - this.id = null; - } - - /** - * Must implement this for new chart types. - * - * @param {Object} chart D3Selection containing group in which the chartData elements will be charted. - * @param {Object} chartData ChartData object to be charted. - * @param {ChartRenderer} renderContext Object containing various helper items which you may need for plotting. - // * @param {DOMElement} renderContext.container The DOM container in which to place the chart. - // * @param {Object} renderContext.state The state of the chart. - // * @param {Object} renderContext.size ... - // * @param {Object} renderContext.margin ... - // * @param {Object} renderContext.scales Chart x and y Scales. - // * @param {D3Scake} renderContext.scales.x X scale - // * @param {Object} renderContext.scales.y Map mapping y axis units to Scakes - // * @param {Array} renderContext.units Array of units - // * @param {D3Selection} renderContext.chart Chart Area - // * @param {D3Selection} renderContext.chartPlotContainer Content area under which all ChartData are rendered. - // * @param {String} renderContext.chartTransform Transform which positions sub-elements where they should be. If you are using chart or renderContext.chart or renderContext.chartPlotContainer, they are already transformed using this. I.e. there is no need to transform again. But if you are adding a high level object, such as under renderContext.chartSVGContainer, this could be helpful. - // * @param {D3Transition} renderContext.chartTransition Transition for animations - // * @param {D3Selection} renderContext.chartSVGContainer The top SVG element. - * - */ - render(chart, chartData, renderContext) { - throw new TerriaError("Not implemented"); - } - - /** - * Override this if you need a little extra space around your x axis for your chart/visualisation. - */ - getXpadding(chartData, allChartData, allRenderers) { - return 0; - } -} - -export default BaseChart; diff --git a/lib/Charts/ChartData.ts b/lib/Charts/ChartData.ts index 1902ab4f0cf..302c75e0e06 100644 --- a/lib/Charts/ChartData.ts +++ b/lib/Charts/ChartData.ts @@ -1,181 +1,4 @@ -import { min as d3ArrayMin, max as d3ArrayMax } from "d3-array"; -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; - export interface ChartPoint { readonly x: number | Date; readonly y: number; } - -export interface ChartDataOptions { - /** - * The array of points. Each point should have the format {x: X, y: Y}. - */ - readonly points: readonly ChartPoint[]; - - /** - * Unique ID for this set of points. - */ - readonly id?: string; - - /** - * Name of the category for this set of points, e.g. the source catalog item. - */ - readonly categoryName?: string; - - /** - * Name for this set of points. - */ - readonly name?: string; - - /** - * Units of this set of points. - */ - readonly units?: string; - - /** - * A function that returns CSS color code for this set of points. - * - * We use a function instead of an immediate value so that colors can be - * assigned lazily; only when it is required. - */ - readonly getColor: () => string | undefined; - - /** - * Minimum value for Y axis to display, overriding minimum value in data. - */ - readonly yAxisMin?: number; - - /** - * Maximum value for Y axis to display, overriding maximum value in data. - */ - readonly yAxisMax?: number; - - /** - * Chart type. If you want these points rendered with a certain way. Leave empty for auto detection. - */ - readonly type?: string; - - /** - * Click handler (called with (x, y) in data units) if some special behaviour is required for clicking. - */ - readonly onClick?: (x: number, y: number) => void; - - /** - * Request that the chart be scaled so that this series can be shown entirely. - */ - readonly showAll?: boolean; -} - -/** - * A container to pass data to a d3 chart: a single series of data points. - * For documentation on the custom tag, see lib/Models/registerCustomComponentTypes.js. - * - * @param {ChartDataOptions} [options] Further parameters. - */ -export default class ChartData { - /** - * The array of points. Each point should have the format {x: X, y: Y}. - */ - readonly points: readonly ChartPoint[]; - - /** - * A selected point from the array above. Used internally by charting functions for hover/clicking functionality. - */ - readonly point: ChartPoint | undefined; - - /** - * Unique id for this set of points. - */ - readonly id: string | undefined; - - /** - * Name of the category for this set of points., eg. the source catalog item. - */ - readonly categoryName: string | undefined; - - /** - * Name for this set of points. - */ - readonly name: string | undefined; - - /** - * Units of this set of points. - */ - readonly units: string | undefined; - - /** - * A function that returns CSS color code for this set of points. - * - * We use a function instead of an immediate value so that colors can be - * assigned lazily; only when it is required. - */ - readonly getColor: () => string | undefined; - - /** - * Minimum value for y axis to display, overriding minimum value in data. - */ - readonly yAxisMin: number | undefined; - - /** - * Maximum value for y axis to display, overriding maximum value in data. - */ - readonly yAxisMax: number | undefined; - - /** - * Chart type. If you want these points to be rendered with a certain way. Leave empty for auto detection. - */ - readonly type: string | undefined; - - /** - * Click handler (called with (x, y) in data units) if some special behaviour is required on clicking. - */ - readonly onClick: ((x: number, y: number) => void) | undefined; - - /** - * Request that the chart be scaled so that this series can be shown entirely. - * @default true - */ - readonly showAll: boolean; - - readonly yAxisWidth: number; - - constructor(options: ChartDataOptions) { - this.points = options.points; - this.point = undefined; - this.id = options.id; - this.categoryName = options.categoryName; - this.name = options.name; - this.units = options.units; - this.getColor = options.getColor; - this.yAxisMin = options.yAxisMin; - this.yAxisMax = options.yAxisMax; - this.type = options.type; - this.onClick = options.onClick; - this.showAll = defaultValue(options.showAll, true); - this.yAxisWidth = 40; - } - - /** - * Calculates the min and max x and y of the points. - * If there are no points, returns undefined. - * @return {Object} An object {x: [xmin, xmax], y: [ymin, ymax]}. - */ - getDomain(): - | { x: [number | Date, number | Date]; y: [number, number] } - | undefined { - const points = this.points; - if (points.length === 0) { - return undefined; - } - return { - x: [ - d3ArrayMin(points, (point) => point.x)!, - d3ArrayMax(points, (point) => point.x)! - ], - y: [ - d3ArrayMin(points, (point) => point.y)!, - d3ArrayMax(points, (point) => point.y)! - ] - }; - } -} diff --git a/lib/Charts/LineChart.js b/lib/Charts/LineChart.js deleted file mode 100644 index e2137f8599f..00000000000 --- a/lib/Charts/LineChart.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -import { line as d3Line } from "d3-shape"; -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; -import defined from "terriajs-cesium/Source/Core/defined"; - -class LineChart extends BaseChart { - constructor() { - super(); - this.path = null; - this.chartData = null; - this.sy = null; - } - - render(chart, chartData, renderContext) { - const { chartTransform, scales } = renderContext; - const sx = scales.x, - sy = scales.y[chartData.units], - color = chartData.color || "white"; - // If there are undefined or null y-values, just ignore them. This works well for initial and final undefined values, - // and simply interpolates over intermediate ones. This may not be what we want. - this.path = d3Line() // NOTE: it was originally 'basic', which is not an interpolation - .x((d) => sx(d.x)) - .y((d) => sy(d.y))(chartData.points.filter((point) => defined(point.y))); - // consider calling .defined() to allow discontinuous graphs - - this.chartData = chartData; - this.sy = sy; - - d3Sync( - chart, - [chartData], - "path", - (line, enter) => { - line.attr("d", this.path).style("fill", "none").style("stroke", color); - - // Can't do this on a transition call... - if (typeof line.classed === "function") { - line.classed("line", true); - } - }, - chartTransform - ); - } - - zoomOnAxisX(newScaleX) { - this.path = d3Line() - .x((d) => newScaleX(d.x)) - .y((d) => this.sy(d.y))( - this.chartData.points.filter((point) => defined(point.y)) - ); - - d3Select(`#${this.id}`).selectAll("path").attr("d", this.path); - } -} - -module.exports = LineChart; diff --git a/lib/Charts/MomentChart.js b/lib/Charts/MomentChart.js deleted file mode 100644 index 5fd0852d68f..00000000000 --- a/lib/Charts/MomentChart.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; - -class MomentChart extends BaseChart { - constructor() { - super(); - } - - render(chart, chartData, { chartTransform, scales, size }) { - const n = chartData.points.length; - // spacing is the number of pixels per data point. When it's very low, we want to make the lines - // thinner and lighter - const spacing = - (scales.x(chartData.points[n - 1].x) - scales.x(chartData.points[0].x)) / - n; - d3Sync(chart, chartData.points, "rect", function (el, isNew) { - el.attr("class", (d, i) => `dataIndex-${i}`) - .style("fill", chartData.color || "turquoise") - .attr("fill-opacity", function (d, i) { - return i === chartData.selectedIndex ? 1 : spacing > 3 ? 0.4 : 0.2; - }) - .attr("x", (d) => scales.x(d.x)) - .attr("y", 0) - .attr("width", function (d, i) { - return i === chartData.selectedIndex ? 3 : 1; - // return spacing > 20 ? 3 : 1; - }) - .attr("height", size.plotHeight) - .attr("cursor", "pointer"); - }); - } - - highlightMoment(selectedIndex) { - d3Select(`#${this.id}`) - .selectAll("rect") - .style("fill-opacity", 0.2) - .style("width", 1) - .filter(`.dataIndex-${selectedIndex}`) - .style("fill-opacity", 1) - .style("width", 3); - } - - zoomOnAxisX(newScaleX) { - d3Select(`#${this.id}`) - .selectAll("rect") - .attr("x", (d) => newScaleX(d.x)); - } -} - -module.exports = MomentChart; diff --git a/lib/Charts/MomentChartPoints.js b/lib/Charts/MomentChartPoints.js deleted file mode 100644 index 90b7a5c6522..00000000000 --- a/lib/Charts/MomentChartPoints.js +++ /dev/null @@ -1,205 +0,0 @@ -"use strict"; - -import BaseChart from "./BaseChart"; -import d3Sync from "./d3Sync"; -import { select as d3Select } from "d3-selection"; -import { interpolateNumber as d3InterpolateNumber } from "d3-interpolate"; - -const defaultMarkerSizeSmall = 2; -const defaultMarkerSizeLarge = 5; - -const selectedMarkerSizeSmall = 5; -const selectedMarkerSizeLarge = 8; - -class MomentChartPoints extends BaseChart { - constructor() { - super(); - this.alternateYAxis = null; - this._alternateYAxisFilteredPoints = null; - this.chartData = null; - this.yMin = null; - this.yMax = null; - this.lastKnownXScale = null; - } - - render(chart, chartData, { chartTransform, scales, size }, alternateYAxis) { - if (alternateYAxis !== null) { - this.alternateYAxis = alternateYAxis.firstY; - this._alternateYAxisFilteredPoints = alternateYAxis.firstY.points.filter( - (p) => p.y !== null - ); - this.yMin = alternateYAxis.yMin; - this.yMax = alternateYAxis.yMax; - } else { - this.alternateYAxis = null; - this._alternateYAxisFilteredPoints = null; - this.yMin = 0; - this.yMax = 1; - } - - this.chartData = chartData; - const ch = this; - - this.lastKnownXScale = scales.x; - const spacing = this._calculateSpacing(); - - d3Sync( - chart, - chartData.points, - "circle", - (el, isNew) => - el - .attr("class", (d, i) => `dataIndex-${i}`) - .style("fill", chartData.color || "turquoise") - .attr("r", function (d, i) { - if (i === chartData.selectedIndex) - return spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall; - return spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall; - }) - .style("fill-opacity", (d, i) => - i === chartData.selectedIndex ? 1 : 0.3 - ) - .attr("cx", (d) => scales.x(d.x)) - .attr("cy", function (d) { - if (ch.alternateYAxis === null) return size.plotHeight / 2; - const xIndex = ch._findNearestX(d.x); - if ( - xIndex === null || - xIndex === ch._alternateYAxisFilteredPoints.length - 2 || - xIndex === -1 - ) - return size.plotHeight / 2; - - // Get the location that the marker should live along the x axis - const xAsPercentage = ch._getXAsPercentage( - d.x, - ch._alternateYAxisFilteredPoints[xIndex].x, - ch._alternateYAxisFilteredPoints[xIndex + 1].x - ); - - // find a y value based on the x - const interNum = d3InterpolateNumber( - ch._alternateYAxisFilteredPoints[xIndex].y, - ch._alternateYAxisFilteredPoints[xIndex + 1].y - ); - const calculatedRawY = interNum(xAsPercentage); - - // Because the line the dots are being mapped against may not have the widest range - // we need to adjust it's y position in the chart - const chartHeightInterpolator = d3InterpolateNumber( - 0, - size.plotHeight - ); - const newMin = chartHeightInterpolator( - ch._calcPercentageBetweenMinAndMax( - ch.alternateYAxis.yAxisMin, - ch.yMin, - ch.yMax - ) - ); - const newMax = chartHeightInterpolator( - ch._calcPercentageBetweenMinAndMax( - ch.alternateYAxis.yAxisMax, - ch.yMin, - ch.yMax - ) - ); - - return ( - size.plotHeight - - ch._getYAsPercentage( - calculatedRawY, - newMin, - newMax, - ch.alternateYAxis.yAxisMin, - ch.alternateYAxis.yAxisMax - ) - ); - }), - chartTransform - ); - } - - zoomOnAxisX(newScaleX) { - this.lastKnownXScale = newScaleX; - - const spacing = this._calculateSpacing(); - const chartData = this.chartData; - d3Select(`#${this.id}`) - .selectAll("circle") - .attr("cx", (d) => newScaleX(d.x)) - .attr("r", function (d, i) { - if (i === chartData.selectedIndex) - return spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall; - return spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall; - }); - } - - highlightMoment(selectedIndex) { - const spacing = this._calculateSpacing(); - this.chartData.selectedIndex = selectedIndex; - d3Select(`#${this.id}`) - .selectAll("circle") - .style("fill-opacity", 0.3) - .attr("r", (d) => - spacing > defaultMarkerSizeSmall - ? defaultMarkerSizeLarge - : defaultMarkerSizeSmall - ) - .filter(`.dataIndex-${selectedIndex}`) - .style("fill-opacity", 1) - .attr("r", (d) => - spacing > defaultMarkerSizeSmall - ? selectedMarkerSizeLarge - : selectedMarkerSizeSmall - ); - } - - _calculateSpacing() { - const n = this.chartData.points.length; - return ( - (this.lastKnownXScale(this.chartData.points[n - 1].x) - - this.lastKnownXScale(this.chartData.points[0].x)) / - n - ); - } - - _getXAsPercentage(x, startX, endX) { - const unixStart = new Date(startX).getTime(); - const unixEnd = new Date(endX).getTime(); - return (new Date(x).getTime() - unixStart) / (unixEnd - unixStart); - } - - // https://stackoverflow.com/a/31687097/1979085 - _getYAsPercentage(fauxY, minAllowed, maxAllowed, min, max) { - return ( - ((maxAllowed - minAllowed) * (fauxY - min)) / (max - min) + minAllowed - ); - } - - _calcPercentageBetweenMinAndMax(x, min, max) { - return (x - min) / (max - min); - } - - _findNearestX(x) { - const xToSearchFor = new Date(x).getTime(); - for (var i = 0; i < this._alternateYAxisFilteredPoints.length; i++) { - if ( - new Date(this._alternateYAxisFilteredPoints[i].x).getTime() >= - xToSearchFor - ) - return i - 1; - } - return null; - } -} - -module.exports = MomentChartPoints; diff --git a/lib/Charts/Scales.js b/lib/Charts/Scales.js deleted file mode 100644 index a25ca48fb72..00000000000 --- a/lib/Charts/Scales.js +++ /dev/null @@ -1,193 +0,0 @@ -"use strict"; - -import { min as d3ArrayMin, max as d3ArrayMax } from "d3-array"; -import { - scaleTime as d3ScaleTime, - scaleLinear as d3ScaleLinear -} from "d3-scale"; -import defined from "terriajs-cesium/Source/Core/defined"; -import uniq from "lodash-es/uniq"; - -const unknown = undefined; - -const Scales = { - /** - * Calculates the appropriate d3 scales. - * - * @param {Size} size Dimensions of plot area, as returned by Size class. - * @param {Object} [domain] Optional, object containing [minimum, maximum] arrays for each axis. - * @param {Number[]} domain.x [x-minimum, x-maximum]. - * @param {Object} domain.y An object whose keys are units ("undefined" for unknown), values being [y-minimum, y-maximum]. - * @param {ChartData[]} data The data for each line. This is required to extract units. Also if no domain is provided, it is calculated from the data. - * @return {Object} {x, y: { [unit1], [unit2] ... }} where values are D3-scale objects. - */ - calculate(size, domain, data, xPadding = 0) { - const allUnits = uniq(data.map((line) => line.units || unknown)); - function computeDomain() { - if (data[0].points.length === 0) { - return; - } - // if there is at least one domain with "showAll", filter the others out. - const keepAll = data.filter((series) => series.showAll).length === 0; - - const filteredData = data.filter((d) => { - return d.points.length > 0; - }); - - // domains is an Array of the domains for each data element, with the units. - const domains = filteredData.map(function (series) { - const seriesD = series.getDomain(); - series.yAxisMin = seriesD.y[0]; - series.yAxisMax = seriesD.y[1]; - return { - units: series.units || unknown, - domain: seriesD - }; - }); - const importantDomains = domains.filter( - (d, i) => keepAll || filteredData[i].showAll - ); - - // domain.x is a simple [minx, maxx] array. - // domain.y is an object with keys being the different units, and values being [miny, maxy]. - const domain = { - x: [ - d3ArrayMin( - importantDomains.map((l) => l.domain.x), - (x) => x[0] - ), - d3ArrayMax( - importantDomains.map((l) => l.domain.x), - (x) => x[1] - ) - ], - y: {} - }; - allUnits.forEach((theUnits) => { - const domainsWithTheseUnits = domains.filter( - (l) => l.units === theUnits - ); - domain.y[theUnits] = [ - d3ArrayMin(domainsWithTheseUnits, (d) => d.domain.y[0]), - d3ArrayMax(domainsWithTheseUnits, (d) => d.domain.y[1]) - ]; - }); - for (const theseUnits in domain.y) { - if (Object.prototype.hasOwnProperty.call(domain.y, theseUnits)) { - const thisYDomain = domain.y[theseUnits]; - // If the y-domain is positive and could reasonably be expanded to include zero, do so. - // (Eg. the range is 5 to 50, do it; if it is 5 to 8, do not. Set the boundary arbitrarily at 5 to 12.5, ie. 1:2.5.) - if (thisYDomain[0] > 0 && thisYDomain[0] / thisYDomain[1] < 0.4) { - thisYDomain[0] = 0; - } - // If the y-domain is negative and could reasonably be expanded to include zero, do so. - if (thisYDomain[1] < 0 && thisYDomain[0] / thisYDomain[1] < 0.4) { - thisYDomain[1] = 0; - } - const dataWithTheseUnits = data.filter((l) => l.units === theseUnits); - - // Override y-domain if user has requested it. - const yAxisMin = Math.min.apply( - Math, - dataWithTheseUnits - .filter((d) => defined(d.yAxisMin)) - .map((d) => d.yAxisMin) - ); - if (isFinite(yAxisMin) && thisYDomain[0] < yAxisMin) { - thisYDomain[0] = yAxisMin; - } - - const yAxisMax = Math.max.apply( - Math, - dataWithTheseUnits - .filter((d) => defined(d.yAxisMax)) - .map((d) => d.yAxisMax) - ); - if (isFinite(yAxisMax) && thisYDomain[1] > yAxisMax) { - thisYDomain[1] = yAxisMax; - } - } - } - return domain; - } - - function computeXScale() { - const xScale = - domain.x[0] instanceof Date ? d3ScaleTime() : d3ScaleLinear(); - return xScale.range([xPadding, size.width - xPadding]).domain(domain.x); - } - - function computeYScales() { - // The x-axis takes up plot space, if it is at the bottom of the plot (ie. if the y-domain is entirely positive), - // but not if it is in the middle of the plot (ie. if the y-domain includes zero). - // - // For now, we assume that the x-axis will be displayed aligned with the first data series' y-scale. - - const mainYDomain = domain.y[allUnits[0]]; - const yContainsZero = mainYDomain[0] < 0 && mainYDomain[1] > 0; - - if (yContainsZero) { - const yPositiveOnly = d3ScaleLinear() - .range([size.plotHeight, 0]) - .domain([0, mainYDomain[1]]); - if (yPositiveOnly(mainYDomain[0]) < size.heightMinusXAxisLabelHeight) { - // There's only a very small negative range. The y-axis is near the bottom of the panel. - // The x-axis can be xAxisHeight from the bottom, and the negative part of the y-axis fits in the xAxisHeight. - // We want to use this scale, but we need to expand its range and domain. To do this, just use plotHeight = yPositiveOnly(mainYDomain[0]). - size.plotHeight = yPositiveOnly(mainYDomain[0]); - } else { - // There's a big negative range, so the y-axis is not near the bottom of the panel. - size.plotHeight = size.heightMinusXAxisLabelHeight; - } - } else if (mainYDomain[0] < 0 && mainYDomain[1] < 0) { - // If range is entirely negative, the x-axis is at the top of the plot, so doesn't take up any space. - size.plotHeight = size.heightMinusXAxisLabelHeight; - } - - const yScales = {}; - for (const theseUnits in domain.y) { - if (Object.prototype.hasOwnProperty.call(domain.y, theseUnits)) { - const thisYDomain = domain.y[theseUnits]; - yScales[theseUnits] = d3ScaleLinear() - .range([size.plotHeight, 0]) - .domain(thisYDomain); - } - } - return yScales; - } - - if (data.length === 0) { - return; - } - if (!defined(domain)) { - domain = computeDomain(); - if (!domain) { - return; - } - } - return { x: computeXScale(), y: computeYScales() }; - }, - - unknownUnits: unknown, - - /** - * Return the automatically-generated tick values, but with the last one removed if it is too close to the end. - * @param {d3.scale} scale The scale along which to calculate the tick values. - * @param {Integer} numberOfTicks Number of ticks. - * @return {Array} Tick values. - */ - truncatedTickValues(scale, numberOfTicks) { - const values = scale.ticks(numberOfTicks); - const lastValue = values[values.length - 1]; - if ( - (lastValue - scale.domain()[0]) / - (scale.domain()[1] - scale.domain()[0]) > - 1 - 0.4 / values.length - ) { - values.pop(); - } - return values; - } -}; - -export default Scales; diff --git a/lib/Charts/Size.js b/lib/Charts/Size.js deleted file mode 100644 index 2b5df1f7f2e..00000000000 --- a/lib/Charts/Size.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; -import Title from "./Title"; -const defaultXAxisHeight = 14; // The default height of the x-axis itself, ie. the numbering of the ticks. -const defaultXAxisLabelHeight = 20; // The default additional height of the x-axis label, eg. "time". - -const yAxisWidth = 45; - -const Size = { - calculate(element, margin, state, numberOfYAxes) { - const xAxisHeight = defaultValue(state.xAxisHeight, defaultXAxisHeight); - const xAxisLabelHeight = defaultValue( - state.xAxisLabelHeight, - defaultXAxisLabelHeight - ); - const yAxesWidth = numberOfYAxes * yAxisWidth; - const titleHeight = Title.getHeight(state.titleSettings); - const width = element.offsetWidth - margin.left - margin.right - yAxesWidth; - const height = - element.offsetHeight - margin.top - margin.bottom - titleHeight; - const heightMinusXAxisLabelHeight = - height - - (defined(state.axisLabel) && defined(state.axisLabel.x) - ? xAxisLabelHeight - : 0); - const plotHeight = - heightMinusXAxisLabelHeight - (state.mini ? 0 : xAxisHeight); - return { - width: width, - yAxesWidth: yAxesWidth, - height: height, - heightMinusXAxisLabelHeight: heightMinusXAxisLabelHeight, - plotHeight: plotHeight, - xAxisHeight: xAxisHeight, - xAxisLabelHeight: xAxisLabelHeight - }; - }, - - yAxisWidth: yAxisWidth -}; - -export default Size; diff --git a/lib/Charts/Title.js b/lib/Charts/Title.js deleted file mode 100644 index 6657b4c0e25..00000000000 --- a/lib/Charts/Title.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; - -import { nest as d3Nest } from "d3-collection"; - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; - -const defaultClassName = "base-chart-title"; -const defaultHeight = 30; // The additional height of the title, which may in fact be the legend. - -/** - * Handles the drawing of the chart title, which may be a string or a legend. - * - * @param {String} [titleSettings.type='string'] May be 'string' or 'legend'. - * @param {String} [titleSettings.title] For 'string'-type titles, the title. - * @param {String} [titleSettings.className] The className to use for the title DOM element. Defaults to 'base-chart-title'. - * @param {Number} [titleSettings.height=defaultTitleHeight] The height of the title bar. - */ -const Title = { - className(titleSettings) { - return titleSettings && titleSettings.className - ? titleSettings.className - : defaultClassName; - }, - - getHeight(titleSettings) { - return defaultValue( - defined(titleSettings) ? titleSettings.height : 0, - defaultHeight - ); - }, - - create(d3Element, titleSettings) { - // For a nicely centered title, use css: .chart-title {left: 0, right: 0, text-align: center;} and maybe {margin: 0 auto;}. - d3Element - .append("div") - .attr("class", Title.className(titleSettings)) - .style("opacity", 1e-6) - .style("position", "absolute"); - }, - - enterUpdateAndExit( - d3Element, - titleSettings, - margin, - catalogItems, - transitionDuration - ) { - // The title might be the legend, or a simple string. - const title = d3Element - .select("." + Title.className(titleSettings)) - .style("top", margin.top + "px"); - title - .transition() - .duration(transitionDuration) - .style("opacity", Title.getHeight(titleSettings) > 0 ? 1 : 1e-6); - if (defined(titleSettings)) { - let titleData = catalogItems; - if (titleSettings.type === "string") { - titleData = [{ id: "_string__", name: titleSettings.title }]; - } - - // in d3 v4, selection.data method returns new selections - // rather than modifying the selection in-place. - const titleComponents = title - .selectAll(".title-component") - .data(titleData, (d) => d.id); - // Check whether there are multiple category names and/or column names. - const numberOfCategories = d3Nest().key((d) => d.categoryName).length; - const numberOfColumnNames = d3Nest().key((d) => d.name).length; - // This is to only show the interesting parts of the name & categoryName in the title, - // similar to Tooltip.js. - const getName = function (d, index) { - if (!d.categoryName) { - return d.name; - } - if (numberOfColumnNames === 1) { - return d.categoryName + (index === 0 ? " " + d.name : ""); - } - if (numberOfCategories === 1) { - return (index === 0 ? d.categoryName + " " : "") + d.name; - } - if (d.name === d.categoryName) { - return d.categoryName; - } - return d.categoryName + " " + d.name; - }; - // Enter. - const addedTitleComponents = titleComponents - .enter() - .append("span") - .attr("class", "title-component"); - - if (titleSettings.type === "legend") { - addedTitleComponents.append("span").attr("class", "color"); - } - addedTitleComponents.append("span").attr("class", "name"); - // Enter and update. - const mergedTitleComponents = addedTitleComponents.merge(titleComponents); - mergedTitleComponents - .select(".color") - .style("background-color", (d) => d.color) - .style("border-radius", (d) => { - return d.type === "momentPoints" ? "50%" : "0"; - }); - mergedTitleComponents.select(".name").text(getName); - // Exit. - titleComponents.exit().remove(); - } - } -}; - -export default Title; diff --git a/lib/Charts/Tooltip.js b/lib/Charts/Tooltip.js deleted file mode 100644 index bc3adc1aff6..00000000000 --- a/lib/Charts/Tooltip.js +++ /dev/null @@ -1,278 +0,0 @@ -"use strict"; - -import { nest as d3Nest } from "d3-collection"; -import { - select as d3Select, - event as d3Event, - clientPoint as d3ClientPoint -} from "d3-selection"; -import { transition as d3Transition } from "d3-transition"; // eslint-disable-line no-unused-vars - -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; -import defined from "terriajs-cesium/Source/Core/defined"; - -import dateformat from "dateformat"; - -const defaultTooltipOffset = { - // The meaning of these offsets depend on the alignment. - top: 10, - right: 10, - bottom: 10, - left: 10 -}; -const defaultClassName = "base-chart-tooltip"; -const defaultId = "base-chart-tooltip-id"; -const showHideDuration = 250; - -/** - * Handles the drawing of the chart tooltip, which shows the values of the selected data in a legend. - * - * @param {String} [tooltipSettings.id] The id to use for the tooltip DOM element, defaults to 'base-chart-tooltip-id'. Do not change this after creation. - * @param {String} [tooltipSettings.className] The className to use for the tooltip DOM element, defaults to 'base-chart-tooltip'. Do not change this after creation. - * @param {String} [tooltipSettings.align] One of 'hover' (hover at the mouse position), 'left', 'right', 'prefer-right' (chooses left or right depending on mouse position). - * @param {Object} [tooltipSettings.offset] An object with top, left and right properties; these properties' meanings depend on the alignment above. - * With right/left alignment, the offset is relative to the svg. - */ -const Tooltip = { - defaultClassName: defaultClassName, - defaultId: defaultId, - - id(tooltipSettings) { - return defaultValue(tooltipSettings.id, defaultId); - }, - - select(tooltipSettings) { - return d3Select("#" + Tooltip.id(tooltipSettings)); - }, - - create(container, tooltipSettings) { - // Make the tooltip DOM element, invisible to start. - if (defined(tooltipSettings)) { - container - .append("div") - .attr("id", Tooltip.id(tooltipSettings)) - .attr( - "class", - defaultValue(tooltipSettings.className, defaultClassName) - ) - .style("opacity", 1e-6) - .style("position", "absolute") - .style("display", "none"); - } - }, - - destroy(tooltipSettings) { - // Remove the tooltip DOM element. - if (defined(tooltipSettings)) { - const id = Tooltip.id(tooltipSettings); - const tooltipElement = d3Select("#" + id).nodes(); - if (tooltipElement) { - d3Select("#" + id).remove(); - //NOTE: why not remove it directly like above? - // tooltipElement.parentElement.removeChild(tooltipElement); - } - } - }, - - singleRowHtml(color, name, value, units) { - if (value === null) return; - const styleAttribute = defined(color) - ? 'style="background-color: ' + color + '" ' - : ""; - - const formattedVal = isNaN(value) ? value : value.toFixed(2); - - return ` - - - - - ${name} - - - ${formattedVal} - ${units || ""} - - `; - }, - - html(selectedData, xLocation) { - let html; - const readableX = - typeof xLocation.getMonth === "function" - ? dateformat(xLocation, "dd/mm/yyyy, HH:MMTT") - : xLocation; - html = '

' + readableX + "

"; - - // If there is only one line showing, then label it with the category name, not the column name. - // Else, if there is only one column name (shared by all the categories), show the category names - // and don't show the column name. - // Else, if there is only one category name, then there is no need to show it. - // In general, show both, grouped by category name. - - // If there is only a moment dataset it's x values (a date will be shown) - if ( - selectedData.length === 1 && - (selectedData[0].type === "moment" || - selectedData[0].type === "momentPoints") - ) { - return html; - } else if (selectedData.length === 1) { - const onlyLine = selectedData[0]; - html += '

' + onlyLine.categoryName + "

"; - html += ""; - html += this.singleRowHtml( - onlyLine.color, - `${onlyLine.name}`, - onlyLine.type === "moment" || onlyLine.type === "momentPoints" - ? readableX - : onlyLine.point.y, - onlyLine.units - ); - html += "
"; - return html; - } - - // The next line turns [chartData1A, chartData2, chartData1B] into - // [{key: 'categoryName1', values: [chartData1A, chartData1B]}, {key: 'categoryName2', values: [chartData2]}]. - const dataGroupedByCategory = d3Nest() - .key((d) => d.categoryName) - .entries(selectedData); - // And similarly for the column names. - // const dataGroupedByName = d3Nest() - // .key(d => d.name) - // .entries(selectedData); - - // if (dataGroupedByName.length === 1) { - // // All lines have the same name. - // html += ''; - // dataGroupedByName[0].values.forEach(line => { - // html += this.singleRowHtml( - // line.color, - // line.categoryName, - // line.point.y, - // line.units - // ); - // }); - // html += "
"; - // return html; - // } - - dataGroupedByCategory.forEach((group) => { - if ( - group.values[0].type === "moment" || - group.values[0].type === "momentPoints" - ) { - return; - } - // if (dataGroupedByCategory.length > 1) { - // html += '

' + group.key + "

"; - // } - html += '

' + group.key + "

"; - html += ''; - group.values.forEach((line) => { - html += this.singleRowHtml( - line.color, - line.name, - line.point.y, - line.units - ); - }); - html += "
"; - }); - return html; - }, - - show(html, tooltipElement, tooltipSettings, boundingRect) { - tooltipElement - .html(html) - .style("display", "block") - .transition() - .duration(showHideDuration) - .style("opacity", 1) - .style("max-width", "300px") - .style("visibility", "visible"); - - const tooltipWidth = +tooltipElement.nodes()[0].offsetWidth; - const tooltipOffset = defaultValue( - tooltipSettings.offset, - defaultTooltipOffset - ); - let top, left, right; - - const clientPos = d3ClientPoint(tooltipElement.node().parentNode, d3Event); - const clientX = clientPos[0]; - const clientY = clientPos[1]; - - switch (tooltipSettings.align) { - case "left": - top = tooltipOffset.top; - left = tooltipOffset.left; - break; - case "right": - top = tooltipOffset.top; - right = tooltipOffset.right; - break; - case "prefer-right": { - // Only show on the left if we would be under the tooltip on the right, but not on the left. - top = tooltipOffset.top; - const leftEdgeWhenPositionedRight = - boundingRect.width - tooltipOffset.right - tooltipWidth; - const rightEdgeWhenPositionedLeft = tooltipOffset.left + tooltipWidth; - if ( - clientX >= leftEdgeWhenPositionedRight && - clientX > rightEdgeWhenPositionedLeft - ) { - left = tooltipOffset.left; - } else { - right = tooltipOffset.right; - } - break; - } - case "hover": - default: - top = d3Event.clientY - tooltipOffset.top; - left = d3Event.clientX + (-tooltipWidth - tooltipOffset.left); - break; - } - - const tooltipHeight = tooltipElement.node().offsetHeight; - - const possibleYClash = clientY < tooltipHeight + tooltipOffset.top; - if (possibleYClash) { - tooltipElement.style("bottom", "60px"); - tooltipElement.style("top", null); - } else { - tooltipElement.style("top", top + "px"); - tooltipElement.style("bottom", null); - } - - if (left !== undefined) { - tooltipElement.style("left", left + "px"); - } else { - tooltipElement.style("left", "auto"); - } - if (right !== undefined) { - tooltipElement.style("right", right + "px"); - } else { - tooltipElement.style("right", "auto"); - } - }, - - hide(tooltipElement) { - tooltipElement - .transition() - .duration(showHideDuration) - .style("opacity", 1e-6); - // visibility hidden cannot transition, and it is too flashy if you use it without. - // We need it because opacity=0 along can get in front of other elements and prevent the hover from working at all. - // So delay it until (and only if) the opacity has already done its job. - setTimeout(function () { - if (+tooltipElement.style("opacity") < 0.002) { - tooltipElement.style("visibility", "hidden"); - } - }, showHideDuration * 1.2); - } -}; - -export default Tooltip; diff --git a/lib/Charts/d3Sync.js b/lib/Charts/d3Sync.js deleted file mode 100644 index 3634a4cfd0e..00000000000 --- a/lib/Charts/d3Sync.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; - -const childNodesSelector = function () { - return this.childNodes; -}; - -/** - * Convenience wrapper that manages D3's enter/exit mechanics to synchronise an array of data - * with DOM elements. - * @param {Element} parent HTML element which will contain the nodes - * @param {Object[]} arrayData Data to be synchronised. - * @param {String} childElementTagName Name of HTML element to be created for each data point. - * @param {Function} updateCallBack Function called with (d3object, isNewElement). If it returns the d3object, - * an opacity transition will be applied. - * @param {Boolean} transition Parameter passed to d3.transition(). - */ -function d3Sync( - parent, - arrayData, - childElementTagName, - updateCallback, - transition = null -) { - // move stray elements that are not 'childElementTagName' to the bottom - // that way they can be removed with the fade out animation - parent - .selectAll(childNodesSelector) - .filter((d, i, nodes) => nodes[i].tagName !== childElementTagName) - .each(function () { - this.parentElement.appendChild(this); - }); - // synchronise intended elements - const existing = parent - .selectAll(childNodesSelector) - .data(arrayData, function (d) { - return d ? d.id || d : this.id; - }); // id hack for ChartData objects which get re-created each time there are any changes - const enter = existing.enter().append(childElementTagName); - const exit = existing.exit(); - if (transition) { - exit.transition(transition).style("opacity", 1e-2).remove(); - const entered = updateCallback(enter, true); - if (entered) { - // We don't want to randomly transition all attributes on new elements, because it looks bad. - // Instead, let's just transition opacity. - entered.style("opacity", 0).transition(transition).style("opacity", 1); - } - updateCallback(existing.transition(transition).style("opacity", 1), false); - } else { - exit.remove(); - updateCallback(enter, true); - updateCallback(existing, false); - } -} - -module.exports = d3Sync; diff --git a/lib/Charts/initializeChartTypes.js b/lib/Charts/initializeChartTypes.js deleted file mode 100644 index 6efa76d91e7..00000000000 --- a/lib/Charts/initializeChartTypes.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict"; - -import defined from "terriajs-cesium/Source/Core/defined"; - -import LineChart from "./LineChart"; -import BarChart from "./BarChart"; -import MomentChart from "./MomentChart"; -import MomentChartPoints from "./MomentChartPoints"; - -const chartType = { - line: new LineChart(), - bar: new BarChart(), - moment: new MomentChart(), - momentPoints: new MomentChartPoints() -}; - -function getChart(chartType) { - if (chartType === "line") return new LineChart(); - if (chartType === "bar") return new BarChart(); - if (chartType === "moment") return new MomentChart(); - if (chartType === "momentPoints") return new MomentChartPoints(); -} - -/** - * @param {ChartData} chartData ChartData object for which chart type needs to be determined. - */ -export function determineChartType(chartData) { - function pointsPerYear() { - const years = - (domain.x[1].getTime() - domain.x[0].getTime()) / - (3600 * 24 * 365.25 * 1000); - return numPoints / years; - } - - function pointsPerRangeUnit() { - const range = Math.floor(domain.x[1] - domain.x[0]); - return numPoints / range; - } - - const pointsWithoutY = () => - chartData.points.filter((p) => !defined(p.y)).length; - - // respect chartData.type - if (defined(chartType[chartData.type])) { - return getChart(chartData.type); - } - const numPoints = chartData.points.length; - if (numPoints === 0) return getChart("line"); - const domain = chartData.getDomain(); - const isDate = domain.x[0] instanceof Date; - try { - if ( - (isDate && pointsPerYear() < 1) || - (!isDate && pointsPerRangeUnit() < 1) || - pointsWithoutY() > 0 - ) { - return getChart("line"); - // return chartType.bar; // TODO restore when bar charts work perfectly. - } - } catch (e) { - console.error(e.stack); - } - return getChart("line"); -} - -/** - * singleton - */ -export function initializeChartData(chartData) { - if (chartData.renderer === undefined) { - chartData.renderer = determineChartType(chartData); - } -} - -/** - * Ensure that we have determined the chart type for every data item in our state. - */ -function initializeChartTypes(state) { - state.chartItems.forEach(initializeChartData); -} - -export default initializeChartTypes; diff --git a/lib/Core/ConsoleAnalytics.d.ts b/lib/Core/ConsoleAnalytics.d.ts new file mode 100644 index 00000000000..d59edf93e53 --- /dev/null +++ b/lib/Core/ConsoleAnalytics.d.ts @@ -0,0 +1,17 @@ +declare class ConsoleAnalytics { + start: ( + configParameters: Partial<{ + enableConsoleAnalytics: boolean; + googleAnalyticsKey: any; + googleAnalyticsOptions: any; + }> + ) => void; + logEvent: ( + category: string, + action: string, + label?: string, + value?: number + ) => void; +} + +export default ConsoleAnalytics; diff --git a/lib/Core/ConsoleAnalytics.js b/lib/Core/ConsoleAnalytics.js index 2a6dc4e662a..ba57a641e37 100644 --- a/lib/Core/ConsoleAnalytics.js +++ b/lib/Core/ConsoleAnalytics.js @@ -1,5 +1,3 @@ -"use strict"; - var defined = require("terriajs-cesium/Source/Core/defined").default; const i18next = require("i18next").default; @@ -49,4 +47,4 @@ ConsoleAnalytics.prototype.logEvent = function ( } }; -module.exports = ConsoleAnalytics; +export default ConsoleAnalytics; diff --git a/lib/Core/DataUri.js b/lib/Core/DataUri.ts similarity index 85% rename from lib/Core/DataUri.js rename to lib/Core/DataUri.ts index 0518ed093e8..8b8097284d0 100644 --- a/lib/Core/DataUri.js +++ b/lib/Core/DataUri.ts @@ -1,18 +1,14 @@ -"use strict"; - -var DataUri = { +export default { /** * Turn a file with the supplied type and stringified data into a data uri that can be set as the href of an anchor tag. * @param {String} type Data type, eg. 'json' or 'csv'. * @param {String} dataString The data. * @return {String} A string that can be used to in an anchor tag's 'href' attribute to represent downloadable data. */ - make: function (type, dataString) { + make: function (type: string, dataString: string): string | undefined { if (dataString) { // Using attachment/* mime type makes safari download as attachment. text/* works on Chrome (as does attachment). return "data:attachment/" + type + "," + encodeURIComponent(dataString); } } }; - -module.exports = DataUri; diff --git a/lib/Core/ServerConfig.d.ts b/lib/Core/ServerConfig.d.ts new file mode 100644 index 00000000000..7195274ec41 --- /dev/null +++ b/lib/Core/ServerConfig.d.ts @@ -0,0 +1,6 @@ +declare class ServerConfig { + config: unknown; + init(serverConfigUrl: string): Promise; +} + +export default ServerConfig; diff --git a/lib/Core/ServerConfig.js b/lib/Core/ServerConfig.js index 766d0bb136e..0d9a3b45725 100644 --- a/lib/Core/ServerConfig.js +++ b/lib/Core/ServerConfig.js @@ -1,5 +1,3 @@ -"use strict"; - const i18next = require("i18next").default; var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default; var loadJson = require("./loadJson").default; @@ -49,4 +47,4 @@ ServerConfig.prototype.init = function (serverConfigUrl) { }); }; -module.exports = ServerConfig; +export default ServerConfig; diff --git a/lib/Core/animation.ts b/lib/Core/animation.ts index 4a18f7e8322..fac0929bf78 100644 --- a/lib/Core/animation.ts +++ b/lib/Core/animation.ts @@ -2,19 +2,19 @@ * The purpose of this module is to provide a standard way to perform actions after an animations ends. * The advantage is that the code is more tightly coupled together and we don't have to guess when the animation ends. * There is also a timeout fallback that will reject the Promise if the animation end event is not fired in time. - * + * * Old code: * triggerSomeAnimation(); setTimeout(function() { somePostAction(); }, 200); - * - * New code: + * + * New code: animateEnd(this.elementRef.current).finally(somePostAction); triggerAnimation(); - * - * + * + * */ const ANIMATION_TIMEOUT = 500; @@ -33,10 +33,10 @@ const transitionEnd = (element: Element | null) => }); const animationTimeout = ( - timeoutID: ReturnType | undefined + _timeoutID: ReturnType | undefined ) => new Promise((_, reject) => { - timeoutID = setTimeout(() => { + _timeoutID = setTimeout(() => { reject("Animation timed out. Did you forget to animate the element?"); }, ANIMATION_TIMEOUT); }); diff --git a/lib/Core/arrayContains.js b/lib/Core/arrayContains.js deleted file mode 100644 index 8930b8214ad..00000000000 --- a/lib/Core/arrayContains.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -function arrayContains(array, value) { - for (var i = 0; i < array.length; ++i) { - if (array[i] === value) { - return true; - } - } - - return false; -} -module.exports = arrayContains; diff --git a/lib/Core/autoUpdate.ts b/lib/Core/autoUpdate.ts index f98e137e830..27a9300312a 100644 --- a/lib/Core/autoUpdate.ts +++ b/lib/Core/autoUpdate.ts @@ -19,7 +19,7 @@ export default function autoUpdate(updater: (...args: any[]) => void) { updater.call(args.context, args.value); }); }, - (disposer, value) => { + (disposer, _value) => { console.log("cleanup"); if (disposer) { disposer(); diff --git a/lib/Core/containsAny.js b/lib/Core/containsAny.js deleted file mode 100644 index dac94168d30..00000000000 --- a/lib/Core/containsAny.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -var defined = require("terriajs-cesium/Source/Core/defined").default; - -/** - * Determins is a given string contains any of a number of possible strings. - * - * @param {String} s The string to test. - * @param {String[]} possibleStrings The possible strings to test `s` for. - * @return {Boolean} true if `s` contains any of the strings in `possibleStrings`; otherwise, false. - */ -var containsAny = function (s, possibleStrings) { - if (!defined(s)) { - return false; - } - - for (var i = 0; i < possibleStrings.length; ++i) { - if (s.indexOf(possibleStrings[i]) >= 0) { - return true; - } - } - return false; -}; - -module.exports = containsAny; diff --git a/lib/Core/containsAny.ts b/lib/Core/containsAny.ts new file mode 100644 index 00000000000..9c16a415932 --- /dev/null +++ b/lib/Core/containsAny.ts @@ -0,0 +1,23 @@ +import defined from "terriajs-cesium/Source/Core/defined"; + +/** + * Determins is a given string contains any of a number of possible strings. + * + * @param s The string to test. + * @param possibleStrings The possible strings to test `s` for. + * @return true if `s` contains any of the strings in `possibleStrings`; otherwise, false. + */ +const containsAny = function (s: string, possibleStrings: string[]) { + if (!defined(s)) { + return false; + } + + for (let i = 0; i < possibleStrings.length; ++i) { + if (s.indexOf(possibleStrings[i]) >= 0) { + return true; + } + } + return false; +}; + +export default containsAny; diff --git a/lib/Core/createColorForIdTransformer.ts b/lib/Core/createColorForIdTransformer.ts index 8c94dd4486b..cd4d6850eeb 100644 --- a/lib/Core/createColorForIdTransformer.ts +++ b/lib/Core/createColorForIdTransformer.ts @@ -15,7 +15,7 @@ const usedColors: { [c: string]: number | undefined } = {}; */ function createColorForIdTransformer() { return createTransformer( - (id: string): string => { + (_id: string): string => { const nextColor = leastUsedColor(); useColor(nextColor); return nextColor; diff --git a/lib/Core/formatPropertyValue.js b/lib/Core/formatPropertyValue.ts similarity index 84% rename from lib/Core/formatPropertyValue.js rename to lib/Core/formatPropertyValue.ts index ba83320aec4..31da5c7ebe1 100644 --- a/lib/Core/formatPropertyValue.js +++ b/lib/Core/formatPropertyValue.ts @@ -1,6 +1,4 @@ -"use strict"; - -var linkifyContent = require("./linkifyContent"); +import linkifyContent from "./linkifyContent"; /** * Format the value for the description, used by the Feature Info Panel. @@ -9,7 +7,7 @@ var linkifyContent = require("./linkifyContent"); * @param {} value The value to format. * @param {Object} [options] Number formatting options, passed to Number.toLocaleString(). */ -function formatPropertyValue(value, options) { +function formatPropertyValue(value: any, options?: Intl.NumberFormatOptions) { if (typeof value === "number") { // Note we default useGrouping to false (not true) and maximumFractionDigits to 20 (not 3). return value.toLocaleString(undefined, { @@ -27,4 +25,4 @@ function formatPropertyValue(value, options) { return value; } -module.exports = formatPropertyValue; +export default formatPropertyValue; diff --git a/lib/Core/getDataType.ts b/lib/Core/getDataType.ts index 02404486f9f..dcdb062a8f9 100644 --- a/lib/Core/getDataType.ts +++ b/lib/Core/getDataType.ts @@ -1,10 +1,12 @@ import i18next from "i18next"; import { action, observable } from "mobx"; +import { type ComponentType } from "react"; interface DataType { value: string; name: string; description?: string; + customComponent?: ComponentType; } export interface RemoteDataType extends DataType {} @@ -44,6 +46,10 @@ const builtinRemoteDataTypes: RemoteDataType[] = [ value: "esri-mapServer", name: "core.dataType.esri-mapServer" }, + { + value: "esri-imageServer", + name: "core.dataType.esri-imageServer" + }, /* { value: "esri-mapServer-group", name: "Esri ArcGIS MapServer" @@ -124,6 +130,14 @@ const builtinRemoteDataTypes: RemoteDataType[] = [ { value: "json", name: "core.dataType.json" + }, + { + value: "cog", + name: "core.dataType.cog" + }, + { + value: "i3s", + name: "core.dataType.i3s" } // Add next builtin remote upload type ]; @@ -181,18 +195,19 @@ const builtinLocalDataTypes: LocalDataType[] = [ name: "core.dataType.shp", extensions: ["zip"] } + // Add next builtin local upload type ]; /** - * Custom remote data types. Add to it by calling addRemoteDataType(). + * Custom remote data types. Add to it by calling addOrReplaceRemoteFileUploadType(). */ export const customRemoteDataTypes: Map = observable( new Map() ); /** - * Custom local data types. Add by calling addLocalDataType(). + * Custom local data types. Add by calling addOrReplaceLocalFileUploadType(). */ export const customLocalDataTypes: Map = observable( new Map() @@ -200,24 +215,24 @@ export const customLocalDataTypes: Map = observable( export default function getDataTypes(): GetDataTypes { const uniqueRemoteDataTypes: Map = new Map([ - ...(builtinRemoteDataTypes.map((dtype) => [ - dtype.value, - translateDataType(dtype) - ]) as [string, RemoteDataType][]), + ...(builtinRemoteDataTypes.map((dtype) => [dtype.value, dtype]) as [ + string, + RemoteDataType + ][]), ...customRemoteDataTypes.entries() ]); const uniqueLocalDataTypes: Map = new Map([ - ...(builtinLocalDataTypes.map((dtype) => [ - dtype.value, - translateDataType(dtype) - ]) as [string, LocalDataType][]), + ...(builtinLocalDataTypes.map((dtype) => [dtype.value, dtype]) as [ + string, + LocalDataType + ][]), ...customLocalDataTypes.entries() ]); return { - remoteDataType: [...uniqueRemoteDataTypes.values()], - localDataType: [...uniqueLocalDataTypes.values()] + remoteDataType: [...uniqueRemoteDataTypes.values()].map(translateDataType), + localDataType: [...uniqueLocalDataTypes.values()].map(translateDataType) }; } @@ -256,6 +271,7 @@ function translateDataType(dataType: T): T { name: i18next.t(dataType.name), description: dataType.description ? i18next.t(dataType.description) - : undefined + : undefined, + customComponent: dataType.customComponent }; } diff --git a/lib/Core/linkifyContent.js b/lib/Core/linkifyContent.ts similarity index 72% rename from lib/Core/linkifyContent.js rename to lib/Core/linkifyContent.ts index c8063ae9b8e..af31b3cccd0 100644 --- a/lib/Core/linkifyContent.js +++ b/lib/Core/linkifyContent.ts @@ -1,11 +1,11 @@ -"use strict"; +import linkifyIt from "linkify-it"; -var linkify = require("linkify-it")(); +const linkify = linkifyIt(); -function linkifyContent(content) { - var matches = linkify.match(content), - result = [], - last; +function linkifyContent(content: string) { + const matches = linkify.match(content), + result = []; + let last: number; if (matches) { last = 0; @@ -29,4 +29,4 @@ function linkifyContent(content) { return content; } -module.exports = linkifyContent; +export default linkifyContent; diff --git a/lib/Core/loadArrayBuffer.ts b/lib/Core/loadArrayBuffer.ts index 575ef970e8f..b1ca292fffa 100644 --- a/lib/Core/loadArrayBuffer.ts +++ b/lib/Core/loadArrayBuffer.ts @@ -6,5 +6,3 @@ export default function loadArrayBuffer( ): Promise { return Resource.fetchArrayBuffer({ url: urlOrResource, headers: headers })!; } - -module.exports = loadArrayBuffer; diff --git a/lib/Core/loadText.js b/lib/Core/loadText.js deleted file mode 100644 index 2c05abe1128..00000000000 --- a/lib/Core/loadText.js +++ /dev/null @@ -1,10 +0,0 @@ -const Resource = require("terriajs-cesium/Source/Core/Resource").default; - -function loadText(urlOrResource, headers) { - var resource = Resource.createIfNeeded(urlOrResource); - return resource.fetchText({ - headers: headers - }); -} - -module.exports = loadText; diff --git a/lib/Core/loadText.ts b/lib/Core/loadText.ts new file mode 100644 index 00000000000..dcf4af63aac --- /dev/null +++ b/lib/Core/loadText.ts @@ -0,0 +1,12 @@ +import Resource from "terriajs-cesium/Source/Core/Resource"; + +async function loadText(urlOrResource: string | Resource): Promise { + const resource = (Resource as any).createIfNeeded(urlOrResource) as Resource; + const response = resource.fetchText(); + if (response === undefined) { + throw new Error("Request throttled"); + } + return response; +} + +export default loadText; diff --git a/lib/Core/loadWithXhr.d.ts b/lib/Core/loadWithXhr.d.ts new file mode 100644 index 00000000000..70f42f9119c --- /dev/null +++ b/lib/Core/loadWithXhr.d.ts @@ -0,0 +1,13 @@ +import Resource from "terriajs-cesium/Source/Core/Resource"; + +interface Options extends Resource.ConstructorOptions { + responseType?: string; + headers?: any; + overrideMimeType?: string; + method?: "GET" | "POST" | "PUT"; + data?: any; +} + +declare function loadWithXhr(options: Options): Promise; + +export default loadWithXhr; diff --git a/lib/Core/loadWithXhr.js b/lib/Core/loadWithXhr.js index 668a6bab9b4..d656fe46f32 100644 --- a/lib/Core/loadWithXhr.js +++ b/lib/Core/loadWithXhr.js @@ -31,4 +31,4 @@ Object.defineProperties(loadWithXhr, { } }); -module.exports = loadWithXhr; +export default loadWithXhr; diff --git a/lib/Core/loadXML.js b/lib/Core/loadXML.ts similarity index 51% rename from lib/Core/loadXML.js rename to lib/Core/loadXML.ts index 6987ba8f0d5..863f2ab3dd0 100644 --- a/lib/Core/loadXML.js +++ b/lib/Core/loadXML.ts @@ -1,8 +1,10 @@ -const Resource = require("terriajs-cesium/Source/Core/Resource").default; +import Resource from "terriajs-cesium/Source/Core/Resource"; -async function loadXML(urlOrResource) { - var resource = Resource.createIfNeeded(urlOrResource); - const respone = await resource.fetchXML(); +async function loadXML( + urlOrResource: string | Resource +): Promise { + const resource = (Resource as any).createIfNeeded(urlOrResource) as Resource; + const response = await resource.fetchXML(); /** * Sometimes Cesium's Resource.fetchXML will return an Array Buffer (usually in Node.js env) @@ -13,17 +15,17 @@ async function loadXML(urlOrResource) { * See full license here https://github.com/fengyuanchen/is-array-buffer/blob/main/LICENSE */ if ( - respone instanceof ArrayBuffer || - toString.call(respone) === "[object ArrayBuffer]" + response instanceof ArrayBuffer || + toString.call(response) === "[object ArrayBuffer]" ) { const enc = new TextDecoder("utf-8"); - const xmlString = enc.decode(respone); + const xmlString = enc.decode(response as any); const parser = new DOMParser(); return parser.parseFromString(xmlString, "text/xml"); } - return respone; + return response; } -module.exports = loadXML; +export default loadXML; diff --git a/lib/Core/markdownToHtml.ts b/lib/Core/markdownToHtml.ts index 63e5231c452..fafb1df3a20 100644 --- a/lib/Core/markdownToHtml.ts +++ b/lib/Core/markdownToHtml.ts @@ -1,10 +1,8 @@ -"use strict"; - -const defined = require("terriajs-cesium/Source/Core/defined").default; -const MarkdownIt = require("markdown-it"); -const DOMPurify = require("dompurify/dist/purify"); -import injectTerms from "./injectTerms"; +import DOMPurify from "dompurify"; +import MarkdownIt from "markdown-it"; +import defined from "terriajs-cesium/Source/Core/defined"; import { Term } from "../ReactViewModels/defaultTerms"; +import injectTerms from "./injectTerms"; const md = new MarkdownIt({ html: true, diff --git a/lib/Core/pollToPromise.js b/lib/Core/pollToPromise.js deleted file mode 100644 index d66d1a41558..00000000000 --- a/lib/Core/pollToPromise.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; - -var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default; -var getTimestamp = require("terriajs-cesium/Source/Core/getTimestamp").default; - -var pollToPromise = function (f, options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); - - var pollInterval = defaultValue(options.pollInterval, 1); - var timeout = defaultValue(options.timeout, 5000); - - return new Promise((resolve, reject) => { - var startTimestamp = getTimestamp(); - var endTimestamp = startTimestamp + timeout; - - function poller() { - if (f()) { - resolve(); - } else { - if (getTimestamp() > endTimestamp) { - reject(); - } else { - setTimeout(poller, pollInterval); - } - } - } - - poller(); - }); -}; - -module.exports = pollToPromise; diff --git a/lib/Core/pollToPromise.ts b/lib/Core/pollToPromise.ts new file mode 100644 index 00000000000..57efa3c01c6 --- /dev/null +++ b/lib/Core/pollToPromise.ts @@ -0,0 +1,35 @@ +import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; +import getTimestamp from "terriajs-cesium/Source/Core/getTimestamp"; + +interface Options { + pollInterval?: number; + timeout?: number; +} + +const pollToPromise = function (f: () => boolean, options: Options) { + options = defaultValue(options, (defaultValue as any).EMPTY_OBJECT); + + const pollInterval = defaultValue(options.pollInterval, 1); + const timeout = defaultValue(options.timeout, 5000); + + return new Promise((resolve, reject) => { + const startTimestamp = getTimestamp(); + const endTimestamp = startTimestamp + timeout; + + function poller() { + if (f()) { + resolve(); + } else { + if (getTimestamp() > endTimestamp) { + reject(); + } else { + setTimeout(poller, pollInterval); + } + } + } + + poller(); + }); +}; + +export default pollToPromise; diff --git a/lib/Core/readJson.js b/lib/Core/readJson.ts similarity index 58% rename from lib/Core/readJson.js rename to lib/Core/readJson.ts index da1a98c7d10..672d804f268 100644 --- a/lib/Core/readJson.js +++ b/lib/Core/readJson.ts @@ -1,20 +1,19 @@ -"use strict"; - -const json5 = require("json5"); -const readText = require("./readText"); +import json5 from "json5"; +import { JsonObject } from "./Json"; +import readText from "./readText"; /** * Try to read the file as JSON. If that fails, try JSON5. * @param {File} file The file. * @return {Promise} The JSON or json5 object described by the file. */ -function readJson(file) { +function readJson(file: Blob): Promise { return readText(file).then((s) => { try { - return JSON.parse(s); + return JSON.parse(s!); } catch (e) { if (e instanceof SyntaxError) { - return json5.parse(s); + return json5.parse(s!); } else { throw e; } @@ -22,4 +21,4 @@ function readJson(file) { }); } -module.exports = readJson; +export default readJson; diff --git a/lib/Core/readText.js b/lib/Core/readText.ts similarity index 53% rename from lib/Core/readText.js rename to lib/Core/readText.ts index fc85b495436..622362de8f0 100644 --- a/lib/Core/readText.js +++ b/lib/Core/readText.ts @@ -1,10 +1,7 @@ -"use strict"; -const i18next = require("i18next").default; +import i18next from "i18next"; +import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError"; -const DeveloperError = - require("terriajs-cesium/Source/Core/DeveloperError").default; - -function readText(file) { +function readText(file: Blob): Promise { return new Promise((resolve, reject) => { if (typeof file === "undefined") { throw new DeveloperError(i18next.t("core.readText.fileRequired")); @@ -14,8 +11,8 @@ function readText(file) { reader.readAsText(file); reader.onload = function (event) { - const allText = event.target.result; - resolve(allText); + const allText = event.target?.result; + resolve((allText ?? undefined) as string | undefined); }; reader.onerror = function (e) { reject(e); @@ -23,4 +20,4 @@ function readText(file) { }); } -module.exports = readText; +export default readText; diff --git a/lib/Core/readXml.js b/lib/Core/readXml.js deleted file mode 100644 index c21fb7264be..00000000000 --- a/lib/Core/readXml.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -const i18next = require("i18next").default; -var readText = require("./readText"); - -var RuntimeError = require("terriajs-cesium/Source/Core/RuntimeError").default; - -var parser; - -function readXml(file) { - return readText(file).then(function (result) { - if (!parser) { - parser = new DOMParser(); - } - - var xml = parser.parseFromString(result, "application/xml"); - if ( - !xml || - !xml.documentElement || - xml.getElementsByTagName("parsererror").length > 0 - ) { - throw new RuntimeError(i18next.t("core.readXml.xmlError")); - } - return xml; - }); -} - -module.exports = readXml; diff --git a/lib/Core/readXml.ts b/lib/Core/readXml.ts new file mode 100644 index 00000000000..c2816aeeaa2 --- /dev/null +++ b/lib/Core/readXml.ts @@ -0,0 +1,28 @@ +import i18next from "i18next"; +import readText from "./readText"; +import RuntimeError from "terriajs-cesium/Source/Core/RuntimeError"; + +let parser: DOMParser; + +function readXml(file: Blob) { + return readText(file).then(function (result) { + if (!parser) { + parser = new DOMParser(); + } + + if (!result) { + return undefined; + } + const xml = parser.parseFromString(result, "application/xml"); + if ( + !xml || + !xml.documentElement || + xml.getElementsByTagName("parsererror").length > 0 + ) { + throw new RuntimeError(i18next.t("core.readXml.xmlError")); + } + return xml; + }); +} + +export default readXml; diff --git a/lib/Core/replaceUnderscores.js b/lib/Core/replaceUnderscores.js deleted file mode 100644 index e6e500ff6fd..00000000000 --- a/lib/Core/replaceUnderscores.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -/** - * Replace all underscores in the string with spaces. If the argument is not a string, return it unchanged. - * @param {} string The string to replace. If the argument is not a string, does nothing. - * @return {} The argument with all underscores replaced with spaces. If the argument is not a string, returns the argument unchanged. - */ -function replaceUnderscores(string) { - if (typeof string === "string" || string instanceof String) { - return string.replace(/_/g, " "); - } - return string; -} - -module.exports = replaceUnderscores; diff --git a/lib/Core/replaceUnderscores.ts b/lib/Core/replaceUnderscores.ts new file mode 100644 index 00000000000..c8772e389fa --- /dev/null +++ b/lib/Core/replaceUnderscores.ts @@ -0,0 +1,12 @@ +/** + * Replace all underscores in the string with spaces. If the argument is not a string, return it unchanged. + * @param string The string to replace. If the argument is not a string, does nothing. + * @return The argument with all underscores replaced with spaces. If the argument is not a string, returns the argument unchanged. + */ +function replaceUnderscores(str: any): typeof str { + return typeof str === "string" || str instanceof String + ? str.replace(/_/g, " ") + : str; +} + +export default replaceUnderscores; diff --git a/lib/Core/triggerResize.js b/lib/Core/triggerResize.ts similarity index 61% rename from lib/Core/triggerResize.js rename to lib/Core/triggerResize.ts index 664226ddf15..017b57f0399 100644 --- a/lib/Core/triggerResize.js +++ b/lib/Core/triggerResize.ts @@ -1,16 +1,12 @@ -"use strict"; - /** * Trigger a window resize event. */ -function triggerResize() { +export default function triggerResize() { try { window.dispatchEvent(new Event("resize")); } catch (e) { - var evt = window.document.createEvent("UIEvents"); + const evt = window.document.createEvent("UIEvents"); evt.initUIEvent("resize", true, false, window, 0); window.dispatchEvent(evt); } } - -module.exports = triggerResize; diff --git a/lib/Map/Cesium/CesiumRenderLoopPauser.ts b/lib/Map/Cesium/CesiumRenderLoopPauser.ts index 2b85a0f0fad..632bd19694e 100644 --- a/lib/Map/Cesium/CesiumRenderLoopPauser.ts +++ b/lib/Map/Cesium/CesiumRenderLoopPauser.ts @@ -2,7 +2,6 @@ import defined from "terriajs-cesium/Source/Core/defined"; import destroyObject from "terriajs-cesium/Source/Core/destroyObject"; import CesiumEvent from "terriajs-cesium/Source/Core/Event"; import getTimestamp from "terriajs-cesium/Source/Core/getTimestamp"; -import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; import TaskProcessor from "terriajs-cesium/Source/Core/TaskProcessor"; import CesiumWidget from "terriajs-cesium/Source/Widget/CesiumWidget"; @@ -285,7 +284,7 @@ export default class CesiumRenderLoopPauser { this.renderingIsPaused = false; } - private postRender(date: JulianDate) { + private postRender() { // We can safely stop rendering when: // - the camera position hasn't changed in over a second, // - there are no tiles waiting to load, and diff --git a/lib/Map/ColorMap/EnumColorMap.ts b/lib/Map/ColorMap/EnumColorMap.ts index 30a721778e5..96afafeff23 100644 --- a/lib/Map/ColorMap/EnumColorMap.ts +++ b/lib/Map/ColorMap/EnumColorMap.ts @@ -40,7 +40,6 @@ export default class EnumColorMap extends ColorMap { } const values = this.values; - let i, len; for (let i = 0, len = values.length; i < len; ++i) { if (values[i] === value) { return this.colors[i]; diff --git a/lib/Map/DragPoints/CesiumDragPoints.js b/lib/Map/DragPoints/CesiumDragPoints.js index c7d36c6f918..18f1a57b3aa 100644 --- a/lib/Map/DragPoints/CesiumDragPoints.js +++ b/lib/Map/DragPoints/CesiumDragPoints.js @@ -43,7 +43,7 @@ var CesiumDragPoints = function (terria, pointMovedCallback) { /** * Whether user is currently dragging point. - * @type {Bool} + * @type {Boolean} */ this._dragInProgress = false; @@ -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) { diff --git a/lib/Map/DragPoints/DragPoints.d.ts b/lib/Map/DragPoints/DragPoints.d.ts new file mode 100644 index 00000000000..a450bdb86c1 --- /dev/null +++ b/lib/Map/DragPoints/DragPoints.d.ts @@ -0,0 +1,17 @@ +import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource"; +import Terria from "../../Models/Terria"; + +declare class DragPoints { + constructor( + terria: Terria, + pointMovedCallback: (draggableObjects: CustomDataSource) => void + ); + + setUp(): void; + updateDraggableObjects(draggableObjects: CustomDataSource): void; + getDragCount(): number; + resetDragCount(): void; + destroy(): void; +} + +export default DragPoints; diff --git a/lib/Map/DragPoints/DragPoints.js b/lib/Map/DragPoints/DragPoints.js index fc50c39646b..0062d4b6c97 100644 --- a/lib/Map/DragPoints/DragPoints.js +++ b/lib/Map/DragPoints/DragPoints.js @@ -1,5 +1,3 @@ -"use strict"; - var defined = require("terriajs-cesium/Source/Core/defined").default; var CesiumDragPoints = require("./CesiumDragPoints"); var LeafletDragPoints = require("./LeafletDragPoints"); @@ -99,4 +97,4 @@ DragPoints.prototype._createDragPointsHelper = function (pointMovedCallback) { } }; -module.exports = DragPoints; +export default DragPoints; diff --git a/lib/Map/DragPoints/LeafletDragPoints.js b/lib/Map/DragPoints/LeafletDragPoints.js index b5d129e51c9..a5896610c01 100644 --- a/lib/Map/DragPoints/LeafletDragPoints.js +++ b/lib/Map/DragPoints/LeafletDragPoints.js @@ -39,7 +39,7 @@ var LeafletDragPoints = function (terria, pointMovedCallback) { /** * Whether user is currently dragging point. - * @type {Bool} + * @type {Boolean} */ this._dragInProgress = false; diff --git a/lib/Map/ImageryProvider/ArcGisImageServerImageryProvider.ts b/lib/Map/ImageryProvider/ArcGisImageServerImageryProvider.ts new file mode 100644 index 00000000000..49b098b716d --- /dev/null +++ b/lib/Map/ImageryProvider/ArcGisImageServerImageryProvider.ts @@ -0,0 +1,230 @@ +import { makeObservable } from "mobx"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import Credit from "terriajs-cesium/Source/Core/Credit"; +import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; +import CesiumEvent from "terriajs-cesium/Source/Core/Event"; +import GeographicProjection from "terriajs-cesium/Source/Core/GeographicProjection"; +import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme"; +import Math from "terriajs-cesium/Source/Core/Math"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +import Resource from "terriajs-cesium/Source/Core/Resource"; +import TilingScheme from "terriajs-cesium/Source/Core/TilingScheme"; +import DiscardMissingTileImagePolicy from "terriajs-cesium/Source/Scene/DiscardMissingTileImagePolicy"; +import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; +import ImageryProvider from "terriajs-cesium/Source/Scene/ImageryProvider"; +import TileDiscardPolicy from "terriajs-cesium/Source/Scene/TileDiscardPolicy"; +import { JsonObject } from "../../Core/Json"; +import { ImageServerIdentifyResult } from "../../Models/Catalog/Esri/ArcGisInterfaces"; + +interface Options { + url: string; + token?: string; + minimumLevel?: number; + maximumLevel?: number; + rectangle?: Rectangle; + credit: Credit | string; + enablePickFeatures?: boolean; + usePreCachedTiles?: boolean; + tileWidth?: number; + tileHeight?: number; + tilingScheme?: TilingScheme; + parameters?: JsonObject; +} + +/** This is adapted from Cesium's ArcGisMapServerImageryProvider + * https://github.com/CesiumGS/cesium/blob/51aae2d21014cfc28e948b1719d07f1912df9434/packages/engine/Source/Scene/ArcGisMapServerImageryProvider.js + * Code licensed under the Apache License v2.0. + * For details, see https://github.com/CesiumGS/cesium/blob/main/LICENSE.md + */ + +export default class ArcGisImageServerImageryProvider { + readonly tilingScheme: TilingScheme; + readonly ellipsoid: Ellipsoid; + readonly tileWidth: number; + readonly tileHeight: number; + readonly minimumLevel: number; + readonly maximumLevel: number; + readonly rectangle: Rectangle; + readonly errorEvent = new CesiumEvent(); + readonly ready = true; + readonly credit: Credit; + + /** Note: this can be set dynamically */ + enablePickFeatures: boolean; + readonly usePreCachedTiles: boolean; + readonly tileDiscardPolicy: TileDiscardPolicy; + readonly baseResource: Resource; + + readonly defaultNightAlpha = undefined; + readonly defaultDayAlpha = undefined; + readonly hasAlphaChannel = true; + readonly defaultAlpha = undefined; + readonly defaultBrightness = undefined; + readonly defaultContrast = undefined; + readonly defaultGamma = undefined; + readonly defaultHue = undefined; + readonly defaultSaturation = undefined; + readonly defaultMagnificationFilter = undefined; + readonly defaultMinificationFilter = undefined; + readonly readyPromise = Promise.resolve(true); + + constructor(options: Options) { + makeObservable(this); + + this.tilingScheme = options.tilingScheme ?? new GeographicTilingScheme(); + + this.rectangle = options.rectangle ?? this.tilingScheme.rectangle; + this.ellipsoid = Ellipsoid.WGS84; + + this.tileWidth = options.tileWidth ?? 256; + this.tileHeight = options.tileHeight ?? 256; + + this.minimumLevel = options.minimumLevel ?? 0; + this.maximumLevel = options.maximumLevel ?? 25; + + this.ready = true; + + this.credit = + typeof options.credit === "string" + ? new Credit(options.credit) + : options.credit; + + this.enablePickFeatures = options.enablePickFeatures ?? true; + this.usePreCachedTiles = options.usePreCachedTiles ?? false; + + this.baseResource = new Resource(options.url); + this.baseResource.appendForwardSlash(); + + if (options.parameters) { + this.baseResource.appendQueryParameters(options.parameters); + } + + if (options.token) { + this.baseResource.appendQueryParameters({ + token: options.token + }); + } + + this.tileDiscardPolicy = new DiscardMissingTileImagePolicy({ + missingImageUrl: this.buildImageResource(0, 0, this.maximumLevel).url, + pixelsToCheck: [ + new Cartesian2(0, 0), + new Cartesian2(200, 20), + new Cartesian2(20, 200), + new Cartesian2(80, 110), + new Cartesian2(160, 130) + ], + disableCheckIfAllPixelsAreTransparent: true + }); + } + + get proxy() { + return this.baseResource.proxy; + } + + buildImageResource(x: number, y: number, level: number) { + if (this.usePreCachedTiles) { + return this.baseResource.getDerivedResource({ + url: `tile/${level}/${y}/${x}` + }); + } else { + const nativeRectangle = this.tilingScheme.tileXYToNativeRectangle( + x, + y, + level + ); + const bbox = `${nativeRectangle.west},${nativeRectangle.south},${nativeRectangle.east},${nativeRectangle.north}`; + + const query: JsonObject = { + bbox: bbox, + size: `${this.tileWidth},${this.tileHeight}`, + format: "png32", + transparent: true, + f: "image" + }; + + if (this.tilingScheme.projection instanceof GeographicProjection) { + query.bboxSR = 4326; + query.imageSR = 4326; + } else { + query.bboxSR = 3857; + query.imageSR = 3857; + } + + return this.baseResource.getDerivedResource({ + url: "exportImage", + queryParameters: query + }); + } + } + + getTileCredits(): Credit[] { + return []; + } + + requestImage(x: number, y: number, level: number): any { + return ImageryProvider.loadImage( + this, + this.buildImageResource(x, y, level) + ); + } + + async pickFeatures( + x: number, + y: number, + level: number, + longitude: number, + latitude: number + ): Promise { + if (!this.enablePickFeatures) { + return []; + } + + let horizontal; + let vertical; + let sr; + if (this.tilingScheme.projection instanceof GeographicProjection) { + horizontal = Math.toDegrees(longitude); + vertical = Math.toDegrees(latitude); + sr = "4326"; + } else { + const projected = this.tilingScheme.projection.project( + new Cartographic(longitude, latitude, 0.0) + ); + horizontal = projected.x; + vertical = projected.y; + sr = "3857"; + } + + const query = { + f: "json", + geometryType: "esriGeometryPoint", + geometry: `{x: ${horizontal}, y: ${vertical}, spatialReference: {wkid: ${sr}}}`, + // Disable catalog items - as we don't use them + returnCatalogItems: false + }; + + const resource = this.baseResource.getDerivedResource({ + url: "identify", + queryParameters: query + }); + + const json = (await resource.fetchJson()) as ImageServerIdentifyResult; + const result: ImageryLayerFeatureInfo[] = []; + + if (json.value) { + const featureInfo = new ImageryLayerFeatureInfo(); + featureInfo.data = json; + featureInfo.name = json.name; + featureInfo.properties = json.properties; + featureInfo.description = json.value; + + result.push(featureInfo); + } + + // Todo: handle json.catalogItems + + return result; + } +} diff --git a/lib/Map/Leaflet/ImageryProviderLeafletTileLayer.ts b/lib/Map/Leaflet/ImageryProviderLeafletTileLayer.ts index 2927d091c29..8d1dea7c2dd 100644 --- a/lib/Map/Leaflet/ImageryProviderLeafletTileLayer.ts +++ b/lib/Map/Leaflet/ImageryProviderLeafletTileLayer.ts @@ -152,11 +152,11 @@ export default class ImageryProviderLeafletTileLayer extends L.TileLayer { // an error event. We want to first raise an error event that optionally returns a promise and // retries after the promise resolves. - const doRequest = (waitPromise?: any) => { + const _doRequest = (waitPromise?: any) => { if (waitPromise) { waitPromise .then(function () { - doRequest(); + _doRequest(); }) .catch((e: unknown) => { // The tile has failed irrecoverably, so invoke Leaflet's standard diff --git a/lib/Map/Leaflet/LeafletDataSourceDisplay.ts b/lib/Map/Leaflet/LeafletDataSourceDisplay.ts index 5e80a0023f6..38fac9eaa13 100644 --- a/lib/Map/Leaflet/LeafletDataSourceDisplay.ts +++ b/lib/Map/Leaflet/LeafletDataSourceDisplay.ts @@ -1,21 +1,19 @@ +import L from "leaflet"; import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; +import EventHelper from "terriajs-cesium/Source/Core/EventHelper"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import createGuid from "terriajs-cesium/Source/Core/createGuid"; +import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; +import destroyObject from "terriajs-cesium/Source/Core/destroyObject"; import BoundingSphereState from "terriajs-cesium/Source/DataSources/BoundingSphereState"; import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource"; import DataSource from "terriajs-cesium/Source/DataSources/DataSource"; import DataSourceCollection from "terriajs-cesium/Source/DataSources/DataSourceCollection"; -import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; import Entity from "terriajs-cesium/Source/DataSources/Entity"; import EntityCluster from "terriajs-cesium/Source/DataSources/EntityCluster"; -import EventHelper from "terriajs-cesium/Source/Core/EventHelper"; import isDefined from "../../Core/isDefined"; -import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; -import L from "leaflet"; import LeafletScene from "./LeafletScene"; -const createGuid = require("terriajs-cesium/Source/Core/createGuid").default; -const destroyObject = - require("terriajs-cesium/Source/Core/destroyObject").default; - interface Visualizer { update(time: JulianDate): boolean; destroy(): void; diff --git a/lib/Map/Leaflet/LeafletScene.ts b/lib/Map/Leaflet/LeafletScene.ts index b5c682264e7..02785b1236a 100644 --- a/lib/Map/Leaflet/LeafletScene.ts +++ b/lib/Map/Leaflet/LeafletScene.ts @@ -1,4 +1,5 @@ import CesiumEvent from "terriajs-cesium/Source/Core/Event"; +import L from "leaflet"; export default class LeafletScene { readonly map: L.Map; diff --git a/lib/Map/Leaflet/LeafletSelectionIndicator.ts b/lib/Map/Leaflet/LeafletSelectionIndicator.ts index dc263c542c9..69fe7239620 100644 --- a/lib/Map/Leaflet/LeafletSelectionIndicator.ts +++ b/lib/Map/Leaflet/LeafletSelectionIndicator.ts @@ -1,26 +1,19 @@ +import L from "leaflet"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; import EasingFunction from "terriajs-cesium/Source/Core/EasingFunction"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; -import L from "leaflet"; +import CesiumMath from "terriajs-cesium/Source/Core/Math"; +import TweenCollection from "terriajs-cesium/Source/Scene/TweenCollection"; import isDefined from "../../Core/isDefined"; import Leaflet from "../../Models/Leaflet"; -const TweenCollection = - require("terriajs-cesium/Source/Scene/TweenCollection").default; const selectionIndicatorUrl = require("../../../wwwroot/images/NM-LocationTarget.svg"); interface Tween { cancelTween(): void; } -interface TweenCollection { - length: number; - add(args: any): Tween; - update(): void; -} - const cartographicScratch = new Cartographic(); export default class LeafletSelectionIndicator { diff --git a/lib/Map/Leaflet/LeafletVisualizer.ts b/lib/Map/Leaflet/LeafletVisualizer.ts index 40ec1940394..cafce0d4afa 100644 --- a/lib/Map/Leaflet/LeafletVisualizer.ts +++ b/lib/Map/Leaflet/LeafletVisualizer.ts @@ -1,28 +1,25 @@ +import L, { LatLngBounds, LatLngBoundsLiteral, PolylineOptions } from "leaflet"; import AssociativeArray from "terriajs-cesium/Source/Core/AssociativeArray"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; import Color from "terriajs-cesium/Source/Core/Color"; -import DataSource from "terriajs-cesium/Source/DataSources/DataSource"; +import destroyObject from "terriajs-cesium/Source/Core/destroyObject"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; -import Entity from "terriajs-cesium/Source/DataSources/Entity"; -import EntityCollection from "terriajs-cesium/Source/DataSources/EntityCollection"; -import EntityCluster from "terriajs-cesium/Source/DataSources/EntityCluster"; -import isDefined from "../../Core/isDefined"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; -import L, { LatLngBounds, PolylineOptions, LatLngBoundsLiteral } from "leaflet"; -import LeafletScene from "./LeafletScene"; +import CesiumMath from "terriajs-cesium/Source/Core/Math"; import PolygonHierarchy from "terriajs-cesium/Source/Core/PolygonHierarchy"; -import PolylineGlowMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineGlowMaterialProperty"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +import writeTextToCanvas from "terriajs-cesium/Source/Core/writeTextToCanvas"; +import DataSource from "terriajs-cesium/Source/DataSources/DataSource"; +import Entity from "terriajs-cesium/Source/DataSources/Entity"; +import EntityCluster from "terriajs-cesium/Source/DataSources/EntityCluster"; +import EntityCollection from "terriajs-cesium/Source/DataSources/EntityCollection"; import PolylineDashMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineDashMaterialProperty"; +import PolylineGlowMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineGlowMaterialProperty"; import Property from "terriajs-cesium/Source/DataSources/Property"; -import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +import isDefined from "../../Core/isDefined"; import { getLineStyleLeaflet } from "../../Models/Catalog/Esri/esriLineStyle"; - -const destroyObject = - require("terriajs-cesium/Source/Core/destroyObject").default; -const writeTextToCanvas = - require("terriajs-cesium/Source/Core/writeTextToCanvas").default; +import LeafletScene from "./LeafletScene"; interface PointDetails { layer?: L.CircleMarker; @@ -1106,8 +1103,6 @@ function getDashArray( material: PolylineDashMaterialProperty, time: JulianDate ): number[] { - let dashArray; - const dashPattern = material.dashPattern ? material.dashPattern.getValue(time) : undefined; @@ -1347,7 +1342,7 @@ function convertEntityPositionsToLatLons(positions: Cartesian3[]): L.LatLng[] { export default class LeafletVisualizer { visualizersCallback( leafletScene: LeafletScene, - _entityCluster: EntityCluster, + _entityCluster: EntityCluster | undefined, dataSource: DataSource ) { const entities = dataSource.entities; diff --git a/lib/Map/LeafletPatched.js b/lib/Map/LeafletPatched.ts similarity index 90% rename from lib/Map/LeafletPatched.js rename to lib/Map/LeafletPatched.ts index 4583f16674e..55062c0e9d0 100644 --- a/lib/Map/LeafletPatched.js +++ b/lib/Map/LeafletPatched.ts @@ -3,7 +3,8 @@ import L from "leaflet"; // Function taken from Leaflet 1.0.1 (https://github.com/Leaflet/Leaflet/blob/v1.0.1/src/layer/vector/Canvas.js#L254-L267) // Leaflet 1.0.2 and later don't trigger click events for every Path, so feature selection only gives 1 result. // Updated to incorporate function changes up to v1.7.1 -L.Canvas.prototype._onClick = function (e) { +const Canvas = L.Canvas as any; +Canvas.prototype._onClick = function (e: any) { const point = this._map.mouseEventToLayerPoint(e); const layers = []; for (let order = this._drawFirst; order; order = order.next) { diff --git a/lib/Map/SizeMap/ConstantPointSizeMap.ts b/lib/Map/SizeMap/ConstantPointSizeMap.ts index 2257852902b..6a3638d5f8c 100644 --- a/lib/Map/SizeMap/ConstantPointSizeMap.ts +++ b/lib/Map/SizeMap/ConstantPointSizeMap.ts @@ -5,7 +5,7 @@ export default class ConstantPointSizeMap extends PointSizeMap { super(); } - mapValueToPointSize(value: string | number | null | undefined): number { + mapValueToPointSize(_value: string | number | null | undefined): number { return this.pointSize; } } diff --git a/lib/Map/Vector/EarthGravityModel1996.d.ts b/lib/Map/Vector/EarthGravityModel1996.d.ts new file mode 100644 index 00000000000..d894292d80c --- /dev/null +++ b/lib/Map/Vector/EarthGravityModel1996.d.ts @@ -0,0 +1,10 @@ +declare class EarthGravityModel1996 { + readonly minimumHeight: number; + readonly maximumHeight: number; + + constructor(gridFileUrl: string); + + getHeight(longitude: number, latitude: number): Promise; +} + +export default EarthGravityModel1996; diff --git a/lib/Map/Vector/EarthGravityModel1996.js b/lib/Map/Vector/EarthGravityModel1996.js index 01d75defd56..cd301cff88b 100644 --- a/lib/Map/Vector/EarthGravityModel1996.js +++ b/lib/Map/Vector/EarthGravityModel1996.js @@ -2,7 +2,7 @@ var CesiumMath = require("terriajs-cesium/Source/Core/Math").default; var defined = require("terriajs-cesium/Source/Core/defined").default; -var loadArrayBuffer = require("../../Core/loadArrayBuffer"); +var loadArrayBuffer = require("../../Core/loadArrayBuffer").default; /** * The Earth Gravity Model 1996 (EGM96) geoid. diff --git a/lib/Map/Vector/Proj4Definitions.ts b/lib/Map/Vector/Proj4Definitions.ts index c56abf86a98..f91b93b3175 100644 --- a/lib/Map/Vector/Proj4Definitions.ts +++ b/lib/Map/Vector/Proj4Definitions.ts @@ -59,7 +59,11 @@ const Proj4Definitions: Record = { "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs", "EPSG:7844": "+proj=longlat +ellps=GRS80 +no_defs +type=crs", "EPSG:7855": - "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs" + "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", + "EPSG:7899": + "+proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", + "EPSG:8059": + "+proj=lcc +lat_0=-32 +lon_0=135 +lat_1=-28 +lat_2=-36 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs" }; export default Proj4Definitions; diff --git a/lib/Map/Vector/Reproject.ts b/lib/Map/Vector/Reproject.ts index b883042f591..51e505fd528 100644 --- a/lib/Map/Vector/Reproject.ts +++ b/lib/Map/Vector/Reproject.ts @@ -1,9 +1,8 @@ +import proj4 from "proj4"; import defined from "terriajs-cesium/Source/Core/defined"; -import Proj4Definitions from "./Proj4Definitions"; import urijs from "urijs"; - -const proj4 = require("proj4").default; -const loadText = require("../../Core/loadText"); +import loadText from "../../Core/loadText"; +import Proj4Definitions from "./Proj4Definitions"; export default { TERRIA_CRS: "EPSG:4326", @@ -60,16 +59,21 @@ export default { ): [number, number] | undefined { const source = sourceCode in Proj4Definitions - ? new proj4.Proj(Proj4Definitions[sourceCode]) + ? proj4.Proj(Proj4Definitions[sourceCode]) : undefined; const dest = destCode in Proj4Definitions - ? new proj4.Proj(Proj4Definitions[destCode]) + ? proj4.Proj(Proj4Definitions[destCode]) : undefined; - if (!sourceCode || !destCode) { + if (!source || !dest) { return; } - return proj4(source, dest, coordinates); + const result = proj4.transform(source, dest, coordinates) ?? {}; + if (result) { + const { x, y } = result; + return [x, y]; + } + return undefined; }, /** diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 533841e91f8..1ce85b28853 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -11,8 +11,6 @@ import { } from "mobx"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import clone from "terriajs-cesium/Source/Core/clone"; -import Color from "terriajs-cesium/Source/Core/Color"; import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; import IonResource from "terriajs-cesium/Source/Core/IonResource"; import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; @@ -24,7 +22,6 @@ import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTil import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; import Cesium3DTilePointFeature from "terriajs-cesium/Source/Scene/Cesium3DTilePointFeature"; import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; -import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; import AbstractConstructor from "../Core/AbstractConstructor"; import isDefined from "../Core/isDefined"; import { isJsonObject, JsonObject } from "../Core/Json"; @@ -33,9 +30,7 @@ import TerriaError from "../Core/TerriaError"; import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; import CommonStrata from "../Models/Definition/CommonStrata"; import createStratumInstance from "../Models/Definition/createStratumInstance"; -import LoadableStratum from "../Models/Definition/LoadableStratum"; -import Model, { BaseModel } from "../Models/Definition/Model"; -import StratumOrder from "../Models/Definition/StratumOrder"; +import Model from "../Models/Definition/Model"; import TerriaFeature from "../Models/Feature/Feature"; import Cesium3DTilesCatalogItemTraits from "../Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits"; import Cesium3dTilesTraits, { @@ -45,32 +40,12 @@ import CatalogMemberMixin, { getName } from "./CatalogMemberMixin"; import ClippingMixin from "./ClippingMixin"; import MappableMixin from "./MappableMixin"; import ShadowMixin from "./ShadowMixin"; - -class Cesium3dTilesStratum extends LoadableStratum(Cesium3dTilesTraits) { - constructor(...args: any[]) { - super(...args); - makeObservable(this); - } - - duplicateLoadableStratum(model: BaseModel): this { - return new Cesium3dTilesStratum() as this; - } - - @computed - get opacity() { - return 1.0; - } -} - -// Register the Cesium3dTilesStratum -StratumOrder.instance.addLoadStratum(Cesium3dTilesStratum.name); - -const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; +import Cesium3dTilesStyleMixin from "./Cesium3dTilesStyleMixin"; interface Cesium3DTilesCatalogItemIface extends InstanceType> {} -class ObservableCesium3DTileset extends Cesium3DTileset { +export class ObservableCesium3DTileset extends Cesium3DTileset { _catalogItem?: Cesium3DTilesCatalogItemIface; @observable destroyed = false; @@ -95,17 +70,14 @@ class ObservableCesium3DTileset extends Cesium3DTileset { type BaseType = Model; function Cesium3dTilesMixin>(Base: T) { - abstract class Cesium3dTilesMixin extends ClippingMixin( - ShadowMixin(MappableMixin(CatalogMemberMixin(Base))) + abstract class Cesium3dTilesMixin extends Cesium3dTilesStyleMixin( + ClippingMixin(ShadowMixin(MappableMixin(CatalogMemberMixin(Base)))) ) { protected tileset?: ObservableCesium3DTileset; constructor(...args: any[]) { super(...args); makeObservable(this); - runInAction(() => { - this.strata.set(Cesium3dTilesStratum.name, new Cesium3dTilesStratum()); - }); } get hasCesium3dTilesMixin() { @@ -374,98 +346,6 @@ function Cesium3dTilesMixin>(Base: T) { return resource; } - @computed get showExpressionFromFilters() { - if (!isDefined(this.filters)) { - return; - } - const terms = this.filters.map((filter) => { - if (!isDefined(filter.property)) { - return ""; - } - - // Escape single quotes, cast property value to number - const property = - "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; - const min = - isDefined(filter.minimumValue) && - isDefined(filter.minimumShown) && - filter.minimumShown > filter.minimumValue - ? property + " >= " + filter.minimumShown - : ""; - const max = - isDefined(filter.maximumValue) && - isDefined(filter.maximumShown) && - filter.maximumShown < filter.maximumValue - ? property + " <= " + filter.maximumShown - : ""; - - return [min, max].filter((x) => x.length > 0).join(" && "); - }); - - const showExpression = terms.filter((x) => x.length > 0).join("&&"); - if (showExpression.length > 0) { - return showExpression; - } - } - - @computed get cesiumTileStyle() { - if ( - !isDefined(this.style) && - (!isDefined(this.opacity) || this.opacity === 1) && - !isDefined(this.showExpressionFromFilters) - ) { - return; - } - - const style = clone(toJS(this.style) || {}); - const opacity = clone(toJS(this.opacity)); - - if (!isDefined(style.defines)) { - style.defines = { opacity }; - } else { - style.defines = Object.assign(style.defines, { opacity }); - } - - // Rewrite color expression to also use the models opacity setting - if (!isDefined(style.color)) { - // Some tilesets (eg. point clouds) have a ${COLOR} variable which - // stores the current color of a feature, so if we have that, we should - // use it, and only change the opacity. We have to do it - // component-wise because `undefined` is mapped to a large float value - // (czm_infinity) in glsl in Cesium and so can only be compared with - // another float value. - // - // There is also a subtle bug which prevents us from using an - // expression in the alpha part of the rgba(). eg, using the - // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' - // to generate an opacity value will cause Cesium to generate wrong - // translucency values making the tileset translucent even when the - // computed opacity is 1.0. It also makes the whole of the point cloud - // appear white when zoomed out to some distance. So for now, the only - // solution is to discard the opacity from the tileset and only use the - // value from the opacity trait. - style.color = - "(rgba(" + - "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + - "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + - "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + - "${opacity}" + - "))"; - } else if (typeof style.color === "string") { - // Check if the color specified is just a css color - const cssColor = Color.fromCssColorString(style.color); - if (isDefined(cssColor)) { - style.color = `color('${style.color}', \${opacity})`; - } - } - - if (isDefined(this.showExpressionFromFilters)) { - style.show = toJS(this.showExpressionFromFilters); - } - - return new Cesium3DTileStyle(style); - } - /** * This function should return null if allowFeaturePicking = false * @param _screenPosition @@ -633,15 +513,6 @@ function Cesium3dTilesMixin>(Base: T) { } }); } - - /** - * The color to use for highlighting features in this catalog item. - * - */ - @override - get highlightColor(): string { - return super.highlightColor || DEFAULT_HIGHLIGHT_COLOR; - } } return Cesium3dTilesMixin; @@ -655,8 +526,6 @@ namespace Cesium3dTilesMixin { } } -export default Cesium3dTilesMixin; - function normalizeShowExpression(show: any): { conditions: [string, boolean][]; } { @@ -682,3 +551,5 @@ function normalizeColorExpression(expr: any): { if (isJsonObject(expr)) Object.assign(normalized, expr); return normalized; } + +export default Cesium3dTilesMixin; diff --git a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts new file mode 100644 index 00000000000..4736be189de --- /dev/null +++ b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts @@ -0,0 +1,167 @@ +import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; +import AbstractConstructor from "../Core/AbstractConstructor"; +import isDefined from "../Core/isDefined"; +import Model, { BaseModel } from "../Models/Definition/Model"; +import Cesium3dTilesTraits from "../Traits/TraitsClasses/Cesium3dTilesTraits"; +import clone from "terriajs-cesium/Source/Core/clone"; +import { computed, makeObservable, override, runInAction, toJS } from "mobx"; +import Color from "terriajs-cesium/Source/Core/Color"; +import LoadableStratum from "../Models/Definition/LoadableStratum"; +import StratumOrder from "../Models/Definition/StratumOrder"; + +class Cesium3dTilesStyleStratum extends LoadableStratum(Cesium3dTilesTraits) { + constructor(...args: any[]) { + super(...args); + makeObservable(this); + } + + duplicateLoadableStratum(model: BaseModel): this { + return new Cesium3dTilesStyleStratum(model) as this; + } + + @computed + get opacity() { + return 1.0; + } +} + +// Register the I3SStratum +StratumOrder.instance.addLoadStratum(Cesium3dTilesStyleStratum.name); + +type BaseType = Model; + +const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; + +function Cesium3dTilesStyleMixin>( + Base: T +) { + abstract class Cesium3dTilesStyleMixin extends Base { + constructor(...args: any[]) { + super(...args); + makeObservable(this); + runInAction(() => { + this.strata.set( + Cesium3dTilesStyleStratum.name, + new Cesium3dTilesStyleStratum() + ); + }); + } + + get hasCesium3dTilesStyleMixin() { + return true; + } + + /** + * The color to use for highlighting features in this catalog item. + * + */ + @override + get highlightColor(): string { + return super.highlightColor || DEFAULT_HIGHLIGHT_COLOR; + } + + @computed + get showExpressionFromFilters() { + if (!isDefined(this.filters)) { + return; + } + const terms = this.filters.map((filter) => { + if (!isDefined(filter.property)) { + return ""; + } + + // Escape single quotes, cast property value to number + const property = + "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; + const min = + isDefined(filter.minimumValue) && + isDefined(filter.minimumShown) && + filter.minimumShown > filter.minimumValue + ? property + " >= " + filter.minimumShown + : ""; + const max = + isDefined(filter.maximumValue) && + isDefined(filter.maximumShown) && + filter.maximumShown < filter.maximumValue + ? property + " <= " + filter.maximumShown + : ""; + + return [min, max].filter((x) => x.length > 0).join(" && "); + }); + + const showExpression = terms.filter((x) => x.length > 0).join("&&"); + if (showExpression.length > 0) { + return showExpression; + } + } + + @computed get cesiumTileStyle(): Cesium3DTileStyle | undefined { + if ( + !isDefined(this.style) && + (!isDefined(this.opacity) || this.opacity === 1) && + !isDefined(this.showExpressionFromFilters) + ) { + return; + } + + const style = clone(toJS(this.style) || {}); + const opacity = clone(toJS(this.opacity)); + + if (!isDefined(style.defines)) { + style.defines = { opacity }; + } else { + style.defines = Object.assign(style.defines, { opacity }); + } + + // Rewrite color expression to also use the models opacity setting + if (!isDefined(style.color)) { + // Some tilesets (eg. point clouds) have a ${COLOR} variable which + // stores the current color of a feature, so if we have that, we should + // use it, and only change the opacity. We have to do it + // component-wise because `undefined` is mapped to a large float value + // (czm_infinity) in glsl in Cesium and so can only be compared with + // another float value. + // + // There is also a subtle bug which prevents us from using an + // expression in the alpha part of the rgba(). eg, using the + // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' + // to generate an opacity value will cause Cesium to generate wrong + // translucency values making the tileset translucent even when the + // computed opacity is 1.0. It also makes the whole of the point cloud + // appear white when zoomed out to some distance. So for now, the only + // solution is to discard the opacity from the tileset and only use the + // value from the opacity trait. + style.color = + "(rgba(" + + "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + + "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + + "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + + "${opacity}" + + "))"; + } else if (typeof style.color === "string") { + // Check if the color specified is just a css color + const cssColor = Color.fromCssColorString(style.color); + if (isDefined(cssColor)) { + style.color = `color('${style.color}', \${opacity})`; + } + } + + if (isDefined(this.showExpressionFromFilters)) { + style.show = toJS(this.showExpressionFromFilters); + } + + return new Cesium3DTileStyle(style); + } + } + return Cesium3dTilesStyleMixin; +} + +namespace Cesium3dTilesStyleMixin { + export interface Instance + extends InstanceType> {} + export function isMixedInto(model: any): model is Instance { + return model && model.hasCesium3dTilesStyleMixin; + } +} + +export default Cesium3dTilesStyleMixin; diff --git a/lib/ModelMixins/CesiumIonMixin.ts b/lib/ModelMixins/CesiumIonMixin.ts new file mode 100644 index 00000000000..e0dfba3f260 --- /dev/null +++ b/lib/ModelMixins/CesiumIonMixin.ts @@ -0,0 +1,47 @@ +import { observable, runInAction } from "mobx"; +import IonResource from "terriajs-cesium/Source/Core/IonResource"; +import AbstractConstructor from "../Core/AbstractConstructor"; +import Model from "../Models/Definition/Model"; +import CatalogMemberTraits from "../Traits/TraitsClasses/CatalogMemberTraits"; +import CesiumIonTraits from "../Traits/TraitsClasses/CesiumIonTraits"; + +type BaseType = Model; + +/** + * A mixin for a model that can be loaded from Cesium ion via an `ionAssetId`. If an asset ID is supplied, + * the `ionResource` will be populated asynchronously after `loadIonResource` is called (usually from + * `forceLoadMetadata`). If defined, the resource in this property should be used as the "URL" given to + * CesiumJS instead of a regular URL. + */ +export default function CesiumIonMixin>( + Base: T +) { + abstract class CesiumIonMixin extends Base { + /** + * The {@link IonResource} that can be given to CesiumJS most places that a resource URL can be used. + */ + @observable + ionResource: IonResource | undefined = undefined; + + /** + * Populates the the `ionResource` from the `ionAssetId`, `ionAccessToken`, and `ionServer` + * traits. This should be called from `forceLoadMetadata`. + */ + async loadIonResource(): Promise { + if (this.ionAssetId) { + const resource = await IonResource.fromAssetId(this.ionAssetId, { + accessToken: this.ionAccessToken, + server: this.ionServer + }); + + runInAction(() => { + this.ionResource = resource; + }); + } else { + this.ionResource = undefined; + } + } + } + + return CesiumIonMixin; +} diff --git a/lib/ModelMixins/DiffableMixin.ts b/lib/ModelMixins/DiffableMixin.ts index 311136f7f7d..5ce2ccf6ea5 100644 --- a/lib/ModelMixins/DiffableMixin.ts +++ b/lib/ModelMixins/DiffableMixin.ts @@ -1,17 +1,16 @@ import { computed, makeObservable, override } from "mobx"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import AbstractConstructor from "../Core/AbstractConstructor"; -import createStratumInstance from "../Models/Definition/createStratumInstance"; import LoadableStratum from "../Models/Definition/LoadableStratum"; import Model, { BaseModel } from "../Models/Definition/Model"; -import StratumOrder from "../Models/Definition/StratumOrder"; +import createStratumInstance from "../Models/Definition/createStratumInstance"; import { SelectableDimensionEnum } from "../Models/SelectableDimensions/SelectableDimensions"; import DiffableTraits from "../Traits/TraitsClasses/DiffableTraits"; import LegendTraits from "../Traits/TraitsClasses/LegendTraits"; import MappableMixin from "./MappableMixin"; import TimeFilterMixin from "./TimeFilterMixin"; -class DiffStratum extends LoadableStratum(DiffableTraits) { +export class DiffStratum extends LoadableStratum(DiffableTraits) { static stratumName = "diffStratum"; constructor(readonly catalogItem: DiffableMixin.Instance) { super(); @@ -56,6 +55,22 @@ class DiffStratum extends LoadableStratum(DiffableTraits) { get disableDateTimeSelector() { return this.catalogItem.isShowingDiff; } + + @computed + get disableExport() { + // disable export if showing diff + // currently there is no way to generate export for the difference layer as + // it requires 2 time parameters which is not supported in standard WCS + return this.catalogItem.isShowingDiff; + } + + @computed + get disableSplitter() { + // disable splitter if showing diff + // currently there is no use splitting the difference layer because + // most comparable features like style, datetime etc are disabled. + return this.catalogItem.isShowingDiff; + } } type BaseType = Model & MappableMixin.Instance; @@ -113,8 +128,6 @@ namespace DiffableMixin { export function isMixedInto(model: any): model is Instance { return model?.hasDiffableMixin; } - - StratumOrder.addLoadStratum(DiffStratum.stratumName); } export default DiffableMixin; diff --git a/lib/ModelMixins/DiscretelyTimeVaryingMixin.ts b/lib/ModelMixins/DiscretelyTimeVaryingMixin.ts index 5fc5cd9b8ab..e230f35d360 100644 --- a/lib/ModelMixins/DiscretelyTimeVaryingMixin.ts +++ b/lib/ModelMixins/DiscretelyTimeVaryingMixin.ts @@ -207,6 +207,14 @@ function DiscretelyTimeVaryingMixin< : this.discreteTimesAsSortedJulianDates![index].time; } + @computed({ equals: JulianDate.equals }) + get nextDiscreteJulianDate() { + const index = this.nextDiscreteTimeIndex; + return index === undefined + ? undefined + : this.discreteTimesAsSortedJulianDates![index].time; + } + @computed get currentDiscreteTimeTag() { const index = this.currentDiscreteTimeIndex; diff --git a/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts b/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts index 90261b5a13b..fd776e03f0e 100644 --- a/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts +++ b/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts @@ -36,19 +36,19 @@ function FeatureInfoUrlTemplateMixin>( abstract buildFeatureFromPickResult( screenPosition: Cartesian2 | undefined, pickResult: any - ): TerriaFeature | undefined; + ): Promise | TerriaFeature | undefined; /** * Returns a {@link Feature} for the pick result. If `featureInfoUrlTemplate` is set, * it asynchronously loads additional info from the url. */ @action - getFeaturesFromPickResult( + async getFeaturesFromPickResult( screenPosition: Cartesian2 | undefined, pickResult: any, loadExternal = true - ): TerriaFeature | undefined { - const feature = this.buildFeatureFromPickResult( + ): Promise { + const feature = await this.buildFeatureFromPickResult( screenPosition, pickResult ); diff --git a/lib/ModelMixins/GltfMixin.ts b/lib/ModelMixins/GltfMixin.ts index 7819882b6a7..80b59873f1a 100644 --- a/lib/ModelMixins/GltfMixin.ts +++ b/lib/ModelMixins/GltfMixin.ts @@ -9,6 +9,7 @@ import ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantPropert import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource"; import Entity from "terriajs-cesium/Source/DataSources/Entity"; import ModelGraphics from "terriajs-cesium/Source/DataSources/ModelGraphics"; +import Axis from "terriajs-cesium/Source/Scene/Axis"; import HeightReference from "terriajs-cesium/Source/Scene/HeightReference"; import AbstractConstructor from "../Core/AbstractConstructor"; import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; @@ -17,11 +18,7 @@ import GltfTraits from "../Traits/TraitsClasses/GltfTraits"; import CatalogMemberMixin from "./CatalogMemberMixin"; import MappableMixin from "./MappableMixin"; import ShadowMixin from "./ShadowMixin"; - -// We want TS to look at the type declared in lib/ThirdParty/terriajs-cesium-extra/index.d.ts -// and import doesn't allows us to do that, so instead we use require + type casting to ensure -// we still maintain the type checking, without TS screaming with errors -const Axis: Axis = require("terriajs-cesium/Source/Scene/Axis").default; +import Resource from "terriajs-cesium/Source/Core/Resource"; type BaseType = Model; @@ -89,8 +86,8 @@ function GltfMixin>(Base: T) { @computed private get cesiumHeightReference() { const heightReference: HeightReference = - // @ts-expect-error - HeightReference[this.heightReference] || HeightReference.NONE; + HeightReference[this.heightReference as keyof typeof HeightReference] || + HeightReference.NONE; return heightReference; } @@ -143,15 +140,20 @@ function GltfMixin>(Base: T) { }; } - protected abstract get gltfModelUrl(): string | undefined; + protected abstract get gltfModelUrl(): string | Resource | undefined; @computed private get modelGraphics() { if (this.gltfModelUrl === undefined) { return undefined; } + + const url = this.gltfModelUrl; + const options = { - uri: new ConstantProperty(proxyCatalogItemUrl(this, this.gltfModelUrl)), + uri: new ConstantProperty( + typeof url === "string" ? proxyCatalogItemUrl(this, url) : url + ), upAxis: new ConstantProperty(this.cesiumUpAxis), forwardAxis: new ConstantProperty(this.cesiumForwardAxis), scale: new ConstantProperty(this.scale !== undefined ? this.scale : 1), diff --git a/lib/ModelMixins/SearchProviders/WebFeatureServiceSearchProviderMixin.ts b/lib/ModelMixins/SearchProviders/WebFeatureServiceSearchProviderMixin.ts index 482740f1bfb..353e0e76def 100644 --- a/lib/ModelMixins/SearchProviders/WebFeatureServiceSearchProviderMixin.ts +++ b/lib/ModelMixins/SearchProviders/WebFeatureServiceSearchProviderMixin.ts @@ -178,7 +178,7 @@ function WebFeatureServiceSearchProviderMixin< results.results.push(...searchResults); }); }) - .catch((e) => { + .catch((_e) => { if (results.isCanceled) { // A new search has superseded this one, so ignore the result. return; diff --git a/lib/ModelMixins/TableMixin.ts b/lib/ModelMixins/TableMixin.ts index 4e84bbf715a..05edc35c267 100644 --- a/lib/ModelMixins/TableMixin.ts +++ b/lib/ModelMixins/TableMixin.ts @@ -229,7 +229,7 @@ function TableMixin>(Base: T) { } return { - name: (this.name || this.uniqueId)!, + name: name, file: new Blob([csvString]) }; } diff --git a/lib/ModelMixins/TimeFilterMixin.ts b/lib/ModelMixins/TimeFilterMixin.ts index 8794caf278e..3c2717bd7d3 100644 --- a/lib/ModelMixins/TimeFilterMixin.ts +++ b/lib/ModelMixins/TimeFilterMixin.ts @@ -161,7 +161,7 @@ function TimeFilterMixin>(Base: T) { if (!MappableMixin.isMixedInto(this)) return []; return filterOutUndefined( this.mapItems.map( - // @ts-expect-error + // @ts-expect-error url attr (mapItem) => ImageryParts.is(mapItem) && mapItem.imageryProvider.url ) ); diff --git a/lib/ModelMixins/XmlRequestMixin.ts b/lib/ModelMixins/XmlRequestMixin.ts index 6bd29d3c32a..5f64e01a919 100644 --- a/lib/ModelMixins/XmlRequestMixin.ts +++ b/lib/ModelMixins/XmlRequestMixin.ts @@ -1,9 +1,8 @@ import URI from "urijs"; import AbstractConstructor from "../Core/AbstractConstructor"; import isDefined from "../Core/isDefined"; - -const loadXML = require("../Core/loadXML"); -const loadWithXhr = require("../Core/loadWithXhr"); +import loadWithXhr from "../Core/loadWithXhr"; +import loadXML from "../Core/loadXML"; export default function XmlRequestMixin>( Base: T diff --git a/lib/Models/AugmentedVirtuality.ts b/lib/Models/AugmentedVirtuality.ts index d6f1b54ae51..f3e2336809a 100644 --- a/lib/Models/AugmentedVirtuality.ts +++ b/lib/Models/AugmentedVirtuality.ts @@ -1,22 +1,20 @@ import { action, computed, + makeObservable, observable, - runInAction, - makeObservable + runInAction } from "mobx"; import CesiumCartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import CesiumMatrix3 from "terriajs-cesium/Source/Core/Matrix3"; +import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; import Camera from "terriajs-cesium/Source/Scene/Camera"; import Scene from "terriajs-cesium/Source/Scene/Scene"; import Terria from "./Terria"; -const sampleTerrainMostDetailed = - require("terriajs-cesium/Source/Core/sampleTerrainMostDetailed").default; - interface EventLoopState { intervalId?: any; } diff --git a/lib/Models/BoxDrawing.ts b/lib/Models/BoxDrawing.ts index 08b68e6b5d8..72e9c5b1b0d 100644 --- a/lib/Models/BoxDrawing.ts +++ b/lib/Models/BoxDrawing.ts @@ -207,11 +207,6 @@ const FACE_POINT_VECTORS = [ new Cartesian3(0.0, 0.0, 0.5) ]; -// The box has 8 corner points and 6 face points that act as scaling grips. -// Here we represent them as 7 vectors in local coordinates space. -// Each vector represents a point and its opposite points can be easily derived from it. -const SCALE_POINT_VECTORS = [...CORNER_POINT_VECTORS, ...FACE_POINT_VECTORS]; - /** * Checks whether the given entity is updatable (i.e repsonds to box parameter changes). */ @@ -1779,23 +1774,6 @@ export function screenToGlobePosition( return globePosition; } -/** - * Project the given point to the ellipsoid surface. - */ -function projectPointToSurface( - position: Cartesian3, - result: Cartesian3 -): Cartesian3 { - const cartographic = Cartographic.fromCartesian( - position, - undefined, - scratchCartographic - ); - cartographic.height = 0; - return Cartographic.toCartesian(cartographic, undefined, result); -} -const scratchCartographic = new Cartographic(); - function setPlaneDimensions( boxDimensions: Cartesian3, planeNormalAxis: Axis, diff --git a/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts b/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts index e6c15f203f1..0b94e797386 100644 --- a/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts +++ b/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts @@ -245,15 +245,6 @@ export default class YDYRCatalogFunctionJob extends CatalogFunctionJobMixin( undefined ); - const regionColumnSplit = DATASETS.find( - (d) => d.title === this.parameters?.["Output Geography"] - )?.geographyName.split("_"); - let regionColumn = ""; - - if (isDefined(regionColumnSplit) && regionColumnSplit!.length === 2) { - regionColumn = `${regionColumnSplit![0]}_code_${regionColumnSplit![1]}`; - } - runInAction(() => { csvResult.setTrait(CommonStrata.user, "name", `${this.name} Results`); csvResult.setTrait( diff --git a/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts b/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts index 087a7836342..f574ccd6359 100644 --- a/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts @@ -64,7 +64,7 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( // Tag newly visible features with SEARCH_RESULT_TAG const disposeWatch = this._watchForNewTileFeatures( tileset, - (feature: Cesium3DTileFeature) => { + async (feature: Cesium3DTileFeature) => { const featureId = feature.getProperty(idPropertyName); if (resultIds.has(featureId)) { feature.setProperty(SEARCH_RESULT_TAG, true); @@ -72,7 +72,7 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( // If we only have a single result, show the feature info panel for it if (results.length === 1) { - disposeFeatureInfoPanel = openInfoPanelForFeature( + disposeFeatureInfoPanel = await openInfoPanelForFeature( this, feature, SEARCH_RESULT_TAG @@ -249,13 +249,13 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( * @returns A disposer to close the feature panel */ const openInfoPanelForFeature = action( - ( + async ( item: Cesium3DTilesCatalogItem, cesium3DTileFeature: Cesium3DTileFeature, excludePropertyFromPanel: string ) => { const pickedFeatures = new PickedFeatures(); - const feature = item.getFeaturesFromPickResult( + const feature = await item.getFeaturesFromPickResult( // The screenPosition param is not used by 3dtiles catalog item, // so just pass a fake value new Cartesian2(), diff --git a/lib/Models/Catalog/CatalogItems/CogCatalogItem.ts b/lib/Models/Catalog/CatalogItems/CogCatalogItem.ts new file mode 100644 index 00000000000..a3975329f16 --- /dev/null +++ b/lib/Models/Catalog/CatalogItems/CogCatalogItem.ts @@ -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 | 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 { + 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 { + // 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) + ); +} diff --git a/lib/Models/Catalog/CatalogItems/CzmlCatalogItem.ts b/lib/Models/Catalog/CatalogItems/CzmlCatalogItem.ts index 6be0d08849a..4458988214a 100644 --- a/lib/Models/Catalog/CatalogItems/CzmlCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/CzmlCatalogItem.ts @@ -19,6 +19,7 @@ import StratumOrder from "../../Definition/StratumOrder"; import HasLocalData from "../../HasLocalData"; import { ModelConstructorParameters } from "../../Definition/Model"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; +import CesiumIonMixin from "../../../ModelMixins/CesiumIonMixin"; /** * A loadable stratum for CzmlCatalogItemTraits that derives TimeVaryingTraits @@ -70,7 +71,9 @@ StratumOrder.addLoadStratum(CzmlTimeVaryingStratum.stratumName); export default class CzmlCatalogItem extends AutoRefreshingMixin( MappableMixin( - UrlMixin(CatalogMemberMixin(CreateModel(CzmlCatalogItemTraits))) + UrlMixin( + CesiumIonMixin(CatalogMemberMixin(CreateModel(CzmlCatalogItemTraits))) + ) ) ) implements TimeVarying, HasLocalData @@ -107,6 +110,8 @@ export default class CzmlCatalogItem loadableData = JSON.parse(this.czmlString); } else if (isDefined(this._czmlFile)) { loadableData = readJson(this._czmlFile); + } else if (isDefined(this.ionResource)) { + loadableData = this.ionResource; } else if (isDefined(this.url)) { loadableData = proxyCatalogItemUrl(this, this.url, this.cacheDuration); } @@ -145,7 +150,7 @@ export default class CzmlCatalogItem } protected forceLoadMetadata(): Promise { - return Promise.resolve(); + return this.loadIonResource(); } @computed diff --git a/lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem.ts b/lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem.ts index 0a3018ea7fb..08400ebf37c 100644 --- a/lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem.ts @@ -21,9 +21,10 @@ import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import ApiRequestTraits from "../../../Traits/TraitsClasses/ApiRequestTraits"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import { featureCollection, FeatureCollection } from "@turf/helpers"; +import CesiumIonMixin from "../../../ModelMixins/CesiumIonMixin"; class GeoJsonCatalogItem - extends GeoJsonMixin(CreateModel(GeoJsonCatalogItemTraits)) + extends CesiumIonMixin(GeoJsonMixin(CreateModel(GeoJsonCatalogItemTraits))) implements HasLocalData { static readonly type = "geojson"; @@ -90,6 +91,12 @@ class GeoJsonCatalogItem return undefined; } + protected override async forceLoadMetadata() { + const ionResourcePromise = this.loadIonResource(); + await super.forceLoadMetadata(); + await ionResourcePromise; + } + protected async forceLoadGeojsonData() { let jsonData: JsonValue | undefined = undefined; @@ -110,6 +117,8 @@ class GeoJsonCatalogItem } else { jsonData = await readJson(this._file); } + } else if (isDefined(this.ionResource)) { + jsonData = await loadJson(this.ionResource); } // We have multiple sources. else if (this.urls.length > 0) { diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts new file mode 100644 index 00000000000..f1399cee7ce --- /dev/null +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -0,0 +1,152 @@ +import i18next from "i18next"; +import { + computed, + makeObservable, + observable, + override, + runInAction, + toJS +} from "mobx"; +import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; +import isDefined from "../../../Core/isDefined"; +import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; +import CreateModel from "../../Definition/CreateModel"; +import { ModelConstructorParameters } from "../../Definition/Model"; +import MappableMixin from "../../../ModelMixins/MappableMixin"; +import UrlMixin from "../../../ModelMixins/UrlMixin"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; +import CatalogMemberMixin, { + getName +} from "../../../ModelMixins/CatalogMemberMixin"; +import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; +import Cesium3dTilesStyleMixin from "../../../ModelMixins/Cesium3dTilesStyleMixin"; +import ShadowMixin from "../../../ModelMixins/ShadowMixin"; +import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; +import FeatureInfoUrlTemplateMixin from "../../../ModelMixins/FeatureInfoUrlTemplateMixin"; +import I3SNode from "terriajs-cesium/Source/Scene/I3SNode"; +import TerriaFeature from "../../Feature/Feature"; + +export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( + FeatureInfoUrlTemplateMixin( + ShadowMixin( + MappableMixin( + UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) + ) + ) + ) +) { + static readonly type = "i3s"; + readonly type = I3SCatalogItem.type; + + @observable + private dataProvider?: I3SDataProvider; + + constructor(...args: ModelConstructorParameters) { + super(...args); + makeObservable(this); + } + + @computed + get boundingSphere() { + if (this.dataProvider?.layers) { + return BoundingSphere.fromBoundingSpheres( + this.dataProvider.layers + .map((layer) => layer.tileset?.boundingSphere) + .filter(isDefined) + ); + } + } + + async forceLoadMapItems() { + if (!isDefined(this.url)) { + throw `\`url\` is not defined for ${getName(this)}`; + } + const i3sProvider = await I3SDataProvider.fromUrl(this.url, { + showFeatures: this.allowFeaturePicking, + geoidTiledTerrainProvider: this.terrainURL + ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) + : undefined + }); + runInAction(() => { + this.dataProvider = i3sProvider; + }); + } + + /** + * This function should return null if allowFeaturePicking = false + * @param _screenPosition + * @param pickResult + */ + buildFeatureFromPickResult( + _screenPosition: Cartesian2 | undefined, + pickResult: any + ) { + if ( + this.allowFeaturePicking && + isDefined(pickResult.content) && + isDefined(pickResult.content.tile.i3sNode) && + isDefined(pickResult.featureId) && + _screenPosition + ) { + const i3sNode: I3SNode = pickResult.content.tile.i3sNode; + return i3sNode.loadFields().then(() => { + const fields = i3sNode.getFieldsForFeature(pickResult.featureId); + const result = new TerriaFeature({ + properties: fields + }); + result._cesium3DTileFeature = pickResult; + return result; + }); + } + return undefined; + } + + @computed + get mapItems() { + if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { + return []; + } + if (this.dataProvider.isDestroyed()) { + this.forceLoadMapItems(); + } + if (this.dataProvider) { + this.dataProvider.show = this.show; + + this.dataProvider.layers.forEach((layer) => { + const tileset = layer.tileset; + + if (tileset) { + tileset.style = toJS(this.cesiumTileStyle); + tileset.shadows = this.cesiumShadows; + // @ts-expect-error - Attach terria catalog item to tileset + tileset._catalogItem = this; + if (this.lightingFactor && tileset.imageBasedLighting) { + tileset.imageBasedLighting.imageBasedLightingFactor = + new Cartesian2(...this.lightingFactor); + } + + const key = this + .colorBlendMode as keyof typeof Cesium3DTileColorBlendMode; + const colorBlendMode = Cesium3DTileColorBlendMode[key]; + if (colorBlendMode !== undefined) + tileset.colorBlendMode = colorBlendMode; + tileset.colorBlendAmount = this.colorBlendAmount; + } + }); + } + return [this.dataProvider]; + } + + @override + get shortReport(): string | undefined { + if (this.terria.currentViewer.type === "Leaflet") { + return i18next.t("models.commonModelErrors.3dTypeIn2dMode", this); + } + return super.shortReport; + } + + get typeName() { + return i18next.t("core.dataType.i3s"); + } +} diff --git a/lib/Models/Catalog/CatalogItems/KmlCatalogItem.ts b/lib/Models/Catalog/CatalogItems/KmlCatalogItem.ts index 8d7cc0e0e30..13797597395 100644 --- a/lib/Models/Catalog/CatalogItems/KmlCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/KmlCatalogItem.ts @@ -21,12 +21,15 @@ import CreateModel from "../../Definition/CreateModel"; import HasLocalData from "../../HasLocalData"; import { ModelConstructorParameters } from "../../Definition/Model"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; +import CesiumIonMixin from "../../../ModelMixins/CesiumIonMixin"; const kmzRegex = /\.kmz$/i; class KmlCatalogItem extends MappableMixin( - UrlMixin(CatalogMemberMixin(CreateModel(KmlCatalogItemTraits))) + UrlMixin( + CesiumIonMixin(CatalogMemberMixin(CreateModel(KmlCatalogItemTraits))) + ) ) implements HasLocalData { @@ -63,28 +66,32 @@ class KmlCatalogItem } protected forceLoadMapItems(): Promise { - return new Promise((resolve) => { - if (isDefined(this.kmlString)) { - const parser = new DOMParser(); - resolve(parser.parseFromString(this.kmlString, "text/xml")); - } else if (isDefined(this._kmlFile)) { - if (this._kmlFile.name && this._kmlFile.name.match(kmzRegex)) { - resolve(this._kmlFile); + return new Promise( + (resolve) => { + if (isDefined(this.kmlString)) { + const parser = new DOMParser(); + resolve(parser.parseFromString(this.kmlString, "text/xml")); + } else if (isDefined(this._kmlFile)) { + if (this._kmlFile.name && this._kmlFile.name.match(kmzRegex)) { + resolve(this._kmlFile); + } else { + resolve(readXml(this._kmlFile)); + } + } else if (isDefined(this.ionResource)) { + resolve(this.ionResource); + } else if (isDefined(this.url)) { + resolve(proxyCatalogItemUrl(this, this.url)); } else { - resolve(readXml(this._kmlFile)); + throw networkRequestError({ + sender: this, + title: i18next.t("models.kml.unableToLoadItemTitle"), + message: i18next.t("models.kml.unableToLoadItemMessage") + }); } - } else if (isDefined(this.url)) { - resolve(proxyCatalogItemUrl(this, this.url)); - } else { - throw networkRequestError({ - sender: this, - title: i18next.t("models.kml.unableToLoadItemTitle"), - message: i18next.t("models.kml.unableToLoadItemMessage") - }); } - }) + ) .then((kmlLoadInput) => { - return KmlDataSource.load(kmlLoadInput); + return KmlDataSource.load(kmlLoadInput!); }) .then((dataSource) => { this._dataSource = dataSource; @@ -113,7 +120,7 @@ class KmlCatalogItem } protected forceLoadMetadata(): Promise { - return Promise.resolve(); + return this.loadIonResource(); } private doneLoading(kmlDataSource: KmlDataSource) { diff --git a/lib/Models/Catalog/CatalogItems/OpenDataSoftCatalogItem.ts b/lib/Models/Catalog/CatalogItems/OpenDataSoftCatalogItem.ts index eb896ffdd8c..6951a5f0d61 100644 --- a/lib/Models/Catalog/CatalogItems/OpenDataSoftCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/OpenDataSoftCatalogItem.ts @@ -394,7 +394,7 @@ export class OpenDataSoftDatasetStratum extends LoadableStratum( // Group all time intervals for each row group (each Point feature) // This calculates the start/stop dates for each row group const groupIntervals = this.catalogItem.activeTableStyle.rowGroups.map( - ([id, rows]) => { + ([_id, rows]) => { let start: JulianDate | undefined; let stop: JulianDate | undefined; diff --git a/lib/Models/Catalog/CatalogReferences/CatalogIndexReference.ts b/lib/Models/Catalog/CatalogReferences/CatalogIndexReference.ts index ba88d1a821b..d3921fb2d60 100644 --- a/lib/Models/Catalog/CatalogReferences/CatalogIndexReference.ts +++ b/lib/Models/Catalog/CatalogReferences/CatalogIndexReference.ts @@ -27,7 +27,7 @@ export default class CatalogIndexReference extends ReferenceMixin( } protected async forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { if (this.uniqueId === undefined) { return; diff --git a/lib/Models/Catalog/CatalogReferences/SplitItemReference.ts b/lib/Models/Catalog/CatalogReferences/SplitItemReference.ts index bc122e37e80..da3c672cfcb 100644 --- a/lib/Models/Catalog/CatalogReferences/SplitItemReference.ts +++ b/lib/Models/Catalog/CatalogReferences/SplitItemReference.ts @@ -24,7 +24,7 @@ export default class SplitItemReference extends ReferenceMixin( } protected async forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { if (this.splitSourceItemId === undefined || this.uniqueId === undefined) { throw new TerriaError({ diff --git a/lib/Models/Catalog/CatalogReferences/ThreddsItemReference.ts b/lib/Models/Catalog/CatalogReferences/ThreddsItemReference.ts index 08f0a71685c..34f86a1847d 100644 --- a/lib/Models/Catalog/CatalogReferences/ThreddsItemReference.ts +++ b/lib/Models/Catalog/CatalogReferences/ThreddsItemReference.ts @@ -28,7 +28,7 @@ export class ThreddsDatasetStratum extends LoadableStratum( super(); } - duplicateLoadableStratum(newModel: BaseModel): this { + duplicateLoadableStratum(_newModel: BaseModel): this { return new ThreddsDatasetStratum( this.threddsItemReference, this.threddsDataset @@ -109,7 +109,7 @@ export default class ThreddsItemReference extends UrlMixin( } async forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { this.setThreddsStrata(this); @@ -121,7 +121,7 @@ export default class ThreddsItemReference extends UrlMixin( ); if (model === undefined) return; this.setThreddsStrata(model); - previousTarget = model; + _previousTarget = model; return model; } } diff --git a/lib/Models/Catalog/CatalogReferences/UrlReference.ts b/lib/Models/Catalog/CatalogReferences/UrlReference.ts index f63c7388e3b..4fdf4c61340 100644 --- a/lib/Models/Catalog/CatalogReferences/UrlReference.ts +++ b/lib/Models/Catalog/CatalogReferences/UrlReference.ts @@ -33,7 +33,7 @@ export default class UrlReference extends UrlMixin( } protected forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { if (this.url === undefined || this.uniqueId === undefined) { return Promise.resolve(undefined); diff --git a/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts b/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts index 251d85d2586..813f284ad8c 100644 --- a/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts +++ b/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts @@ -117,8 +117,6 @@ export class CkanServerStratum extends LoadableStratum(CkanCatalogGroupTraits) { static async load( catalogGroup: CkanCatalogGroup ): Promise { - const terria = catalogGroup.terria; - let ckanServerResponse: CkanServerResponse | undefined = undefined; // Each item in the array causes an independent request to the CKAN, and the results are concatenated diff --git a/lib/Models/Catalog/Ckan/CkanDefaultFormatsStratum.ts b/lib/Models/Catalog/Ckan/CkanDefaultFormatsStratum.ts index af3ad40932c..6361cedf060 100644 --- a/lib/Models/Catalog/Ckan/CkanDefaultFormatsStratum.ts +++ b/lib/Models/Catalog/Ckan/CkanDefaultFormatsStratum.ts @@ -10,7 +10,7 @@ export default class CkanDefaultFormatsStratum extends LoadableStratum( ) { static stratumName = "ckanDefaultFormats"; - duplicateLoadableStratum(newModel: BaseModel): this { + duplicateLoadableStratum(_newModel: BaseModel): this { return new CkanDefaultFormatsStratum() as this; } diff --git a/lib/Models/Catalog/Ckan/CkanItemReference.ts b/lib/Models/Catalog/Ckan/CkanItemReference.ts index e8d150b198d..229a05a81c0 100644 --- a/lib/Models/Catalog/Ckan/CkanItemReference.ts +++ b/lib/Models/Catalog/Ckan/CkanItemReference.ts @@ -50,7 +50,7 @@ export class CkanDatasetStratum extends LoadableStratum( makeObservable(this); } - duplicateLoadableStratum(newModel: BaseModel): this { + duplicateLoadableStratum(_newModel: BaseModel): this { return new CkanDatasetStratum( this.ckanItemReference, this.ckanCatalogGroup @@ -348,7 +348,7 @@ export default class CkanItemReference extends UrlMixin( } async forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { await this.setCkanStrata(this); @@ -388,7 +388,7 @@ export default class CkanItemReference extends UrlMixin( } if (model === undefined) return; - previousTarget = model; + _previousTarget = model; await this.setCkanStrata(model); model.setTrait(CommonStrata.definition, "name", this.name); @@ -520,7 +520,6 @@ export function isResourceInSupportedFormats( formats: PreparedSupportedFormat[] ): PreparedSupportedFormat | undefined { if (resource === undefined) return undefined; - const matches: PreparedSupportedFormat[] = []; for (let i = 0; i < formats.length; ++i) { const format = formats[i]; if (resourceIsSupported(resource, format)) return format; diff --git a/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts index 08f43394f06..ab01c79185b 100644 --- a/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts @@ -25,11 +25,6 @@ import ArcGisMapServerCatalogGroup, { MapServerStratum } from "./ArcGisMapServerCatalogGroup"; -interface DocumentInfo { - Title?: string; - Author?: string; -} - interface Service { name: string; type: string; @@ -68,7 +63,6 @@ class ArcGisServerStratum extends LoadableStratum(ArcGisCatalogGroupTraits) { static async load( catalogGroup: ArcGisCatalogGroup ): Promise { - const terria = catalogGroup.terria; const uri = new URI(catalogGroup.url).addQuery("f", "json"); return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString())) .then((arcgisServer: ArcGisServer) => { diff --git a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts index fe8f731e409..89023f689ce 100644 --- a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts @@ -120,7 +120,6 @@ export class FeatureServerStratum extends LoadableStratum( static async load( catalogGroup: ArcGisFeatureServerCatalogGroup | ArcGisCatalogGroup ): Promise { - const terria = catalogGroup.terria; const uri = new URI(catalogGroup.url).addQuery("f", "json"); return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString())) diff --git a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem.ts b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem.ts index c01282c3eb5..e3adac2009b 100644 --- a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem.ts +++ b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem.ts @@ -201,16 +201,11 @@ class FeatureServerStratum extends LoadableStratum( item: ArcGisFeatureServerCatalogItem ): Promise { 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 diff --git a/lib/Models/Catalog/Esri/ArcGisImageServerCatalogItem.ts b/lib/Models/Catalog/Esri/ArcGisImageServerCatalogItem.ts new file mode 100644 index 00000000000..2bdb0841d4f --- /dev/null +++ b/lib/Models/Catalog/Esri/ArcGisImageServerCatalogItem.ts @@ -0,0 +1,615 @@ +import i18next from "i18next"; +import uniqWith from "lodash-es/uniqWith"; +import { computed, makeObservable, override, runInAction } from "mobx"; +import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme"; +import URI from "urijs"; +import AsyncLoader from "../../../Core/AsyncLoader"; +import { JsonObject, isJsonObject } from "../../../Core/Json"; +import TerriaError from "../../../Core/TerriaError"; +import createDiscreteTimesFromIsoSegments from "../../../Core/createDiscreteTimes"; +import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined"; +import filterOutUndefined from "../../../Core/filterOutUndefined"; +import isDefined from "../../../Core/isDefined"; +import loadJson from "../../../Core/loadJson"; +import replaceUnderscores from "../../../Core/replaceUnderscores"; +import { scaleDenominatorToLevel } from "../../../Core/scaleToDenominator"; +import ArcGisImageServerImageryProvider from "../../../Map/ImageryProvider/ArcGisImageServerImageryProvider"; +import Reproject from "../../../Map/Vector/Reproject"; +import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin"; +import DiscretelyTimeVaryingMixin from "../../../ModelMixins/DiscretelyTimeVaryingMixin"; +import MappableMixin, { + ImageryParts +} from "../../../ModelMixins/MappableMixin"; +import MinMaxLevelMixin from "../../../ModelMixins/MinMaxLevelMixin"; +import UrlMixin from "../../../ModelMixins/UrlMixin"; +import ArcGisImageServerCatalogItemTraits, { + ArcGisImageServerAvailableRasterFunctionTraits, + ArcGisImageServerRenderingRule +} from "../../../Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits"; +import DiscreteTimeTraits from "../../../Traits/TraitsClasses/DiscreteTimeTraits"; +import LegendTraits, { + LegendItemTraits +} from "../../../Traits/TraitsClasses/LegendTraits"; +import CreateModel from "../../Definition/CreateModel"; +import LoadableStratum from "../../Definition/LoadableStratum"; +import { BaseModel, ModelConstructorParameters } from "../../Definition/Model"; +import StratumFromTraits from "../../Definition/StratumFromTraits"; +import StratumOrder from "../../Definition/StratumOrder"; +import createStratumInstance from "../../Definition/createStratumInstance"; +import { RectangleCoordinates } from "../../FunctionParameters/RectangleParameter"; +import { SelectableDimensionEnum } from "../../SelectableDimensions/SelectableDimensions"; +import getToken from "../../getToken"; +import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; +import { ImageServer, Legends } from "./ArcGisInterfaces"; +import { getRectangleFromLayer } from "./ArcGisMapServerCatalogItem"; + +class ImageServerStratum extends LoadableStratum( + ArcGisImageServerCatalogItemTraits +) { + static stratumName = "arcgisImageserver"; + + constructor( + private readonly _item: ArcGisImageServerCatalogItem, + readonly imageServer: ImageServer, + private readonly _token: string | undefined + ) { + super(); + makeObservable(this); + } + + duplicateLoadableStratum(newModel: BaseModel): this { + return new ImageServerStratum( + newModel as ArcGisImageServerCatalogItem, + this.imageServer, + this._token + ) as this; + } + + static async load(item: ArcGisImageServerCatalogItem) { + if (!isDefined(item.uri)) { + throw new TerriaError({ + title: i18next.t("models.arcGisImageServerCatalogItem.invalidUrlTitle"), + message: i18next.t( + "models.arcGisImageServerCatalogItem.invalidUrlMessage" + ) + }); + } + + let token: string | undefined; + if (isDefined(item.tokenUrl) && isDefined(item.url)) { + token = await getToken(item.terria, item.tokenUrl, item.url); + } + + let serviceUri = getBaseURI(item); + + if (isDefined(token)) { + serviceUri = serviceUri.addQuery("token", token); + } + + const serviceMetadata: ImageServer | undefined = await getJson( + item, + serviceUri + ); + + if (!isDefined(serviceMetadata)) { + throw new TerriaError({ + title: i18next.t("models.arcGisService.invalidServerTitle"), + message: i18next.t("models.arcGisService.invalidServerMessage") + }); + } + + if (!serviceMetadata.capabilities?.includes("Image")) + throw new TerriaError({ + title: i18next.t( + "models.arcGisImageServerCatalogItem.invalidServiceTitle" + ), + message: i18next.t( + "models.arcGisImageServerCatalogItem.invalidServiceMessage" + ) + }); + + // Add any Proj4 definitions if necessary + const epsgCode = + serviceMetadata.fullExtent.spatialReference?.latestWkid ?? + serviceMetadata.fullExtent.spatialReference?.wkid; + if (epsgCode && item.terria.configParameters.proj4ServiceBaseUrl) { + await Reproject.checkProjection( + item.terria.configParameters.proj4ServiceBaseUrl, + `EPSG:${epsgCode}` + ); + } + + const stratum = new ImageServerStratum(item, serviceMetadata, token); + + return stratum; + } + + get name() { + return replaceUnderscores(this.imageServer.name); + } + + get cacheDuration(): string { + return "1d"; + } + + get rectangle() { + const rectangle: RectangleCoordinates = { + west: Infinity, + south: Infinity, + east: -Infinity, + north: -Infinity + }; + + getRectangleFromLayer(this.imageServer.fullExtent, rectangle); + + if ( + rectangle.west === Infinity || + rectangle.south === Infinity || + rectangle.east === -Infinity || + rectangle.north === -Infinity + ) + return undefined; + + return rectangle; + } + + get description() { + return this.imageServer.description; + } + + get attribution() { + return this.imageServer.copyrightText; + } + + get token() { + return this._token; + } + + /** Disable pre-cached tiles if we are making dynamic requests - using parameters, time or rendering rule. */ + @computed get usePreCachedTiles() { + if ( + this._item.parameters || + this._item.currentDiscreteJulianDate || + this._item.renderingRule.rasterFunction + ) + return false; + + // We only support web mercator or wgs84 + if (isDefined(this.imageServer.tileInfo)) { + const wkid = + this.imageServer.tileInfo.spatialReference?.latestWkid ?? + this.imageServer.tileInfo.spatialReference?.wkid; + + if (wkid === 102100 || wkid === 102113 || wkid === 3857 || wkid === 4326) + return true; + } + + return false; + } + + /** Override wkid to web mercator if using pre-cached tiles (and web mercator is supported) */ + get wkid() { + if (this._item.usePreCachedTiles) { + const wkid = this.imageServer.tileInfo?.spatialReference.wkid; + if (wkid === 102100 || wkid === 102113 || wkid === 3857 || wkid === 4326) + return wkid; + } + } + + get tileHeight() { + if (this._item.usePreCachedTiles) return this.imageServer.tileInfo?.rows; + } + + get tileWidth() { + if (this._item.usePreCachedTiles) return this.imageServer.tileInfo?.cols; + } + + get maximumScale() { + return this.imageServer.maxScale; + } + + get maximumLevel() { + const maximumLevelFromScale = scaleDenominatorToLevel( + this._item.maximumScale, + true, + false + ); + + // Make sure the maximum level is not higher than the maximum level of the server tiles + if (this._item.usePreCachedTiles && this.imageServer.tileInfo?.lods) + return Math.min( + this.imageServer.tileInfo.lods[ + this.imageServer.tileInfo.lods.length - 1 + ].level, + maximumLevelFromScale ?? Infinity + ); + + return maximumLevelFromScale; + } + + get minimumLevel() { + if (this._item.usePreCachedTiles && this.imageServer.tileInfo?.lods) + return this.imageServer.tileInfo.lods[0].level; + } + + get allowRasterFunction() { + return this.imageServer.allowRasterFunction; + } + + get availableRasterFunctions() { + if (!this._item.allowRasterFunction) return []; + + return this.imageServer?.rasterFunctionInfos + .filter((rasterFn) => rasterFn.name && rasterFn.name !== "None") + .map((rasterFn) => { + return createStratumInstance( + ArcGisImageServerAvailableRasterFunctionTraits, + { + name: rasterFn.name, + description: + rasterFn.description !== "A raster function template." + ? rasterFn.description + : undefined, + help: rasterFn.help + } + ); + }); + } + + get disableRasterFunctionSelectors() { + return !this._item.allowRasterFunction; + } +} + +StratumOrder.addLoadStratum(ImageServerStratum.stratumName); + +class ImageServerLegendStratum extends LoadableStratum( + ArcGisImageServerCatalogItemTraits +) { + static stratumName = "arcgisImageserverLegend"; + + constructor( + private readonly _item: ArcGisImageServerCatalogItem, + private readonly _legends: Legends | undefined + ) { + super(); + makeObservable(this); + } + + duplicateLoadableStratum(newModel: BaseModel): this { + return new ImageServerLegendStratum( + newModel as ArcGisImageServerCatalogItem, + this._legends + ) as this; + } + + static async load(item: ArcGisImageServerCatalogItem) { + if (!isDefined(item.uri)) { + throw new TerriaError({ + title: i18next.t("models.arcGisImageServerCatalogItem.invalidUrlTitle"), + message: i18next.t( + "models.arcGisImageServerCatalogItem.invalidUrlMessage" + ) + }); + } + + let legendUri = getBaseURI(item).segment("legend"); + + legendUri.addQuery(item.flattenedParameters); + + if (isDefined(item.token)) { + legendUri = legendUri.addQuery("token", item.token); + } + + const legendMetadata: Legends | undefined = await getJson(item, legendUri); + + const stratum = new ImageServerLegendStratum(item, legendMetadata); + + return stratum; + } + + @computed get legends() { + const noDataRegex = /^No[\s_-]?Data$/i; + const labelsRegex = /_Labels$/; + + let items: StratumFromTraits[] = []; + + (this._legends?.layers || []).forEach((l) => { + if (noDataRegex.test(l.layerName) || labelsRegex.test(l.layerName)) { + return; + } + + l.legend?.forEach((leg) => { + const title = replaceUnderscores( + leg.label !== "" ? leg.label : l.layerName + ); + const dataUrl = "data:" + leg.contentType + ";base64," + leg.imageData; + items.push( + createStratumInstance(LegendItemTraits, { + title, + imageUrl: dataUrl, + imageWidth: leg.width, + imageHeight: leg.height + }) + ); + }); + }); + + items = uniqWith(items, (a, b) => a.imageUrl === b.imageUrl); + + return [createStratumInstance(LegendTraits, { items })]; + } +} + +StratumOrder.addLoadStratum(ImageServerLegendStratum.stratumName); + +export default class ArcGisImageServerCatalogItem extends UrlMixin( + DiscretelyTimeVaryingMixin( + MinMaxLevelMixin( + CatalogMemberMixin( + MappableMixin(CreateModel(ArcGisImageServerCatalogItemTraits)) + ) + ) + ) +) { + static readonly type = "esri-imageServer"; + + private _legendStratumLoader = new AsyncLoader( + this.forceLoadLegends.bind(this) + ); + + constructor(...args: ModelConstructorParameters) { + super(...args); + makeObservable(this); + } + + get typeName() { + return i18next.t("models.arcGisImageServerCatalogItem.name"); + } + + get type() { + return ArcGisImageServerCatalogItem.type; + } + + protected async forceLoadMetadata(): Promise { + const stratum = await ImageServerStratum.load(this); + runInAction(() => { + this.strata.set(ImageServerStratum.stratumName, stratum); + }); + await this._legendStratumLoader.load(); + } + + protected async forceLoadLegends(): Promise { + const stratum = await ImageServerLegendStratum.load(this); + runInAction(() => { + this.strata.set(ImageServerLegendStratum.stratumName, stratum); + }); + } + + protected forceLoadMapItems(): Promise { + return Promise.resolve(); + } + + @computed + get discreteTimes() { + const imageServerStratum: ImageServerStratum | undefined = this.strata.get( + ImageServerStratum.stratumName + ) as ImageServerStratum | undefined; + + if (imageServerStratum?.imageServer.timeInfo === undefined) + return undefined; + + const result: (StratumFromTraits & { + time: string; + })[] = []; + + createDiscreteTimesFromIsoSegments( + result, + new Date( + imageServerStratum.imageServer.timeInfo.timeExtent[0] + ).toISOString(), + new Date( + imageServerStratum.imageServer.timeInfo.timeExtent[1] + ).toISOString(), + undefined, + this.maxRefreshIntervals + ); + return result; + } + + @computed + private get _currentImageryParts(): ImageryParts | undefined { + // Make sure legend stays up to date + this._legendStratumLoader.load(); + + const imageryProvider = this._createImageryProvider( + this.currentDiscreteJulianDate + ); + + if (imageryProvider) { + return { + imageryProvider, + alpha: this.opacity, + show: this.show, + clippingRectangle: this.clipToRectangle + ? this.cesiumRectangle + : undefined + }; + } + } + + @computed + get _nextImageryParts(): ImageryParts | undefined { + if ( + this.terria.timelineStack.contains(this) && + !this.isPaused && + this.nextDiscreteTimeTag + ) { + const imageryProvider = this._createImageryProvider( + this.nextDiscreteJulianDate + ); + + if (imageryProvider) { + imageryProvider.enablePickFeatures = false; + return { + imageryProvider, + alpha: 0.0, + show: true, + clippingRectangle: this.clipToRectangle + ? this.cesiumRectangle + : undefined + }; + } + } else { + return undefined; + } + } + + /** Flatten nested JSON parameters (stringify them). These are applied onto ArcGisImageServerImageryProvider requests. + * This will include renderingRule and bandIds if they are set. + */ + @computed + get flattenedParameters() { + const params = Object.entries(this.parameters ?? {}).reduce( + (acc, [key, value]) => { + if (isJsonObject(value)) acc[key] = JSON.stringify(value); + else acc[key] = value; + return acc; + }, + {} + ); + + if (this.renderingRule.rasterFunction) + params.renderingRule = JSON.stringify( + this.traits["renderingRule"].toJson(this.renderingRule) + ); + + if (this.bandIds) params.bandIds = this.bandIds.join(","); + + return params; + } + + private _createImageryProvider = createTransformerAllowUndefined( + ( + time: JulianDate | undefined + ): ArcGisImageServerImageryProvider | undefined => { + if (!isDefined(this.url)) { + return undefined; + } + + const params = { ...this.flattenedParameters }; + if (time) params.time = JulianDate.toDate(time).getTime(); + + let tilingScheme: WebMercatorTilingScheme | GeographicTilingScheme; + + if (this.wkid === 102100 || this.wkid === 102113 || this.wkid === 3857) { + tilingScheme = new WebMercatorTilingScheme(); + } else if (this.wkid === 4326) { + tilingScheme = new GeographicTilingScheme(); + } else { + throw TerriaError.from( + `Tile spatial reference WKID ${this.wkid} is not supported.` + ); + } + + return new ArcGisImageServerImageryProvider({ + url: cleanAndProxyUrl(this, this.url), + tilingScheme: tilingScheme, + maximumLevel: this.maximumLevel, + minimumLevel: this.minimumLevel, + tileHeight: this.tileHeight, + tileWidth: this.tileWidth, + parameters: params, + enablePickFeatures: this.allowFeaturePicking, + usePreCachedTiles: this.usePreCachedTiles, + token: this.token, + credit: this.attribution ?? "" + }); + } + ); + + @computed + get mapItems() { + if (this.isLoadingMetadata) return []; + return [this._currentImageryParts, this._nextImageryParts].filter( + isDefined + ); + } + + @override + get selectableDimensions() { + return filterOutUndefined([ + ...super.selectableDimensions, + this.rasterFunctionSelectableDimensions + ]); + } + + @computed + get rasterFunctionSelectableDimensions(): + | SelectableDimensionEnum + | undefined { + if (this.disableRasterFunctionSelectors) return undefined; + return { + id: "raster-functions", + name: i18next.t("models.arcGisImageServerCatalogItem.rasterFunction"), + options: this.availableRasterFunctions.map((rasterFn) => ({ + id: rasterFn.name, + name: rasterFn.name, + description: rasterFn.description + })), + selectedId: this.renderingRule?.rasterFunction, + allowUndefined: true, + undefinedLabel: "Default", + setDimensionValue: (strata, rasterFunction) => { + if (!this.renderingRule) { + this.setTrait( + strata, + "renderingRule", + createStratumInstance(ArcGisImageServerRenderingRule, { + rasterFunction + }) + ); + } else { + this.renderingRule.setTrait(strata, "rasterFunction", rasterFunction); + } + } + }; + } +} + +function getBaseURI(item: ArcGisImageServerCatalogItem) { + const uri = new URI(item.url); + const lastSegment = uri.segment(-1); + if (lastSegment && lastSegment.match(/\d+/)) { + uri.segment(-1, ""); + } + return uri; +} + +async function getJson(item: ArcGisImageServerCatalogItem, uri: uri.URI) { + try { + const response = await loadJson( + proxyCatalogItemUrl(item, uri.addQuery("f", "json").toString()) + ); + return response; + } catch (err) { + console.log(err); + return undefined; + } +} + +function cleanAndProxyUrl( + catalogItem: ArcGisImageServerCatalogItem, + url: string +) { + return proxyCatalogItemUrl(catalogItem, cleanUrl(url)); +} + +function cleanUrl(url: string) { + // Strip off the search portion of the URL + const uri = new URI(url); + uri.search(""); + return uri.toString(); +} diff --git a/lib/Models/Catalog/Esri/ArcGisInterfaces.ts b/lib/Models/Catalog/Esri/ArcGisInterfaces.ts index 88f9d1b91ab..ac3faa7b81f 100644 --- a/lib/Models/Catalog/Esri/ArcGisInterfaces.ts +++ b/lib/Models/Catalog/Esri/ArcGisInterfaces.ts @@ -1,3 +1,5 @@ +import { JsonObject } from "../../../Core/Json"; + interface DocumentInfo { Title?: string; Author?: string; @@ -20,6 +22,40 @@ interface TimeInfo { timeExtent: [number, number]; } +interface TileInfo { + rows: number; + cols: number; + dpi: number; + format: string; + compressionQuality?: number; + origin: { + x: number; + y: number; + }; + storageInfo?: { + storageFormat: string; + packetSize: number; + }; + spatialReference: SpatialReference; + lods: { + level: number; + resolution: number; + scale: number; + }[]; +} + +export interface Legend { + label?: string; + contentType: string; + imageData: string; + width: number; + height: number; +} + +export interface Legends { + layers?: { layerId: number; layerName: string; legend?: Legend[] }[]; +} + export interface Layer { id: number; name?: string; @@ -46,7 +82,7 @@ export interface MapServer { * So instead we create a single item in the group called "All layers" (models.arcGisMapServerCatalogGroup.singleFusedMapCacheLayerName) */ singleFusedMapCache?: boolean; - tileInfo?: unknown; + tileInfo?: TileInfo; //comma separated list of supported capabilities - e.g. "Map,Query,Data,TilesOnly,Tilemap" capabilities?: string; mapName?: string; @@ -54,3 +90,58 @@ export interface MapServer { fullExtent: Extent; maxScale?: number; } + +export interface ImageServer { + name?: string; + serviceDescription?: string; + description?: string; + copyrightText?: string; + timeInfo?: TimeInfo; + fullExtent: Extent; + + singleFusedMapCache: boolean; //Indicates the existence of tile resource + tileInfo?: TileInfo; + //comma separated list of supported capabilities - e.g. "Image,Metadata..." + capabilities?: string; + spatialReference?: SpatialReference; + + maxScale?: number; + minScale?: number; + + allowRasterFunction: boolean; //Indicates whether the service allows raster functions in request + rasterFunctionInfos: [ + //Optional. Specifies the supported raster function templates the client can invoke. The first one is applied to exportImage request by default + { + name: string; + description: string; + help: string; + } + ]; + + hasColormap: boolean; + hasMultidimensions: boolean; + + bandNames?: string[]; + bandCount?: number; +} + +export interface ImageServerIdentifyResult { + objectId: number; + name: string; + /** CSV of pixel values per band */ + value: string; + location: { + x: number; + y: number; + spatialReference: SpatialReference; + }; + properties: JsonObject | null; + /** catalogItems are returned only when the image service source is a mosaic dataset */ + catalogItems?: null | { + objectIdFieldName: string; + spatialReference: SpatialReference; + geometryType: string; + features: unknown[]; + }; + catalogItemVisibilities?: number[]; +} diff --git a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts index b91a6c9003f..7eedbc52d98 100644 --- a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts +++ b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts @@ -1,11 +1,12 @@ import i18next from "i18next"; import uniqWith from "lodash-es/uniqWith"; import { computed, makeObservable, override, runInAction } from "mobx"; -import { fromPromise, IPromiseBasedObservable } from "mobx-utils"; +import { IPromiseBasedObservable, fromPromise } from "mobx-utils"; import moment from "moment"; import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme"; import ArcGisMapServerImageryProvider from "terriajs-cesium/Source/Scene/ArcGisMapServerImageryProvider"; import URI from "urijs"; +import TerriaError, { networkRequestError } from "../../../Core/TerriaError"; import createDiscreteTimesFromIsoSegments from "../../../Core/createDiscreteTimes"; import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined"; import filterOutUndefined from "../../../Core/filterOutUndefined"; @@ -14,8 +15,8 @@ import loadJson from "../../../Core/loadJson"; import replaceUnderscores from "../../../Core/replaceUnderscores"; import { scaleDenominatorToLevel } from "../../../Core/scaleToDenominator"; import { setsAreEqual } from "../../../Core/setsAreEqual"; -import TerriaError, { networkRequestError } from "../../../Core/TerriaError"; import Proj4Definitions from "../../../Map/Vector/Proj4Definitions"; +import Reproject from "../../../Map/Vector/Reproject"; import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin"; import DiscretelyTimeVaryingMixin from "../../../ModelMixins/DiscretelyTimeVaryingMixin"; import MappableMixin, { @@ -30,37 +31,19 @@ import LegendTraits, { } from "../../../Traits/TraitsClasses/LegendTraits"; import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits"; import CreateModel from "../../Definition/CreateModel"; -import createStratumInstance from "../../Definition/createStratumInstance"; import LoadableStratum from "../../Definition/LoadableStratum"; import { BaseModel, ModelConstructorParameters } from "../../Definition/Model"; import StratumFromTraits from "../../Definition/StratumFromTraits"; import StratumOrder from "../../Definition/StratumOrder"; +import createStratumInstance from "../../Definition/createStratumInstance"; +import { RectangleCoordinates } from "../../FunctionParameters/RectangleParameter"; import getToken from "../../getToken"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import MinMaxLevelMixin from "./../../../ModelMixins/MinMaxLevelMixin"; -import { Extent, Layer, MapServer } from "./ArcGisInterfaces"; +import { Extent, Layer, Legends, MapServer } from "./ArcGisInterfaces"; const proj4 = require("proj4").default; -interface RectangleExtent { - east: number; - south: number; - west: number; - north: number; -} - -interface Legend { - label?: string; - contentType: string; - imageData: string; - width: number; - height: number; -} - -interface Legends { - layers?: { layerId: number; layerName: string; legend?: Legend[] }[]; -} - class MapServerStratum extends LoadableStratum( ArcGisMapServerCatalogItemTraits ) { @@ -88,7 +71,7 @@ class MapServerStratum extends LoadableStratum( } static async load(item: ArcGisMapServerCatalogItem) { - if (!isDefined(item.uri)) { + if (!isDefined(item.uri) || !isDefined(item.url)) { throw new TerriaError({ title: i18next.t("models.arcGisMapServerCatalogItem.invalidUrlTitle"), message: i18next.t( @@ -168,6 +151,18 @@ class MapServerStratum extends LoadableStratum( legendMetadata, token ); + + // Add any Proj4 definitions if necessary + const epsgCode = + serviceMetadata.fullExtent.spatialReference?.latestWkid ?? + serviceMetadata.fullExtent.spatialReference?.wkid; + if (epsgCode && item.terria.configParameters.proj4ServiceBaseUrl) { + await Reproject.checkProjection( + item.terria.configParameters.proj4ServiceBaseUrl, + `EPSG:${epsgCode}` + ); + } + return stratum; } @@ -228,7 +223,7 @@ class MapServerStratum extends LoadableStratum( } @computed get rectangle() { - const rectangle: RectangleExtent = { + const rectangle: RectangleCoordinates = { west: Infinity, south: Infinity, east: -Infinity, @@ -681,14 +676,17 @@ function findLayers(layers: Layer[], names: string | undefined) { }); } -function updateBbox(extent: Extent, rectangle: RectangleExtent) { +function updateBbox(extent: Extent, rectangle: RectangleCoordinates) { if (extent.xmin < rectangle.west) rectangle.west = extent.xmin; if (extent.ymin < rectangle.south) rectangle.south = extent.ymin; if (extent.xmax > rectangle.east) rectangle.east = extent.xmax; if (extent.ymax > rectangle.north) rectangle.north = extent.ymax; } -function getRectangleFromLayer(extent: Extent, rectangle: RectangleExtent) { +export function getRectangleFromLayer( + extent: Extent, + rectangle: RectangleCoordinates +) { const wkidCode = extent?.spatialReference?.latestWkid ?? extent?.spatialReference?.wkid; @@ -700,6 +698,7 @@ function getRectangleFromLayer(extent: Extent, rectangle: RectangleExtent) { const wkid = "EPSG:" + wkidCode; if (!isDefined(Proj4Definitions[wkid])) { + console.warn("No Proj4 definition for " + wkid); return; } @@ -723,7 +722,10 @@ function getRectangleFromLayer(extent: Extent, rectangle: RectangleExtent) { } } -function getRectangleFromLayers(rectangle: RectangleExtent, layers: Layer[]) { +function getRectangleFromLayers( + rectangle: RectangleCoordinates, + layers: Layer[] +) { layers.forEach(function (item) { item.extent && getRectangleFromLayer(item.extent, rectangle); }); diff --git a/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts index 08857cfe2f5..6ecade7de18 100644 --- a/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts @@ -60,8 +60,6 @@ export class ArcGisPortalStratum extends LoadableStratum( static async load( catalogGroup: ArcGisPortalCatalogGroup ): Promise { - const terria = catalogGroup.terria; - let portalGroupsServerResponse: | ArcGisPortalGroupSearchResponse | undefined = undefined; diff --git a/lib/Models/Catalog/Esri/ArcGisPortalItemReference.ts b/lib/Models/Catalog/Esri/ArcGisPortalItemReference.ts index 0232f355aa9..2f156699b75 100644 --- a/lib/Models/Catalog/Esri/ArcGisPortalItemReference.ts +++ b/lib/Models/Catalog/Esri/ArcGisPortalItemReference.ts @@ -42,7 +42,7 @@ export class ArcGisPortalItemStratum extends LoadableStratum( makeObservable(this); } - duplicateLoadableStratum(newModel: BaseModel): this { + duplicateLoadableStratum(_newModel: BaseModel): this { return new ArcGisPortalItemStratum( this.arcgisPortalItemReference, this.arcgisPortalCatalogGroup @@ -309,7 +309,7 @@ export default class ArcGisPortalItemReference extends AccessControlMixin( } async forceLoadReference( - previousTarget: BaseModel | undefined + _previousTarget: BaseModel | undefined ): Promise { // So when we first crawl we'll get this far await this.setArcgisStrata(this); @@ -350,7 +350,7 @@ export default class ArcGisPortalItemReference extends AccessControlMixin( ); if (model === undefined) return; - previousTarget = model; + _previousTarget = model; await this.setArcgisStrata(model); const defintionStratum = this.strata.get(CommonStrata.definition); @@ -372,11 +372,6 @@ interface ArcGisItemInfo { error?: JsonObject; } -interface ArcGisItemWithFormat { - supportedFormat: PreparedSupportedFormat; - arcgisItem: ArcGisItem; -} - interface PreparedSupportedFormat { formatRegex: RegExp | undefined; urlRegex: RegExp | undefined; diff --git a/lib/Models/Catalog/Gltf/AssImpCatalogItem.ts b/lib/Models/Catalog/Gltf/AssImpCatalogItem.ts index b593bed3a85..3b9f1886c87 100644 --- a/lib/Models/Catalog/Gltf/AssImpCatalogItem.ts +++ b/lib/Models/Catalog/Gltf/AssImpCatalogItem.ts @@ -13,6 +13,7 @@ import HasLocalData from "../../HasLocalData"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import { ModelConstructorParameters } from "../../Definition/Model"; import { GlTf } from "./GLTF"; +import Resource from "terriajs-cesium/Source/Core/Resource"; // List of supported image formats from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types // + Cesium adds support for ktx2 @@ -36,7 +37,7 @@ export default class AssImpCatalogItem implements HasLocalData { @observable - protected gltfModelUrl: string | undefined; + protected gltfModelUrl: string | Resource | undefined; static readonly type = "assimp"; @@ -178,7 +179,7 @@ export default class AssImpCatalogItem } /** This is used so we only set `this.gltfModelUrl` after process has finished */ - let gltfModelUrl: string | undefined; + let gltfModelUrl: string | Resource | undefined; /** List of unsupported texture URLs to show in warning message */ const unsupportedTextures: string[] = []; diff --git a/lib/Models/Catalog/Gltf/GltfCatalogItem.ts b/lib/Models/Catalog/Gltf/GltfCatalogItem.ts index 3722fd3b0f2..e72c4ca0792 100644 --- a/lib/Models/Catalog/Gltf/GltfCatalogItem.ts +++ b/lib/Models/Catalog/Gltf/GltfCatalogItem.ts @@ -6,9 +6,12 @@ import CommonStrata from "../../Definition/CommonStrata"; import CreateModel from "../../Definition/CreateModel"; import { ModelConstructorParameters } from "../../Definition/Model"; import HasLocalData from "../../HasLocalData"; +import CesiumIonMixin from "../../../ModelMixins/CesiumIonMixin"; export default class GltfCatalogItem - extends UrlMixin(GltfMixin(CreateModel(GltfCatalogItemTraits))) + extends UrlMixin( + CesiumIonMixin(GltfMixin(CreateModel(GltfCatalogItemTraits))) + ) implements HasLocalData { static readonly type = "gltf"; @@ -24,7 +27,15 @@ export default class GltfCatalogItem @computed get gltfModelUrl() { - return this.url; + if (this.ionResource) { + return this.ionResource; + } else { + return this.url; + } + } + + protected override forceLoadMetadata(): Promise { + return this.loadIonResource(); } @observable hasLocalData = false; diff --git a/lib/Models/Catalog/Gtfs/GtfsCatalogItem.ts b/lib/Models/Catalog/Gtfs/GtfsCatalogItem.ts index 0080c2683bd..d973def35f7 100644 --- a/lib/Models/Catalog/Gtfs/GtfsCatalogItem.ts +++ b/lib/Models/Catalog/Gtfs/GtfsCatalogItem.ts @@ -22,6 +22,7 @@ import Entity from "terriajs-cesium/Source/DataSources/Entity"; import ModelGraphics from "terriajs-cesium/Source/DataSources/ModelGraphics"; import PointGraphics from "terriajs-cesium/Source/DataSources/PointGraphics"; import PropertyBag from "terriajs-cesium/Source/DataSources/PropertyBag"; +import Axis from "terriajs-cesium/Source/Scene/Axis"; import ColorBlendMode from "terriajs-cesium/Source/Scene/ColorBlendMode"; import HeightReference from "terriajs-cesium/Source/Scene/HeightReference"; import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; @@ -49,11 +50,6 @@ import { import prettyPrintGtfsEntityField from "./prettyPrintGtfsEntityField"; import VehicleData from "./VehicleData"; -// We want TS to look at the type declared in lib/ThirdParty/terriajs-cesium-extra/index.d.ts -// and import doesn't allows us to do that, so instead we use require + type casting to ensure -// we still maintain the type checking, without TS screaming with errors -const Axis: Axis = require("terriajs-cesium/Source/Scene/Axis").default; - interface RectangleExtent { east: number; south: number; diff --git a/lib/Models/Catalog/Ows/WebMapServiceCapabilities.ts b/lib/Models/Catalog/Ows/WebMapServiceCapabilities.ts index 1c567135c27..8b4e10ada75 100644 --- a/lib/Models/Catalog/Ows/WebMapServiceCapabilities.ts +++ b/lib/Models/Catalog/Ows/WebMapServiceCapabilities.ts @@ -163,7 +163,7 @@ export default class WebMapServiceCapabilities { createTransformer((url: string) => { return Promise.resolve(loadXML(url)).then(function (capabilitiesXml) { const json = xml2json(capabilitiesXml); - if (!defined(json.Capability)) { + if (!capabilitiesXml || !defined(json.Capability)) { throw networkRequestError({ title: "Invalid GetCapabilities", message: diff --git a/lib/Models/Catalog/Ows/WebMapServiceCapabilitiesStratum.ts b/lib/Models/Catalog/Ows/WebMapServiceCapabilitiesStratum.ts index ed847705c89..3c9069a2e65 100644 --- a/lib/Models/Catalog/Ows/WebMapServiceCapabilitiesStratum.ts +++ b/lib/Models/Catalog/Ows/WebMapServiceCapabilitiesStratum.ts @@ -825,7 +825,7 @@ export default class WebMapServiceCapabilitiesStratum extends LoadableStratum( @computed get currentTime() { // Get default times for all layers const defaultTimes = filterOutUndefined( - Array.from(this.capabilitiesLayers).map(([layerName, layer]) => { + Array.from(this.capabilitiesLayers).map(([_layerName, layer]) => { if (!layer) return; const dimensions = this.capabilities.getInheritedValues( layer, diff --git a/lib/Models/Catalog/Ows/WebMapServiceCatalogItem.ts b/lib/Models/Catalog/Ows/WebMapServiceCatalogItem.ts index 54f2b0ad886..a9c28efbb7b 100644 --- a/lib/Models/Catalog/Ows/WebMapServiceCatalogItem.ts +++ b/lib/Models/Catalog/Ows/WebMapServiceCatalogItem.ts @@ -16,6 +16,7 @@ import combine from "terriajs-cesium/Source/Core/combine"; import GetFeatureInfoFormat from "terriajs-cesium/Source/Scene/GetFeatureInfoFormat"; import WebMapServiceImageryProvider from "terriajs-cesium/Source/Scene/WebMapServiceImageryProvider"; import URI from "urijs"; +import { JsonObject } from "../../../Core/Json"; import TerriaError from "../../../Core/TerriaError"; import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined"; import filterOutUndefined from "../../../Core/filterOutUndefined"; @@ -23,7 +24,7 @@ import isDefined from "../../../Core/isDefined"; import CatalogMemberMixin, { getName } from "../../../ModelMixins/CatalogMemberMixin"; -import DiffableMixin from "../../../ModelMixins/DiffableMixin"; +import DiffableMixin, { DiffStratum } from "../../../ModelMixins/DiffableMixin"; import ExportWebCoverageServiceMixin from "../../../ModelMixins/ExportWebCoverageServiceMixin"; import GetCapabilitiesMixin from "../../../ModelMixins/GetCapabilitiesMixin"; import MappableMixin, { @@ -99,7 +100,9 @@ export class WebMapServiceUrlStratum extends LoadableStratum( } } +// Order is important so that the traits are overridden correctly StratumOrder.addLoadStratum(WebMapServiceUrlStratum.stratumName); +StratumOrder.addLoadStratum(DiffStratum.stratumName); class WebMapServiceCatalogItem extends TileErrorHandlerMixin( @@ -497,8 +500,13 @@ class WebMapServiceCatalogItem } @computed - get diffModeParameters() { - return { styles: this.diffStyleId }; + get diffModeParameters(): JsonObject { + return this.isShowingDiff ? { styles: this.diffStyleId } : {}; + } + + @computed + get diffModeGetFeatureInfoParameters(): JsonObject { + return this.isShowingDiff ? { styles: this.diffStyleId } : {}; } getTagForTime(date: JulianDate): string | undefined { @@ -550,10 +558,6 @@ class WebMapServiceCatalogItem ...this.getFeatureInfoParameters }; - const diffModeParameters = this.isShowingDiff - ? this.diffModeParameters - : {}; - if (this.supportsColorScaleRange) { parameters.COLORSCALERANGE = this.colorScaleRange; } @@ -562,7 +566,11 @@ class WebMapServiceCatalogItem parameters.styles = this.styles ?? ""; getFeatureInfoParameters.styles = this.styles ?? ""; - Object.assign(parameters, diffModeParameters); + Object.assign(parameters, this.diffModeParameters); + Object.assign( + getFeatureInfoParameters, + this.diffModeGetFeatureInfoParameters + ); // Remove problematic query parameters from URL - these are handled by the parameters objects diff --git a/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts b/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts index 4c494292f84..18cb04ff5aa 100644 --- a/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts +++ b/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts @@ -131,7 +131,7 @@ export default class WebMapTileServiceCapabilities { createTransformer((url: string) => { return Promise.resolve(loadXML(url)).then(function (capabilitiesXml) { const json = xml2json(capabilitiesXml); - if (!defined(json.ServiceIdentification)) { + if (!capabilitiesXml || !defined(json.ServiceIdentification)) { throw networkRequestError({ title: i18next.t( "models.webMapTileServiceCatalogGroup.invalidCapabilitiesTitle" diff --git a/lib/Models/Catalog/Ows/WebProcessingServiceCapabilities.ts b/lib/Models/Catalog/Ows/WebProcessingServiceCapabilities.ts index 0f30e1e2bda..16a9e7f7933 100644 --- a/lib/Models/Catalog/Ows/WebProcessingServiceCapabilities.ts +++ b/lib/Models/Catalog/Ows/WebProcessingServiceCapabilities.ts @@ -54,7 +54,7 @@ export interface ServiceProvider { export default class WebProcessingServiceCapabilities { constructor( - readonly capabilitiesXml: string, + readonly capabilitiesXml: XMLDocument, readonly capabilities: Capabilities ) {} @@ -62,7 +62,7 @@ export default class WebProcessingServiceCapabilities { return Promise.resolve(loadXML(url)).then(function (capabilitiesXml) { const capabilities = parseCapabilities(xml2json(capabilitiesXml)); - if (capabilities === undefined) { + if (capabilitiesXml === undefined || capabilities === undefined) { throw networkRequestError({ title: i18next.t( "models.webProcessingServiceCatalogGroup.invalidCapabilitiesTitle" diff --git a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts index 2ac05f34368..c44df590385 100644 --- a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts +++ b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts @@ -8,7 +8,6 @@ import { makeObservable, override } from "mobx"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; import URI from "urijs"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; @@ -188,11 +187,13 @@ class WpsLoadableStratum extends LoadableStratum( } get storeSupported() { - return Boolean(this.processDescription.storeSupported); + const value = this.processDescription.storeSupported?.toLowerCase(); + return value === "true" ? true : value === "false" ? false : undefined; } get statusSupported() { - return Boolean(this.processDescription.statusSupported); + const value = this.processDescription.statusSupported?.toLowerCase(); + return value === "true" ? true : value === "false" ? false : undefined; } } @@ -427,11 +428,17 @@ const LiteralDataConverter = { ...options }); } else if (dtype === "date") { - const dt = new DateParameter(catalogFunction, { ...options }); + const dt = new DateParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "literal"; return dt; } else if (dtype?.toLowerCase() === "datetime") { - const dt = new DateTimeParameter(catalogFunction, { ...options }); + const dt = new DateTimeParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "literal"; return dt; } @@ -469,7 +476,10 @@ const ComplexDateConverter = { if (schema !== "http://www.w3.org/TR/xmlschema-2/#date") { return undefined; } - const dparam = new DateParameter(catalogFunction, options); + const dparam = new DateParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dparam.variant = "complex"; return dparam; }, @@ -502,7 +512,10 @@ const ComplexDateTimeConverter = { if (schema !== "http://www.w3.org/TR/xmlschema-2/#dateTime") { return undefined; } - const dt = new DateTimeParameter(catalogFunction, options); + const dt = new DateTimeParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "complex"; return dt; }, @@ -572,7 +585,6 @@ const RectangleConverter = { parameterToInput: function (functionParameter: FunctionParameter) { const parameter = functionParameter as RectangleParameter; const value = parameter.value; - if (!isDefined(value)) { return; } @@ -581,10 +593,10 @@ const RectangleConverter = { // We only support CRS84 and EPSG:4326 if (parameter.crs.indexOf("crs84") !== -1) { // CRS84 uses long, lat rather that lat, long order. - bboxMinCoord1 = CesiumMath.toDegrees(value.west); - bboxMinCoord2 = CesiumMath.toDegrees(value.south); - bboxMaxCoord1 = CesiumMath.toDegrees(value.east); - bboxMaxCoord2 = CesiumMath.toDegrees(value.north); + bboxMinCoord1 = value.west; + bboxMinCoord2 = value.south; + bboxMaxCoord1 = value.east; + bboxMaxCoord2 = value.north; // Comfortingly known as WGS 84 longitude-latitude according to Table 3 in OGC 07-092r1. urn = "urn:ogc:def:crs:OGC:1.3:CRS84"; } else { @@ -592,10 +604,10 @@ const RectangleConverter = { // 4326 specified in version 6.6 of the EPSG database available at http://www.epsg.org/. That CRS specifies // the axis order as Latitude followed by Longitude. // We don't know about other URN versions, so are going to return 6.6 regardless of what was requested. - bboxMinCoord1 = CesiumMath.toDegrees(value.south); - bboxMinCoord2 = CesiumMath.toDegrees(value.west); - bboxMaxCoord1 = CesiumMath.toDegrees(value.north); - bboxMaxCoord2 = CesiumMath.toDegrees(value.east); + bboxMinCoord1 = value.south; + bboxMinCoord2 = value.west; + bboxMaxCoord1 = value.north; + bboxMaxCoord2 = value.east; urn = "urn:ogc:def:crs:EPSG:6.6:4326"; } diff --git a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts index c3eafa7feca..28d43aa4638 100644 --- a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts +++ b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts @@ -3,17 +3,18 @@ import { action, computed, isObservableArray, + makeObservable, observable, + override, runInAction, - toJS, - makeObservable, - override + toJS } from "mobx"; import Mustache from "mustache"; +import createGuid from "terriajs-cesium/Source/Core/createGuid"; import URI from "urijs"; -import isDefined from "../../../Core/isDefined"; import { JsonObject } from "../../../Core/Json"; import TerriaError from "../../../Core/TerriaError"; +import isDefined from "../../../Core/isDefined"; import CatalogFunctionJobMixin from "../../../ModelMixins/CatalogFunctionJobMixin"; import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin"; import XmlRequestMixin from "../../../ModelMixins/XmlRequestMixin"; @@ -23,22 +24,19 @@ import { FeatureInfoTemplateTraits } from "../../../Traits/TraitsClasses/Feature import WebProcessingServiceCatalogFunctionJobTraits from "../../../Traits/TraitsClasses/WebProcessingServiceCatalogFunctionJobTraits"; import CommonStrata from "../../Definition/CommonStrata"; import CreateModel from "../../Definition/CreateModel"; -import createStratumInstance from "../../Definition/createStratumInstance"; import LoadableStratum from "../../Definition/LoadableStratum"; -import { BaseModel } from "../../Definition/Model"; +import { BaseModel, ModelConstructorParameters } from "../../Definition/Model"; import StratumFromTraits from "../../Definition/StratumFromTraits"; import StratumOrder from "../../Definition/StratumOrder"; +import createStratumInstance from "../../Definition/createStratumInstance"; import updateModelFromJson from "../../Definition/updateModelFromJson"; import upsertModelFromJson from "../../Definition/upsertModelFromJson"; import GeoJsonCatalogItem from "../CatalogItems/GeoJsonCatalogItem"; import CatalogMemberFactory from "../CatalogMemberFactory"; -import { ModelConstructorParameters } from "../../Definition/Model"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; const executeWpsTemplate = require("./ExecuteWpsTemplate.xml"); -const createGuid = require("terriajs-cesium/Source/Core/createGuid").default; - class WpsLoadableStratum extends LoadableStratum( WebProcessingServiceCatalogFunctionJobTraits ) { @@ -362,7 +360,7 @@ export default class WebProcessingServiceCatalogFunctionJob extends XmlRequestMi this.geoJsonItem = new GeoJsonCatalogItem(createGuid(), this.terria); updateModelFromJson(this.geoJsonItem, CommonStrata.user, { name: `${this.name} Input Features`, - // Use cesium primitives so we don't have to deal with feature picking/selection + // Use cesium primitives, so we don't have to deal with feature picking/selection forceCesiumPrimitives: true, geoJsonData: { type: "FeatureCollection", @@ -370,10 +368,10 @@ export default class WebProcessingServiceCatalogFunctionJob extends XmlRequestMi totalFeatures: this.geojsonFeatures!.length } }).logError( - "Error ocurred while updating Input Features GeoJSON model JSON" + "Error occurred while updating Input Features GeoJSON model JSON" ); }); - (await this.geoJsonItem!.loadMapItems()).throwIfError; + (await this.geoJsonItem!.loadMapItems()).throwIfError(); } runInAction(() => { diff --git a/lib/Models/Catalog/registerCatalogMembers.ts b/lib/Models/Catalog/registerCatalogMembers.ts index 9894967f50e..41483a6b0f6 100644 --- a/lib/Models/Catalog/registerCatalogMembers.ts +++ b/lib/Models/Catalog/registerCatalogMembers.ts @@ -16,6 +16,7 @@ import CzmlCatalogItem from "./CatalogItems/CzmlCatalogItem"; import GeoJsonCatalogItem from "./CatalogItems/GeoJsonCatalogItem"; import GeoRssCatalogItem from "./CatalogItems/GeoRssCatalogItem"; import GpxCatalogItem from "./CatalogItems/GpxCatalogItem"; +import I3SCatalogItem from "./CatalogItems/I3SCatalogItem"; import IonImageryCatalogItem from "./CatalogItems/IonImageryCatalogItem"; import KmlCatalogItem from "./CatalogItems/KmlCatalogItem"; import MapboxMapCatalogItem from "./CatalogItems/MapboxMapCatalogItem"; @@ -41,6 +42,7 @@ import CkanItemReference from "./Ckan/CkanItemReference"; import ArcGisCatalogGroup from "./Esri/ArcGisCatalogGroup"; import ArcGisFeatureServerCatalogGroup from "./Esri/ArcGisFeatureServerCatalogGroup"; import ArcGisFeatureServerCatalogItem from "./Esri/ArcGisFeatureServerCatalogItem"; +import ArcGisImageServerCatalogItem from "./Esri/ArcGisImageServerCatalogItem"; import ArcGisMapServerCatalogGroup from "./Esri/ArcGisMapServerCatalogGroup"; import ArcGisMapServerCatalogItem from "./Esri/ArcGisMapServerCatalogItem"; import ArcGisPortalCatalogGroup from "./Esri/ArcGisPortalCatalogGroup"; @@ -62,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); @@ -105,6 +108,10 @@ export default function registerCatalogMembers() { ArcGisMapServerCatalogItem.type, ArcGisMapServerCatalogItem ); + CatalogMemberFactory.register( + ArcGisImageServerCatalogItem.type, + ArcGisImageServerCatalogItem + ); CatalogMemberFactory.register( ArcGisMapServerCatalogGroup.type, ArcGisMapServerCatalogGroup @@ -140,6 +147,7 @@ export default function registerCatalogMembers() { CesiumTerrainCatalogItem.type, CesiumTerrainCatalogItem ); + CatalogMemberFactory.register(I3SCatalogItem.type, I3SCatalogItem); CatalogMemberFactory.register( IonImageryCatalogItem.type, IonImageryCatalogItem @@ -234,6 +242,7 @@ export default function registerCatalogMembers() { UrlTemplateImageryCatalogItem ); CatalogMemberFactory.register(AssImpCatalogItem.type, AssImpCatalogItem); + CatalogMemberFactory.register(CogCatalogItem.type, CogCatalogItem); UrlToCatalogMemberMapping.register( matchesExtension("csv"), @@ -280,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( @@ -302,6 +315,11 @@ export default function registerCatalogMembers() { ArcGisMapServerCatalogItem.type, true ); + UrlToCatalogMemberMapping.register( + matchesUrl(/\/arcgis\/rest\/.*\/ImageServer(\/.*)?$/i), + ArcGisImageServerCatalogItem.type, + true + ); UrlToCatalogMemberMapping.register( matchesUrl(/\/arcgis\/rest\/.*\/MapServer(\/.*)?$/i), ArcGisMapServerCatalogGroup.type, @@ -332,6 +350,11 @@ export default function registerCatalogMembers() { ArcGisMapServerCatalogItem.type, true ); + UrlToCatalogMemberMapping.register( + matchesUrl(/\/rest\/.*\/ImageServer(\/.*)?$/i), + ArcGisImageServerCatalogItem.type, + true + ); UrlToCatalogMemberMapping.register( matchesUrl(/\/rest\/.*\/MapServer(\/.*)?$/i), ArcGisMapServerCatalogGroup.type, @@ -395,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)); }; diff --git a/lib/Models/Cesium.ts b/lib/Models/Cesium.ts index d795f65c296..0dcd21cf1b0 100644 --- a/lib/Models/Cesium.ts +++ b/lib/Models/Cesium.ts @@ -92,6 +92,7 @@ import Terria from "./Terria"; import UserDrawing from "./UserDrawing"; import { setViewerMode } from "./ViewerMode"; import ScreenSpaceEventHandler from "terriajs-cesium/Source/Core/ScreenSpaceEventHandler"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; //import Cesium3DTilesInspector from "terriajs-cesium/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector"; @@ -196,7 +197,7 @@ export default class Cesium extends GlobeOrMap { // Workaround for Firefox bug with WebGL and printing: // https://bugzilla.mozilla.org/show_bug.cgi?id=976173 - const firefoxBugOptions = (FeatureDetection as any).isFirefox() + const firefoxBugOptions = FeatureDetection.isFirefox() ? { contextOptions: { webgl: { preserveDrawingBuffer: true } @@ -240,7 +241,7 @@ export default class Cesium extends GlobeOrMap { this._eventHelper.add( this.scene.globe.tileLoadProgressEvent, (currentLoadQueueLength: number) => - this._updateTilesLoadingCount(currentLoadQueueLength) as any + this._updateTilesLoadingCount(currentLoadQueueLength) ); // Disable HDR lighting for better performance and to avoid changing imagery colors. @@ -409,16 +410,15 @@ export default class Cesium extends GlobeOrMap { this._disposeWorkbenchMapItemsSubscription = this.observeModelLayer(); this._disposeTerrainReaction = autorun(() => { this.scene.globe.terrainProvider = this.terrainProvider; - // TODO: bring over globe and atmosphere splitting support from terriajs-cesium - // this.scene.globe.splitDirection = this.terria.showSplitter - // ? this.terria.terrainSplitDirection - // : SplitDirection.NONE; + this.scene.globe.splitDirection = this.terria.showSplitter + ? this.terria.terrainSplitDirection + : SplitDirection.NONE; this.scene.globe.depthTestAgainstTerrain = this.terria.depthTestAgainstTerrainEnabled; - // if (this.scene.skyAtmosphere) { - // this.scene.skyAtmosphere.splitDirection = - // this.scene.globe.splitDirection; - // } + if (this.scene.skyAtmosphere) { + this.scene.skyAtmosphere.splitDirection = + this.scene.globe.splitDirection; + } }); this._disposeSplitterReaction = this._reactToSplitterChanges(); @@ -928,6 +928,14 @@ export default class Cesium extends GlobeOrMap { duration: flightDurationSeconds, destination: target.rectangle }); + } else if ( + defined(target.imageryProvider) && + defined(target.imageryProvider.rectangle) + ) { + return flyToPromise(camera, { + duration: flightDurationSeconds, + destination: target.imageryProvider.rectangle + }); } else { return Promise.resolve(); } @@ -1281,7 +1289,10 @@ export default class Cesium extends GlobeOrMap { * */ @action - pickFromScreenPosition(screenPosition: Cartesian2, ignoreSplitter: boolean) { + async pickFromScreenPosition( + screenPosition: Cartesian2, + ignoreSplitter: boolean + ) { const pickRay = this.scene.camera.getPickRay(screenPosition); const pickPosition = isDefined(pickRay) ? this.scene.globe.pick(pickRay, this.scene) @@ -1289,7 +1300,7 @@ export default class Cesium extends GlobeOrMap { const pickPositionCartographic = pickPosition && Ellipsoid.WGS84.cartesianToCartographic(pickPosition); - const vectorFeatures = this.pickVectorFeatures(screenPosition); + const vectorFeatures = await this.pickVectorFeatures(screenPosition); const providerCoords = this._attachProviderCoordHooks(); const pickRasterPromise = @@ -1407,7 +1418,7 @@ export default class Cesium extends GlobeOrMap { * @param screenPosition position on the screen to look for features * @returns The features found. */ - private pickVectorFeatures(screenPosition: Cartesian2) { + private async pickVectorFeatures(screenPosition: Cartesian2) { // Pick vector features const vectorFeatures = []; const pickedList = this.scene.drillPick(screenPosition); @@ -1436,7 +1447,9 @@ export default class Cesium extends GlobeOrMap { typeof catalogItem?.getFeaturesFromPickResult === "function" && this.terria.allowFeatureInfoRequests ) { - const result = catalogItem.getFeaturesFromPickResult.bind(catalogItem)( + const result = await catalogItem.getFeaturesFromPickResult.bind( + catalogItem + )( screenPosition, picked, vectorFeatures.length < catalogItem.maxRequests @@ -1634,10 +1647,12 @@ export default class Cesium extends GlobeOrMap { return this._makeImageryLayerFromParts(m, item) as ImageryLayer; } else if (isCesium3DTileset(m)) { return m; + } else if (m instanceof I3SDataProvider) { + return filterOutUndefined(m.layers.map((layer) => layer.tileset)); } return undefined; }) - ); + ).flat(1); /* Flatten I3S tilesets */ } private _makeImageryLayerFromParts( @@ -1742,7 +1757,7 @@ export default class Cesium extends GlobeOrMap { _addVectorTileHighlight( imageryProvider: MapboxVectorTileImageryProvider | ProtomapsImageryProvider, - rectangle: Rectangle + _rectangle: Rectangle ): () => void { const result = new ImageryLayer(imageryProvider, { show: true, diff --git a/lib/Models/Definition/FlattenedFromTraits.ts b/lib/Models/Definition/FlattenedFromTraits.ts index 320cc1fde40..45b227e9f50 100644 --- a/lib/Models/Definition/FlattenedFromTraits.ts +++ b/lib/Models/Definition/FlattenedFromTraits.ts @@ -8,7 +8,7 @@ type SingleTrait = If< TTrait extends ModelTraits ? FlattenedFromTraits : never >; -type ArrayTrait = ReadonlyArray>; +type ArrayTrait<_TTrait, TElement> = ReadonlyArray>; /** * Transforms a {@link ModelTraits} class into a type usable as a flattened view of multiple strata. diff --git a/lib/Models/Definition/LoadableStratum.ts b/lib/Models/Definition/LoadableStratum.ts index d6cd1f5168f..c14211318f6 100644 --- a/lib/Models/Definition/LoadableStratum.ts +++ b/lib/Models/Definition/LoadableStratum.ts @@ -24,7 +24,7 @@ export default function LoadableStratum< get: function () { return undefined; }, - set: function (value: any) { + set: function (_value: any) { throw new DeveloperError( "Traits of a LoadableStratum may not be set." ); diff --git a/lib/Models/Definition/ModelPropertiesFromTraits.ts b/lib/Models/Definition/ModelPropertiesFromTraits.ts index a3d32bd7565..c6c28092c6b 100644 --- a/lib/Models/Definition/ModelPropertiesFromTraits.ts +++ b/lib/Models/Definition/ModelPropertiesFromTraits.ts @@ -8,7 +8,7 @@ type SingleTrait = If< TTrait, TTrait extends ModelTraits ? Model : never >; -type ArrayTrait = ReadonlyArray>; +type ArrayTrait<_TTrait, TElement> = ReadonlyArray>; /** * Transforms a {@link ModelTraits} class into a type representative of the traits properties exposed on diff --git a/lib/Models/Definition/StratumFromTraits.ts b/lib/Models/Definition/StratumFromTraits.ts index ca347f534ee..1b92787f473 100644 --- a/lib/Models/Definition/StratumFromTraits.ts +++ b/lib/Models/Definition/StratumFromTraits.ts @@ -8,7 +8,7 @@ type SingleTrait = If< TTrait extends ModelTraits ? StratumFromTraits : never >; -type ArrayTrait = Array>; +type ArrayTrait<_TTrait, TElement> = Array>; /** * Transforms a {@link ModelTraits} class into a type usable as a stratum. diff --git a/lib/Models/ErrorServiceProviders/ErrorService.ts b/lib/Models/ErrorServiceProviders/ErrorService.ts index 03c59b904e6..dd506e3b773 100644 --- a/lib/Models/ErrorServiceProviders/ErrorService.ts +++ b/lib/Models/ErrorServiceProviders/ErrorService.ts @@ -1,31 +1,14 @@ import TerriaError from "../../Core/TerriaError"; - +import { ConfigParameters } from "../Terria"; export interface ErrorServiceOptions { - provider: string; + provider?: string; configuration: any; } /** * The interface that all valid error service providers must implement. */ -export type ErrorServiceProvider = { - error(error: string | Error | TerriaError): void; -}; - -/** - * Asynchronously loads and returns an error service provider instance for the given options. - */ -export async function initializeErrorServiceProvider( - options?: ErrorServiceOptions -): Promise { - const provider = options?.provider; - const configuration = options?.configuration; - - if (provider === "rollbar") { - const rollbarModule = await import("./RollbarErrorServiceProvider"); - const rollbarProvider = new rollbarModule.default(configuration); - return rollbarProvider; - } else { - throw new Error(`Unknown error service provider: ${provider}`); - } +export interface ErrorServiceProvider { + init: (config: ConfigParameters) => void; + error: (error: string | Error | TerriaError) => void; } diff --git a/lib/Models/ErrorServiceProviders/RollbarErrorServiceProvider.ts b/lib/Models/ErrorServiceProviders/RollbarErrorServiceProvider.ts deleted file mode 100644 index c2831b6a57d..00000000000 --- a/lib/Models/ErrorServiceProviders/RollbarErrorServiceProvider.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Rollbar from "rollbar"; -import TerriaError, { TerriaErrorSeverity } from "../../Core/TerriaError"; -import { ErrorServiceProvider } from "./ErrorService"; - -export default class RollbarErrorServiceProvider - implements ErrorServiceProvider -{ - readonly rollbar: Rollbar; - - /** - * @param configuration Configuration for the Rollbar instance. See https://docs.rollbar.com/docs/rollbarjs-configuration-reference#context-1 - * - * Caveat: Rollbar API requests are blocked by some privacy extensions for browsers. - */ - constructor(configuration: any) { - this.rollbar = new Rollbar({ - captureUncaught: true, - captureUnhandledRejections: true, - enabled: true, - ...configuration - }); - } - - error(error: string | Error | TerriaError) { - if (error instanceof TerriaError) { - if ( - (typeof error.severity === "function" - ? error.severity() - : error.severity) === TerriaErrorSeverity.Error - ) { - this.rollbar.error(error.toError()); - } else { - this.rollbar.warning(error.toError()); - } - } else { - this.rollbar.error(error); - } - } -} diff --git a/lib/Models/ErrorServiceProviders/StubErrorServiceProvider.ts b/lib/Models/ErrorServiceProviders/StubErrorServiceProvider.ts index 689d8280992..6ac53fe3eb4 100644 --- a/lib/Models/ErrorServiceProviders/StubErrorServiceProvider.ts +++ b/lib/Models/ErrorServiceProviders/StubErrorServiceProvider.ts @@ -5,5 +5,6 @@ import { ErrorServiceProvider } from "./ErrorService"; * A stub error service provider that does nothing. */ export default class StubErrorServiceProvider implements ErrorServiceProvider { + init() {} error(_error: string | Error | TerriaError) {} } diff --git a/lib/Models/Feature/Feature.ts b/lib/Models/Feature/Feature.ts index a149bf0c1ff..fceaf85c047 100644 --- a/lib/Models/Feature/Feature.ts +++ b/lib/Models/Feature/Feature.ts @@ -108,5 +108,3 @@ function addCustomFeatureProperties(entity: Entity) { } } } - -module.exports = TerriaFeature; diff --git a/lib/Models/Feature/ImageryLayerFeatureInfo.js b/lib/Models/Feature/ImageryLayerFeatureInfo.ts similarity index 65% rename from lib/Models/Feature/ImageryLayerFeatureInfo.js rename to lib/Models/Feature/ImageryLayerFeatureInfo.ts index e9a9b84d4fe..eb42f051eff 100644 --- a/lib/Models/Feature/ImageryLayerFeatureInfo.js +++ b/lib/Models/Feature/ImageryLayerFeatureInfo.ts @@ -1,10 +1,6 @@ -"use strict"; - -var ImageryLayerFeatureInfo = - require("terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo").default; -var defined = require("terriajs-cesium/Source/Core/defined").default; - -var formatPropertyValue = require("../../Core/formatPropertyValue"); +import defined from "terriajs-cesium/Source/Core/defined"; +import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; +import formatPropertyValue from "../../Core/formatPropertyValue"; /** * Configures the description of this feature by creating an HTML table of properties and their values. @@ -12,12 +8,12 @@ var formatPropertyValue = require("../../Core/formatPropertyValue"); * @param {Object} properties An object literal containing the properties of the feature. */ ImageryLayerFeatureInfo.prototype.configureDescriptionFromProperties = - function (properties) { - function describe(properties) { - var html = ''; - for (var key in properties) { + function (properties: any) { + function describe(properties: any) { + let html = '
'; + for (const key in properties) { if (Object.prototype.hasOwnProperty.call(properties, key)) { - var value = properties[key]; + const value = properties[key]; if (defined(value)) { if (typeof value === "object") { html += diff --git a/lib/Models/FunctionParameters/DateParameter.ts b/lib/Models/FunctionParameters/DateParameter.ts index a82cd9b322d..bcf33e5c7a8 100644 --- a/lib/Models/FunctionParameters/DateParameter.ts +++ b/lib/Models/FunctionParameters/DateParameter.ts @@ -1,10 +1,70 @@ -import FunctionParameter from "./FunctionParameter"; +import Clock from "terriajs-cesium/Source/Core/Clock"; +import CatalogFunctionMixin from "../../ModelMixins/CatalogFunctionMixin"; +import FunctionParameter, { Options } from "./FunctionParameter"; +import { makeObservable, override } from "mobx"; +import moment from "moment"; + +interface DateParameterOptions extends Options { + /** + * Clock to read the default date time value + */ + clock: Clock; +} export default class DateParameter extends FunctionParameter { static readonly type = "date"; readonly type = "date"; variant = "complex"; + private readonly clock: Clock; + + constructor( + protected readonly catalogFunction: CatalogFunctionMixin.Instance, + options: DateParameterOptions + ) { + super(catalogFunction, options); + this.clock = options.clock; + makeObservable(this); + } + + /** + * Return current date value. + * + * When no value is available and the field is marked as required, then + * return the current timeline clock date. + */ + @override + get value(): string | undefined { + return ( + super.value ?? (this.isRequired ? this.currentClockDate() : undefined) + ); + } + + /** + * Validate and set datetime value + */ + setValue(stratumId: string, newValue: string) { + super.setValue( + stratumId, + this.isValidDate(newValue) ? newValue : undefined + ); + } + + private isValidDate(value: string): boolean { + const date = new Date(value); + return date instanceof Date && !isNaN(date.valueOf()); + } + + /** + * Returns current clock time in local time zone. + */ + private currentClockDate(): string { + const currentTime = this.clock.currentTime; + const ct = new Date(currentTime.toString()); + const date = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); + return date; + } + /** * Process value so that it can be used in an URL. */ diff --git a/lib/Models/FunctionParameters/DateTimeParameter.ts b/lib/Models/FunctionParameters/DateTimeParameter.ts index c65cf658878..50f17d7d89f 100644 --- a/lib/Models/FunctionParameters/DateTimeParameter.ts +++ b/lib/Models/FunctionParameters/DateTimeParameter.ts @@ -1,10 +1,71 @@ -import FunctionParameter from "./FunctionParameter"; +import { makeObservable, override } from "mobx"; +import moment from "moment"; +import Clock from "terriajs-cesium/Source/Core/Clock"; +import CatalogFunctionMixin from "../../ModelMixins/CatalogFunctionMixin"; +import FunctionParameter, { Options } from "./FunctionParameter"; + +interface DateTimeParameterOptions extends Options { + /** + * Clock to read the default date time value + */ + clock: Clock; +} export default class DateTimeParameter extends FunctionParameter { static readonly type = "dateTime"; readonly type = "dateTime"; variant = "complex"; + private readonly clock: Clock; + + constructor( + protected readonly catalogFunction: CatalogFunctionMixin.Instance, + options: DateTimeParameterOptions + ) { + super(catalogFunction, options); + this.clock = options.clock; + makeObservable(this); + } + + /** + * Return current date time value. + * + * When no value is available and the field is marked as required, then + * return the current timeline clock time. + */ + @override + get value(): string | undefined { + return ( + super.value ?? (this.isRequired ? this.currentClockTime() : undefined) + ); + } + + /** + * Validate and set datetime value + */ + setValue(stratumId: string, newValue: string) { + super.setValue( + stratumId, + this.isValidDateTime(newValue) ? newValue : undefined + ); + } + + private isValidDateTime(value: string): boolean { + const date = new Date(value); + return date instanceof Date && !isNaN(date.valueOf()); + } + + /** + * Returns current clock time in local time zone. + */ + private currentClockTime(): string { + const currentTime = this.clock.currentTime; + const ct = new Date(currentTime.toString()); + const date = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); + const time = moment.utc(ct.toISOString()).local().format("HH:mm"); + return `${date}T${time}`; + } + /** * Process value so that it can be used in an URL. */ diff --git a/lib/Models/FunctionParameters/FunctionParameter.ts b/lib/Models/FunctionParameters/FunctionParameter.ts index cd45dbeca2a..646b0026d22 100644 --- a/lib/Models/FunctionParameters/FunctionParameter.ts +++ b/lib/Models/FunctionParameters/FunctionParameter.ts @@ -51,7 +51,7 @@ export default abstract class FunctionParameter< return this.catalogFunction.parameters?.[this.id] as T; } - setValue(strataId: string, v: T) { + setValue(strataId: string, v: T | undefined) { if (isDefined(v)) { let newParameters: JsonObject = { [this.id]: v! diff --git a/lib/Models/FunctionParameters/InfoParameter.ts b/lib/Models/FunctionParameters/InfoParameter.ts index 88fa15b9aaf..84a0a4ddfb0 100644 --- a/lib/Models/FunctionParameters/InfoParameter.ts +++ b/lib/Models/FunctionParameters/InfoParameter.ts @@ -51,7 +51,7 @@ export default class InfoParameter extends FunctionParameter { this._value = v; } - clearValue(strataId: string) { + clearValue(_strataId: string) { this._value = undefined; } } diff --git a/lib/Models/GlobeOrMap.ts b/lib/Models/GlobeOrMap.ts index a50b43e2874..f708c06acbf 100644 --- a/lib/Models/GlobeOrMap.ts +++ b/lib/Models/GlobeOrMap.ts @@ -27,13 +27,15 @@ import TableOutlineStyleTraits, { } from "../Traits/TraitsClasses/Table/OutlineStyleTraits"; import TableStyleTraits from "../Traits/TraitsClasses/Table/StyleTraits"; import CameraView from "./CameraView"; -import Cesium3DTilesCatalogItem from "./Catalog/CatalogItems/Cesium3DTilesCatalogItem"; import CommonStrata from "./Definition/CommonStrata"; import createStratumInstance from "./Definition/createStratumInstance"; import TerriaFeature from "./Feature/Feature"; import Terria from "./Terria"; +import TerriaError from "../Core/TerriaError"; -require("./Feature/ImageryLayerFeatureInfo"); // overrides Cesium's prototype.configureDescriptionFromProperties +import "./Feature/ImageryLayerFeatureInfo"; // overrides Cesium's prototype.configureDescriptionFromProperties +import hasTraits from "./Definition/hasTraits"; +import HighlightColorTraits from "../Traits/TraitsClasses/HighlightColorTraits"; export default abstract class GlobeOrMap { abstract readonly type: string; @@ -234,17 +236,16 @@ export default abstract class GlobeOrMap { // Get the highlight color from the catalogItem trait or default to baseMapContrastColor const catalogItem = feature._catalogItem; - let highlightColor; - if (catalogItem instanceof Cesium3DTilesCatalogItem) { - highlightColor = - Color.fromCssColorString( - runInAction(() => catalogItem.highlightColor) - ) ?? defaultColor; + let highlightColorString; + if (hasTraits(catalogItem, HighlightColorTraits, "highlightColor")) { + highlightColorString = runInAction(() => catalogItem.highlightColor); + runInAction(() => catalogItem.highlightColor); } else { - highlightColor = - Color.fromCssColorString(this.terria.baseMapContrastColor) ?? - defaultColor; + highlightColorString = this.terria.baseMapContrastColor; } + const highlightColor: Color = isDefined(highlightColorString) + ? Color.fromCssColorString(highlightColorString) + : defaultColor; // highlighting doesn't work if the highlight colour is full white // so in this case use something close to white instead @@ -258,9 +259,13 @@ export default abstract class GlobeOrMap { this._removeHighlightCallback = function () { if ( isDefined(feature._cesium3DTileFeature) && - !feature._cesium3DTileFeature.tileset.isDestroyed() + feature._cesium3DTileFeature.tileset.isDestroyed() === false ) { - feature._cesium3DTileFeature.color = originalColor; + try { + feature._cesium3DTileFeature.color = originalColor; + } catch (err) { + TerriaError.from(err).log(); + } } }; } else if (isDefined(feature.polygon)) { diff --git a/lib/Models/ItemSearchProviders/TextIndex.ts b/lib/Models/ItemSearchProviders/TextIndex.ts index 39dead55b13..88895e3c317 100644 --- a/lib/Models/ItemSearchProviders/TextIndex.ts +++ b/lib/Models/ItemSearchProviders/TextIndex.ts @@ -1,8 +1,7 @@ import MiniSearch, { Options as MiniSearchOptions } from "minisearch"; -import joinUrl from "./joinUrl"; +import loadText from "../../Core/loadText"; import { IndexBase, IndexType } from "./Types"; - -const loadText = require("../../Core/loadText"); +import joinUrl from "./joinUrl"; // Text search query type TextSearchQuery = string; diff --git a/lib/Models/Leaflet.ts b/lib/Models/Leaflet.ts index 76921a6a215..6bc55a6e3e8 100644 --- a/lib/Models/Leaflet.ts +++ b/lib/Models/Leaflet.ts @@ -60,12 +60,6 @@ import GlobeOrMap from "./GlobeOrMap"; import { LeafletAttribution } from "./LeafletAttribution"; import Terria from "./Terria"; -// We want TS to look at the type declared in lib/ThirdParty/terriajs-cesium-extra/index.d.ts -// and import doesn't allows us to do that, so instead we use require + type casting to ensure -// we still maintain the type checking, without TS screaming with errors -const FeatureDetection: FeatureDetection = - require("terriajs-cesium/Source/Core/FeatureDetection").default; - // This class is an observer. It probably won't contain any observables itself export default class Leaflet extends GlobeOrMap { @@ -173,7 +167,7 @@ export default class Leaflet extends GlobeOrMap { this.dataSourceDisplay = new LeafletDataSourceDisplay({ scene: this.scene, dataSourceCollection: this.dataSources, - visualizersCallback: this._leafletVisualizer.visualizersCallback as any // TODO: fix type error + visualizersCallback: this._leafletVisualizer.visualizersCallback }); this._eventHelper = new EventHelper(); @@ -204,7 +198,7 @@ export default class Leaflet extends GlobeOrMap { map.boxZoom, map.keyboard, map.dragging, - map.tap + map.tapHold ]); const pickLocation = this.pickLocation.bind(this); const pickFeature = (entity: Entity, event: L.LeafletMouseEvent) => { @@ -501,11 +495,9 @@ export default class Leaflet extends GlobeOrMap { ): Promise { if (!isDefined(target)) { return Promise.resolve(); - //throw new DeveloperError("target is required."); } let bounds; - // Target is a KML data source if (isDefined(target.entities)) { if (isDefined(this.dataSourceDisplay)) { bounds = this.dataSourceDisplay.getLatLngBounds(target); @@ -522,19 +514,25 @@ export default class Leaflet extends GlobeOrMap { extent = target.cesiumRectangle; } if (!isDefined(extent)) { - // Zoom to the first item! return this.doZoomTo(target.mapItems[0], flightDurationSeconds); } } else { extent = target.rectangle; } - // Account for a bounding box crossing the date line. - if (extent.east < extent.west) { - extent = Rectangle.clone(extent); - extent.east += CesiumMath.TWO_PI; + // Ensure extent is defined before accessing its properties + if (isDefined(extent)) { + // Account for a bounding box crossing the date line. + if (extent.east < extent.west) { + extent = Rectangle.clone(extent); + extent.east += CesiumMath.TWO_PI; + } + bounds = rectangleToLatLngBounds(extent); + } else { + // Handle the case where extent is undefined + console.error("Unable to determine bounds for zooming."); + return Promise.resolve(); } - bounds = rectangleToLatLngBounds(extent); } if (isDefined(bounds)) { @@ -598,7 +596,7 @@ export default class Leaflet extends GlobeOrMap { */ @action - private _featurePicked(entity: Entity, event: L.LeafletMouseEvent) { + private async _featurePicked(entity: Entity, event: L.LeafletMouseEvent) { this._pickFeatures(event.latlng); // Ignore clicks on the feature highlight. @@ -621,7 +619,9 @@ export default class Leaflet extends GlobeOrMap { typeof catalogItem.getFeaturesFromPickResult === "function" && this.terria.allowFeatureInfoRequests ) { - const result = catalogItem.getFeaturesFromPickResult.bind(catalogItem)( + const result = await catalogItem.getFeaturesFromPickResult.bind( + catalogItem + )( undefined, entity, (this._pickedFeatures?.features.length || 0) < catalogItem.maxRequests diff --git a/lib/Models/LocationMarkerUtils.ts b/lib/Models/LocationMarkerUtils.ts index d6c93ba9df4..bbdaca02bcd 100644 --- a/lib/Models/LocationMarkerUtils.ts +++ b/lib/Models/LocationMarkerUtils.ts @@ -1,5 +1,4 @@ import i18next from "i18next"; -import markerIcon from "./markerIcon"; import prettifyCoordinates from "../Map/Vector/prettifyCoordinates"; import CommonStrata from "./Definition/CommonStrata"; import CzmlCatalogItem from "./Catalog/CatalogItems/CzmlCatalogItem"; @@ -7,6 +6,9 @@ import Terria from "./Terria"; import LatLonHeight from "../Core/LatLonHeight"; import { toJS } from "mobx"; +// @ts-expect-error image import +import markerIcon from "../../wwwroot/images/map-pin.png"; + export const LOCATION_MARKER_DATA_SOURCE_NAME = "TerriaJS Location Marker Points"; export const MARKER_UNIQUE_ID = "__TERRRIAJS-LOCATIONMARKER__"; diff --git a/lib/Models/MapInteractionMode.ts b/lib/Models/MapInteractionMode.ts index 6294ae5e2cf..56e8a9a3c08 100644 --- a/lib/Models/MapInteractionMode.ts +++ b/lib/Models/MapInteractionMode.ts @@ -2,6 +2,7 @@ import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; import PickedFeatures from "../Map/PickedFeatures/PickedFeatures"; import { observable, makeObservable } from "mobx"; import ViewState from "../ReactViewModels/ViewState"; +import React from "react"; export enum UIMode { Difference diff --git a/lib/Models/NoViewer.ts b/lib/Models/NoViewer.ts index 6b546e1da86..a45ab3dea51 100644 --- a/lib/Models/NoViewer.ts +++ b/lib/Models/NoViewer.ts @@ -27,7 +27,7 @@ class NoViewer extends GlobeOrMap { doZoomTo( v: CameraView | Rectangle | MappableMixin.Instance, - t: any + _t: any ): Promise { if (v instanceof CameraView) { this._currentView = v; @@ -40,9 +40,9 @@ class NoViewer extends GlobeOrMap { notifyRepaintRequired() {} pickFromLocation( - latLngHeight: LatLonHeight, - providerCoords: ProviderCoordsMap, - existingFeatures: TerriaFeature[] + _latLngHeight: LatLonHeight, + _providerCoords: ProviderCoordsMap, + _existingFeatures: TerriaFeature[] ) {} getCurrentCameraView(): CameraView { @@ -56,8 +56,10 @@ class NoViewer extends GlobeOrMap { pauseMapInteraction() {} resumeMapInteraction() {} _addVectorTileHighlight( - imageryProvider: MapboxVectorTileImageryProvider | ProtomapsImageryProvider, - rectangle: Rectangle + _imageryProvider: + | MapboxVectorTileImageryProvider + | ProtomapsImageryProvider, + _rectangle: Rectangle ) { return () => {}; } diff --git a/lib/Models/SearchProviders/StubSearchProvider.ts b/lib/Models/SearchProviders/StubSearchProvider.ts index 6ba2399bff7..304a65ddb88 100644 --- a/lib/Models/SearchProviders/StubSearchProvider.ts +++ b/lib/Models/SearchProviders/StubSearchProvider.ts @@ -30,13 +30,13 @@ export default class StubSearchProvider extends SearchProviderMixin( return StubSearchProvider.type; } - protected logEvent(searchText: string) { + protected logEvent(_searchText: string) { return; } protected doSearch( - searchText: string, - results: SearchProviderResults + _searchText: string, + _results: SearchProviderResults ): Promise { return Promise.resolve(); } diff --git a/lib/Models/ShareDataService.ts b/lib/Models/ShareDataService.ts index 6162148eb40..eae9fb1e3fc 100644 --- a/lib/Models/ShareDataService.ts +++ b/lib/Models/ShareDataService.ts @@ -58,7 +58,7 @@ export default class ShareDataService { try { const result = await loadWithXhr({ - url: this.url, + url: this.url!, method: "POST", data: JSON.stringify(shareData), headers: { "Content-Type": "application/json" }, diff --git a/lib/Models/Terria.ts b/lib/Models/Terria.ts index ea1e1b2fdb5..aff7adf4d8e 100644 --- a/lib/Models/Terria.ts +++ b/lib/Models/Terria.ts @@ -96,8 +96,7 @@ import updateModelFromJson from "./Definition/updateModelFromJson"; import upsertModelFromJson from "./Definition/upsertModelFromJson"; import { ErrorServiceOptions, - ErrorServiceProvider, - initializeErrorServiceProvider + ErrorServiceProvider } from "./ErrorServiceProviders/ErrorService"; import StubErrorServiceProvider from "./ErrorServiceProviders/StubErrorServiceProvider"; import TerriaFeature from "./Feature/Feature"; @@ -210,6 +209,26 @@ export interface ConfigParameters { * True to use Bing Maps from Cesium ion (Cesium World Imagery). By default, Ion will be used, unless the `bingMapsKey` property is specified, in which case that will be used instead. To disable the Bing Maps layers entirely, set this property to false and set `bingMapsKey` to null. */ useCesiumIonBingImagery?: boolean; + /** + * The OAuth2 application ID to use to allow login to Cesium ion on the "Add Data" panel. The referenced application must be configured on + * Cesium ion with a Redirect URI of `[TerriaMap Base URL]/build/TerriaJS/cesium-ion-oauth2.html`. For example, if users access your TerriaJS + * application at `https://example.com/AwesomeMap` then the Redirect URI must be exactly + * `https://example.com/AwesomeMap/build/TerriaJS/cesium-ion-oauth2.html`. + */ + cesiumIonOAuth2ApplicationID?: number; + /** + * Specifies where to store the Cesium ion login token. Valid values are: + * - `page` (default) - The login token is associated with the current page load. Even simply reloading the current page will clear the token. This is the safest option. + * - `sessionStorage` - The login token is associated with a browser session, which means it is shared/accessible from any page hosted on the same domain and running in the same browser tab. + * - `localStorage` - The login token is shared/accessible from any page hosted on the same domain, even when running in different tabs or after exiting and restarted the web browser. + */ + cesiumIonLoginTokenPersistence?: string; + /** + * Whether or not Cesium ion assets added via the "Add Data" panel will be shared with others via share links. If true, users will be asked to select a Cesium ion token when adding assets, + * and this choice must be made carefully to avoid exposing more Cesium ion assets than intended. If false (the default), the user's login token will be used, which is safe because this + * token will not be shared with others. + */ + cesiumIonAllowSharingAddedAssets?: boolean; /** * A [Bing Maps API key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) used for requesting Bing Maps base maps and using the Bing Maps geocoder for searching. It is your responsibility to request a key and comply with all terms and conditions. */ @@ -360,6 +379,7 @@ interface StartOptions { }; applicationUrl?: Location; shareDataService?: ShareDataService; + errorService?: ErrorServiceProvider; /** * i18nOptions is explicitly a separate option from `languageConfiguration`, * as `languageConfiguration` can be serialised, but `i18nOptions` may have @@ -516,6 +536,9 @@ export default class Terria { cesiumTerrainAssetId: undefined, cesiumIonAccessToken: undefined, useCesiumIonBingImagery: undefined, + cesiumIonOAuth2ApplicationID: undefined, + cesiumIonLoginTokenPersistence: "page", + cesiumIonAllowSharingAddedAssets: false, bingMapsKey: undefined, hideTerriaLogo: false, brandBarElements: undefined, @@ -679,8 +702,8 @@ export default class Terria { readonly developmentEnv = process?.env?.NODE_ENV === "development"; /** - * An error service instance. The instance can be configured by setting the - * `errorService` config parameter. Here we initialize it to stub provider so + * An error service instance. The instance can be provided via the + * `errorService` startOption. Here we initialize it to stub provider so * that the `terria.errorService` always exists. */ errorService: ErrorServiceProvider = new StubErrorServiceProvider(); @@ -792,7 +815,7 @@ export default class Terria { } @computed get modelValues() { - return Array.from(this.models.values()); + return Array.from(this.models.values()); } @computed @@ -912,19 +935,6 @@ export default class Terria { this.modelIdShareKeysMap.set(id, [shareKey]); } - /** - * Initialize errorService from config parameters. - */ - setupErrorServiceProvider(errorService: ErrorServiceOptions) { - initializeErrorServiceProvider(errorService) - .then((errorService) => { - this.errorService = errorService; - }) - .catch((e) => { - console.error("Failed to initialize error service", e); - }); - } - setupInitializationUrls(baseUri: uri.URI, config: any) { const initializationUrls: string[] = config?.initializationUrls || []; const initSources: InitSource[] = initializationUrls.map((url) => ({ @@ -1016,10 +1026,6 @@ export default class Terria { if (isJsonObject(config) && isJsonObject(config.parameters)) { this.updateParameters(config.parameters); } - - if (this.configParameters.errorService) { - this.setupErrorServiceProvider(this.configParameters.errorService); - } this.setupInitializationUrls(baseUri, config); }); } catch (error) { @@ -1041,7 +1047,17 @@ export default class Terria { setCustomRequestSchedulerDomainLimits( this.configParameters.customRequestSchedulerLimits ); - + if (options.errorService) { + try { + this.errorService = options.errorService; + this.errorService.init(this.configParameters); + } catch (e) { + console.error( + `Failed to initialize error service: ${this.configParameters.errorService?.provider}`, + e + ); + } + } this.analytics?.start(this.configParameters); this.analytics?.logEvent( Category.launch, diff --git a/lib/Models/Workbench.ts b/lib/Models/Workbench.ts index cce7dbc2686..8aad320b746 100644 --- a/lib/Models/Workbench.ts +++ b/lib/Models/Workbench.ts @@ -56,7 +56,7 @@ export default class Workbench { */ @computed get shouldExpandAll(): boolean { - return this._items.every((item) => !(item as any).isOpenInWorkbench); + return this.items.every((item) => !(item as any).isOpenInWorkbench); } /** @@ -97,7 +97,7 @@ export default class Workbench { */ @action collapseAll() { - this._items.map((item) => { + this.items.map((item) => { item.setTrait(CommonStrata.user, "isOpenInWorkbench", false); }); } @@ -107,7 +107,7 @@ export default class Workbench { */ @action expandAll() { - this._items.map((item) => { + this.items.map((item) => { item.setTrait(CommonStrata.user, "isOpenInWorkbench", true); }); } diff --git a/lib/Models/getToken.js b/lib/Models/getToken.ts similarity index 75% rename from lib/Models/getToken.js rename to lib/Models/getToken.ts index 41c0539cf55..7f792bb51a7 100644 --- a/lib/Models/getToken.js +++ b/lib/Models/getToken.ts @@ -1,12 +1,13 @@ -const defined = require("terriajs-cesium/Source/Core/defined").default; -const loadWithXhr = require("../Core/loadWithXhr"); -const TerriaError = require("../Core/TerriaError").default; -var i18next = require("i18next").default; +import defined from "terriajs-cesium/Source/Core/defined"; +import loadWithXhr from "../Core/loadWithXhr"; +import TerriaError from "../Core/TerriaError"; +import i18next from "i18next"; +import Terria from "./Terria"; -function getToken(terria, tokenUrl, url) { +function getToken(terria: Terria, tokenUrl: string, url: string) { const options = { url: tokenUrl, - method: "POST", + method: "POST" as const, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ url: url @@ -48,4 +49,4 @@ function getToken(terria, tokenUrl, url) { }); } -module.exports = getToken; +export default getToken; diff --git a/lib/Models/markerIcon.js b/lib/Models/markerIcon.js deleted file mode 100644 index 34a51c1c210..00000000000 --- a/lib/Models/markerIcon.js +++ /dev/null @@ -1,2 +0,0 @@ -import markerIcon from "../../wwwroot/images/map-pin.png"; -export default markerIcon; diff --git a/lib/Models/sendFeedback.ts b/lib/Models/sendFeedback.ts index 60bbf6016f7..3338a5522b3 100644 --- a/lib/Models/sendFeedback.ts +++ b/lib/Models/sendFeedback.ts @@ -28,6 +28,7 @@ export default function sendFeedback(options: { raiseError(terria, "`terria.configParameters.feedbackUrl` is not defined"); return; } + const feedbackUrl = terria.configParameters.feedbackUrl; const shareLinkPromise = options.sendShareURL ? canShorten(terria) @@ -56,7 +57,7 @@ export default function sendFeedback(options: { ); } return loadWithXhr({ - url: terria.configParameters.feedbackUrl, + url: feedbackUrl, responseType: "json", method: "POST", data: JSON.stringify(feedbackData), diff --git a/lib/ReactViewModels/DisclaimerHandler.js b/lib/ReactViewModels/DisclaimerHandler.js deleted file mode 100644 index 1ce8eec8111..00000000000 --- a/lib/ReactViewModels/DisclaimerHandler.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict"; - -import defined from "terriajs-cesium/Source/Core/defined"; -import combine from "terriajs-cesium/Source/Core/combine"; - -/** - * Shows disclaimers for {@link CatalogItem}s that are dispatched through {@link Terria#disclaimerEvent} - ensures that - * disclaimers that share the same key (as specified in {@link CatalogItem#initialMessage#key}) are only ever displayed - * once per key - if a single disclaimer is triggered 3 different times, for instance, one disclaimer popup will be - * displayed, and then once that disclaimer is accepted, the callbacks for all three {@Terria#disclaimerEvent} will be - * executed. This response will be persisted in localStorage so that subsequent calls to {@Terria#disclaimerEvent} do - * not display any disclaimer and have their successCallback executed immediately. - * - * Also accepts disclaimers with no key, which will always be displayed, and disclaimers with no need for confirmation - * (as specified by {@link CatalogItem#initialMessage#confirmation}), which will have their successCallback executed - * immediately as the message is shown. - * - * Note that this is not for displaying a general disclaimer on startup. - * - * @param terria The {@link Terria} instance to use. - * @param viewState the viewstate to manipulate in order to display notifications. - * @constructor - */ -export default class DisclaimerHandler { - constructor(terria, viewState) { - this.terria = terria; - this.viewState = viewState; - this._pending = {}; - - this.terria.disclaimerListener = this._handleInitialMessage.bind(this); - } - - dispose() { - this.terria.disclaimerListener = undefined; - } - - /** - * Handles the {@Terria#disclaimerEvent} being raised. Only one disclaimer will be shown for each catalogItem with the - * same {@link CatalogItem#initialMessage#key}, but all calls will have their successCallback executed when it's - * accepted. - * - * @param catalogItem The catalog item to display a disclaimer for. - * @param successCallback A callback to execute once the disclaimer is accepted. - * @private - */ - _handleInitialMessage(catalogItem, successCallback) { - var keySpecified = defined(catalogItem.initialMessage.key); - if ( - keySpecified && - this.terria.getLocalProperty(catalogItem.initialMessage.key) - ) { - successCallback(); - return; - } - - if (catalogItem.initialMessage.confirmation) { - if (keySpecified) { - var key = catalogItem.initialMessage.key; - - if (defined(this._pending[key])) { - this._pending[key].push(successCallback); - } else { - this._pending[key] = [successCallback]; - this._openConfirmationModal( - catalogItem, - this._executeCallbacksForKey.bind(this, key) - ); - } - } else { - this._openConfirmationModal(catalogItem, successCallback); - } - } else { - if (keySpecified) { - this.terria.setLocalProperty(catalogItem.initialMessage.key, true); - } - this._openNotificationModel(catalogItem); - successCallback(); - } - } - - /** - * Opens a confirmation modal for the specified {@link CatalogItem}. - * - * @param catalogItem The catalog item to get disclaimer details from. - * @param callback The callback to execute when the modal is dismissed. - * @private - */ - _openConfirmationModal(catalogItem, callback) { - this.viewState.terria.notificationState.addNotificationToQueue( - combine( - { - confirmAction: callback - }, - DisclaimerHandler._generateOptions(catalogItem) - ) - ); - } - - _openNotificationModel(catalogItem) { - this.viewState.terria.notificationState.addNotificationToQueue( - DisclaimerHandler._generateOptions(catalogItem) - ); - } - - /** - * Executes all the callbacks stored in {@link DisclaimerHandler#pending} for the specified key, and clears that key - * in pending. - * - * @param key The key to get callbacks from. - * @private - */ - _executeCallbacksForKey(key) { - (this._pending[key] || []).forEach(function (cb) { - cb(); - }); - this._pending[key] = undefined; - this.terria.setLocalProperty(key, true); - } - - /** - * Generates options for {@link PopupMessageConfirmationViewModel} and {@link PopupMessageViewModel} for a - * {@link CatalogItem} - * @returns {{title: string, message: string, width: number, height: number, confirmText: string}} - * @private - */ - static _generateOptions(catalogItem) { - return { - title: catalogItem.initialMessage.title, - message: catalogItem.initialMessage.content, - confirmText: catalogItem.initialMessage.confirmText, - width: catalogItem.initialMessage.width, - height: catalogItem.initialMessage.height - }; - } -} diff --git a/lib/ReactViewModels/MouseCoords.ts b/lib/ReactViewModels/MouseCoords.ts index 86e2fcf52ca..3142d87e713 100644 --- a/lib/ReactViewModels/MouseCoords.ts +++ b/lib/ReactViewModels/MouseCoords.ts @@ -9,21 +9,14 @@ import Intersections2D from "terriajs-cesium/Source/Core/Intersections2D"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import Ray from "terriajs-cesium/Source/Core/Ray"; import TerrainProvider from "terriajs-cesium/Source/Core/TerrainProvider"; +import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; import isDefined from "../Core/isDefined"; -import JSEarthGravityModel1996 from "../Map/Vector/EarthGravityModel1996"; import pickTriangle, { PickTriangleResult } from "../Map/Cesium/pickTriangle"; +import EarthGravityModel1996 from "../Map/Vector/EarthGravityModel1996"; import prettifyCoordinates from "../Map/Vector/prettifyCoordinates"; import prettifyProjection from "../Map/Vector/prettifyProjection"; import Terria from "../Models/Terria"; -// TypeScript 3.6.3 can't tell JSEarthGravityModel1996 is a class and reports -// Cannot use namespace 'JSEarthGravityModel1996' as a type.ts(2709) -// This is a dodgy workaround. -class EarthGravityModel1996 extends JSEarthGravityModel1996 {} - -const sampleTerrainMostDetailed = - require("terriajs-cesium/Source/Core/sampleTerrainMostDetailed").default; - interface Cancelable { cancel: () => void; } diff --git a/lib/ReactViewModels/ViewState.ts b/lib/ReactViewModels/ViewState.ts index 77a1975d62c..2c0904ed110 100644 --- a/lib/ReactViewModels/ViewState.ts +++ b/lib/ReactViewModels/ViewState.ts @@ -7,7 +7,7 @@ import { runInAction, makeObservable } from "mobx"; -import { Ref } from "react"; +import React, { Ref } from "react"; import defined from "terriajs-cesium/Source/Core/defined"; import addedByUser from "../Core/addedByUser"; import { @@ -36,7 +36,6 @@ import { RelativePosition, TourPoint } from "./defaultTourPoints"; -import DisclaimerHandler from "./DisclaimerHandler"; import SearchState from "./SearchState"; import CatalogSearchProviderMixin from "../ModelMixins/SearchProviders/CatalogSearchProviderMixin"; import { getMarkerCatalogItem } from "../Models/LocationMarkerUtils"; @@ -110,6 +109,17 @@ export default class ViewState { @observable printWindow: Window | null = null; + /** + * The currently-selected web service type on the My Data -> Add web data panel. + */ + @observable remoteDataType: any | undefined = undefined; + + /** + * The ID of the Cesium ion token that is currently selected on the + * My Data -> Add web data -> Cesium ion panel. + */ + @observable currentCesiumIonToken: string | undefined = undefined; + /** * Toggles ActionBar visibility. Do not set manually, it is * automatically set when rendering @@ -373,7 +383,6 @@ export default class ViewState { private _locationMarkerSubscription: IReactionDisposer; private _workbenchHasTimeWMSSubscription: IReactionDisposer; private _storyBeforeUnloadSubscription: IReactionDisposer; - private _disclaimerHandler: DisclaimerHandler; constructor(options: ViewStateOptions) { makeObservable(this); @@ -449,8 +458,6 @@ export default class ViewState { } ); - this._disclaimerHandler = new DisclaimerHandler(terria, this); - this._workbenchHasTimeWMSSubscription = reaction( () => this.terria.workbench.hasTimeWMS, (hasTimeWMS: boolean) => { @@ -540,7 +547,7 @@ export default class ViewState { this._previewedItemIdSubscription(); this._workbenchHasTimeWMSSubscription(); this._locationMarkerSubscription(); - this._disclaimerHandler.dispose(); + this._storyBeforeUnloadSubscription(); this.searchState.dispose(); } diff --git a/lib/ReactViews/Analytics/DateParameterEditor.jsx b/lib/ReactViews/Analytics/DateParameterEditor.jsx deleted file mode 100644 index 21ba73330af..00000000000 --- a/lib/ReactViews/Analytics/DateParameterEditor.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import defined from "terriajs-cesium/Source/Core/defined"; -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; - -import Styles from "./parameter-editors.scss"; -import CommonStrata from "../../Models/Definition/CommonStrata"; - -const DateParameterEditor = createReactClass({ - displayName: "DateParameterEditor", - - propTypes: { - previewed: PropTypes.object, - parameter: PropTypes.object - }, - - getInitialState() { - return this.getDate(); - }, - - getDate() { - const retDate = { date: "" }; - const strDate = this.props.parameter.value; - if (strDate !== undefined) { - const splits = strDate.split("T"); - retDate["date"] = splits[0]; - } - this.setDate(retDate); - - return retDate; - }, - - setDate(date) { - this.props.parameter.setValue(CommonStrata.user, date["date"]); - }, - - onChangeDate(e) { - const newVal = e.target.value; - const date = this.getDate(); - date["date"] = newVal; - this.setDate(date); - this.setState(date); - }, - - render() { - const style = - defined(this.props.parameter) && defined(this.props.parameter.value) - ? Styles.field - : Styles.fieldDatePlaceholder; - - return ( -
- -
- ); - } -}); - -module.exports = DateParameterEditor; diff --git a/lib/ReactViews/Analytics/DateParameterEditor.tsx b/lib/ReactViews/Analytics/DateParameterEditor.tsx new file mode 100644 index 00000000000..816801327d6 --- /dev/null +++ b/lib/ReactViews/Analytics/DateParameterEditor.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; +import React, { useCallback } from "react"; +import CommonStrata from "../../Models/Definition/CommonStrata"; +import DateParameter from "../../Models/FunctionParameters/DateParameter"; +import Styles from "./parameter-editors.scss"; + +interface DateParameterEditorProps { + parameter: DateParameter; +} + +const DateParameterEditor: React.FC = observer( + ({ parameter }) => { + const style = + parameter?.value !== undefined + ? Styles.field + : Styles.fieldDatePlaceholder; + + const onChangeDate = useCallback( + (e) => parameter.setValue(CommonStrata.user, e.target.value), + [parameter] + ); + + return ( +
+ +
+ ); + } +); + +export default DateParameterEditor; diff --git a/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx b/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx index acadc094e7c..7afa4dc6a8e 100644 --- a/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx +++ b/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx @@ -1,97 +1,36 @@ -import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import { runInAction } from "mobx"; -import moment from "moment"; -import defined from "terriajs-cesium/Source/Core/defined"; -import Styles from "./parameter-editors.scss"; +import React, { useCallback } from "react"; import CommonStrata from "../../Models/Definition/CommonStrata"; import DateTimeParameter from "../../Models/FunctionParameters/DateTimeParameter"; -import Terria from "../../Models/Terria"; +import Styles from "./parameter-editors.scss"; interface DateTimeParameterEditorProps { parameter: DateTimeParameter; - terria: Terria; } -const DateTimeParameterEditor: React.FC = ({ - parameter, - terria -}) => { - const style = - defined(parameter) && defined(parameter.value) - ? Styles.field - : Styles.fieldDatePlaceholder; - - useEffect(() => { - let newDateValue, newTimeValue; - if (parameter.value === undefined) { - const currentTime = defined(parameter.value) - ? parameter.value - : terria.timelineStack.clock.currentTime; - - if (currentTime) { - const ct = new Date(currentTime?.toString()); - - newDateValue = moment - .utc(ct.toISOString()) - .local() - .format("YYYY-MM-DD"); - newTimeValue = moment.utc(ct.toISOString()).local().format("HH:mm"); - } - } else { - const ct = new Date(parameter.value); - newDateValue = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); - newTimeValue = moment.utc(ct.toISOString()).local().format("HH:mm"); - } - parameter.setValue(CommonStrata.user, `${newDateValue}T${newTimeValue}`); - }, [parameter, terria.timelineStack.clock.currentTime]); - - const isValidTime = (time: string): boolean => { - const timeFormat = /^([01]\d|2[0-3]):([0-5]\d)$/; - return timeFormat.test(time); - }; - - const isValidDate = (date: string, format = "YYYY-MM-DD"): boolean => { - return moment(date, format, true).isValid(); - }; - - const handleTimeChange = (e: React.ChangeEvent) => { - const time = e.target.value; - if (isValidTime(time)) { - runInAction(() => { - parameter.setValue(CommonStrata.user, `${dateValue}T${time}`); - }); - } - }; - - const handleDateChange = (e: React.ChangeEvent) => { - if (isValidDate(e.target.value)) { - runInAction(() => { - parameter.setValue(CommonStrata.user, `${e.target.value}T${timeValue}`); - }); - } - }; - - const value = parameter.value; - const dateValue = value ? value.split("T")[0] : ""; - const timeValue = value ? value.split("T")[1] : ""; - - return ( -
- - -
- ); -}; - -export default observer(DateTimeParameterEditor); +const DateTimeParameterEditor: React.FC = + observer(({ parameter }) => { + const style = + parameter?.value !== undefined + ? Styles.field + : Styles.fieldDatePlaceholder; + + const onDateTimeChange = useCallback( + (e: React.ChangeEvent) => + parameter.setValue(CommonStrata.user, e.target.value), + [parameter] + ); + + return ( +
+ +
+ ); + }); + +export default DateTimeParameterEditor; diff --git a/lib/ReactViews/Analytics/InvokeFunction.jsx b/lib/ReactViews/Analytics/InvokeFunction.jsx index b58172ff7ec..2b9d06dee52 100644 --- a/lib/ReactViews/Analytics/InvokeFunction.jsx +++ b/lib/ReactViews/Analytics/InvokeFunction.jsx @@ -117,12 +117,10 @@ class InvokeFunction extends React.Component { return ; } - let invalidParameters = false; - if (defined(this.props.previewed.parameters)) { - invalidParameters = !this.props.previewed.functionParameters.every( - this.validateParameter.bind(this) - ); - } + const invalidParameters = this.props.previewed.functionParameters.some( + (param) => this.validateParameter(param) !== true + ); + const { t } = this.props; return (
diff --git a/lib/ReactViews/Analytics/ParameterEditor.jsx b/lib/ReactViews/Analytics/ParameterEditor.jsx index ba9e9067c51..2b9668bbe58 100644 --- a/lib/ReactViews/Analytics/ParameterEditor.jsx +++ b/lib/ReactViews/Analytics/ParameterEditor.jsx @@ -9,6 +9,7 @@ import PropTypes from "prop-types"; import PointParameterEditor from "./PointParameterEditor"; import LineParameterEditor from "./LineParameterEditor"; import PolygonParameterEditor from "./PolygonParameterEditor"; +import RectangleParameterEditor from "./RectangleParameterEditor"; import RegionParameterEditor from "./RegionParameterEditor"; import RegionTypeParameterEditor from "./RegionTypeParameterEditor"; import BooleanParameterEditor from "./BooleanParameterEditor"; @@ -142,27 +143,27 @@ ParameterEditor.parameterTypeConverters = [ } } }, - // { - // id: "rectangle", - // parameterTypeToDiv: function RectangleParameterToDiv( - // type, - // parameterEditor - // ) { - // if (type === this.id) { - // return ( - //
- // {parameterEditor.renderLabel()} - // - //
- // ); - // } - // } - // }, + { + id: "rectangle", + parameterTypeToDiv: function RectangleParameterToDiv( + type, + parameterEditor + ) { + if (type === this.id) { + return ( +
+ {parameterEditor.renderLabel()} + +
+ ); + } + } + }, { id: "polygon", parameterTypeToDiv: function PolygonParameterToDiv(type, parameterEditor) { diff --git a/lib/ReactViews/Analytics/RectangleParameterEditor.jsx b/lib/ReactViews/Analytics/RectangleParameterEditor.jsx new file mode 100644 index 00000000000..578b57b18cd --- /dev/null +++ b/lib/ReactViews/Analytics/RectangleParameterEditor.jsx @@ -0,0 +1,125 @@ +"use strict"; + +import React from "react"; + +import PropTypes from "prop-types"; + +import defined from "terriajs-cesium/Source/Core/defined"; + +import Styles from "./parameter-editors.scss"; + +import CesiumMath from "terriajs-cesium/Source/Core/Math"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; +import UserDrawing from "../../Models/UserDrawing"; +import { withTranslation } from "react-i18next"; +import { observer } from "mobx-react"; +import { runInAction } from "mobx"; +import CommonStrata from "../../Models/Definition/CommonStrata"; + +@observer +class RectangleParameterEditor extends React.Component { + static propTypes = { + previewed: PropTypes.object, + parameter: PropTypes.object, + viewState: PropTypes.object, + t: PropTypes.func.isRequired + }; + + setValueFromText(e) { + RectangleParameterEditor.setValueFromText(e, this.props.parameter); + } + + selectPolygonOnMap() { + selectOnMap( + this.props.previewed.terria, + this.props.viewState, + this.props.parameter + ); + } + + render() { + const { t } = this.props; + return ( +
+ + +
+ ); + } +} + +/** + * Triggered when user types value directly into field. + * @param {String} e Text that user has entered manually. + * @param {FunctionParameter} parameter Parameter to set value on. + */ +RectangleParameterEditor.setValueFromText = function (e, parameter) { + parameter.setValue(CommonStrata.user, [JSON.parse(e.target.value)]); +}; + +/** + * Given a value, return it in human readable form for display. + * @param {Object} value Native format of parameter value. + * @return {String} String for display + */ +export function getDisplayValue(value) { + if (!defined(value)) { + return ""; + } + return `${value.east}, ${value.north}, ${value.west}, ${value.south}`; +} + +/** + * Prompt user to select/draw on map in order to define parameter. + * @param {Terria} terria Terria instance. + * @param {Object} viewState ViewState. + * @param {FunctionParameter} parameter Parameter. + */ +export function selectOnMap(terria, viewState, parameter) { + const userDrawing = new UserDrawing({ + terria: terria, + drawRectangle: true, + onCleanUp: function () { + viewState.openAddData(); + }, + onDrawingComplete: function (params) { + if (params.points) { + const cartographicPoints = params.points.map((point) => { + const cartographic = Cartographic.fromCartesian( + point, + Ellipsoid.WGS84 + ); + return { + latitude: CesiumMath.toDegrees(cartographic.latitude), + longitude: CesiumMath.toDegrees(cartographic.longitude) + }; + }); + const rectangle = { + west: cartographicPoints[0].longitude, + south: cartographicPoints[0].latitude, + east: cartographicPoints[1].longitude, + north: cartographicPoints[1].latitude + }; + runInAction(() => { + parameter.setValue(CommonStrata.user, rectangle); + }); + } + } + }); + + userDrawing.enterDrawMode(); +} + +export default withTranslation()(RectangleParameterEditor); diff --git a/lib/ReactViews/Analytics/RegionDataParameterEditor.jsx b/lib/ReactViews/Analytics/RegionDataParameterEditor.jsx index ff35de5c814..ac3851207c5 100644 --- a/lib/ReactViews/Analytics/RegionDataParameterEditor.jsx +++ b/lib/ReactViews/Analytics/RegionDataParameterEditor.jsx @@ -7,7 +7,7 @@ import PropTypes from "prop-types"; import defined from "terriajs-cesium/Source/Core/defined"; import knockout from "terriajs-cesium/Source/ThirdParty/knockout"; import VarType from "../../Map/VarType"; -import CatalogItem from "../DataCatalog/CatalogItem"; +import CatalogItem, { ButtonState } from "../DataCatalog/CatalogItem"; import CatalogGroup from "../DataCatalog/CatalogGroup"; import Styles from "./parameter-editors.scss"; @@ -238,7 +238,11 @@ const RegionDataParameterEditor = createReactClass({ selected={this.isActive(catalogItem, column)} text={column.name} onBtnClick={this.toggleActive.bind(this, catalogItem, column)} - btnState={this.isActive(catalogItem, column) ? "remove" : "add"} + btnState={ + this.isActive(catalogItem, column) + ? ButtonState.Remove + : ButtonState.Add + } /> ); } diff --git a/lib/ReactViews/Analytics/parameter-editors.scss b/lib/ReactViews/Analytics/parameter-editors.scss index aaacf5c06cc..8265df2bf9c 100644 --- a/lib/ReactViews/Analytics/parameter-editors.scss +++ b/lib/ReactViews/Analytics/parameter-editors.scss @@ -64,6 +64,11 @@ margin-bottom: $padding; position: relative; clear: both; + + input[type="datetime-local"] { + width: 50%; + } + input[type="date"] { width: 50%; float: left; diff --git a/lib/ReactViews/BottomDock/Timeline/DateFormats.js b/lib/ReactViews/BottomDock/Timeline/DateFormats.ts similarity index 53% rename from lib/ReactViews/BottomDock/Timeline/DateFormats.js rename to lib/ReactViews/BottomDock/Timeline/DateFormats.ts index a8558cf95d4..0026ddc799d 100644 --- a/lib/ReactViews/BottomDock/Timeline/DateFormats.js +++ b/lib/ReactViews/BottomDock/Timeline/DateFormats.ts @@ -1,15 +1,13 @@ -"use strict"; - import defined from "terriajs-cesium/Source/Core/defined"; /** * Formats a date according to the locale if provided, otherwise in a dd/mm/yyyy format. * - * @param {Date} d the date to format - * @param {Locale} [locale] the locale to use for formatting - * @returns {string} A formatted date. + * @param d the date to format + * @param locale the locale to use for formatting + * @returns A formatted date. */ -export function formatDate(d, locale) { +export function formatDate(d: Date, locale?: string): string { if (defined(locale)) { return d.toLocaleDateString(locale); } @@ -19,11 +17,11 @@ export function formatDate(d, locale) { /** * Formats the time according to the locale if provided, otherwise in a hh:mm:ss format. * - * @param {Date} d the date to format - * @param {Locale} [locale] the locale to use for formatting - * @returns {string} A formatted time. + * @param d the date to format + * @param locale the locale to use for formatting + * @returns A formatted time. */ -export function formatTime(d, locale) { +export function formatTime(d: Date, locale?: string): string { if (defined(locale)) { return d.toLocaleTimeString(locale); } @@ -35,20 +33,20 @@ export function formatTime(d, locale) { /** * Combines {@link #formatDate} and {@link #formatTime}. * - * @param {Date} d the date to format - * @param {Locale} [locale] the locale to use for formatting - * @returns {string} A formatted date and time with a comma separating them. + * @param d the date to format + * @param locale the locale to use for formatting + * @returns A formatted date and time with a comma separating them. */ -export function formatDateTime(d, locale) { +export function formatDateTime(d: Date, locale?: string): string { return formatDate(d, locale) + ", " + formatTime(d, locale); } /** * Puts a leading 0 in front of a number of it's less than 10. * - * @param {number} s A number to pad - * @returns {string} A string representing a two-digit number. + * @param s A number to pad + * @returns A string representing a two-digit number. */ -function pad(s) { +function pad(s: number): string { return s < 10 ? "0" + s : `${s}`; } diff --git a/lib/ReactViews/BottomDock/Timeline/Timeline.d.ts b/lib/ReactViews/BottomDock/Timeline/Timeline.d.ts new file mode 100644 index 00000000000..8cd8b6c80c0 --- /dev/null +++ b/lib/ReactViews/BottomDock/Timeline/Timeline.d.ts @@ -0,0 +1,13 @@ +import React from "react"; +import IElementConfig from "../../../Models/IElementConfig"; +import Terria from "../../../Models/Terria"; + +interface PropsType { + terria: Terria; + locale?: unknown; + elementConfig?: IElementConfig; +} + +declare class Timeline extends React.Component {} + +export default Timeline; diff --git a/lib/ReactViews/Clipboard.tsx b/lib/ReactViews/Clipboard.tsx index 19306f063d1..a9f0f60fdd9 100644 --- a/lib/ReactViews/Clipboard.tsx +++ b/lib/ReactViews/Clipboard.tsx @@ -58,6 +58,7 @@ const Clipboard: React.FC = (props) => { removeTimeout(); clipboardBtn.destroy(); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [id]); const isLightTheme = theme === "light"; diff --git a/lib/ReactViews/Custom/ApiTableCustomComponent.ts b/lib/ReactViews/Custom/ApiTableCustomComponent.ts index 816648404f6..20e679d9211 100644 --- a/lib/ReactViews/Custom/ApiTableCustomComponent.ts +++ b/lib/ReactViews/Custom/ApiTableCustomComponent.ts @@ -26,7 +26,7 @@ export default class ApiTableChartCustomComponent extends ChartCustomComponent {} + +export default ChartPanel; diff --git a/lib/ReactViews/Custom/Chart/ZoomX.jsx b/lib/ReactViews/Custom/Chart/ZoomX.jsx index f2eef609237..b3bd31ff5cd 100644 --- a/lib/ReactViews/Custom/Chart/ZoomX.jsx +++ b/lib/ReactViews/Custom/Chart/ZoomX.jsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import { zoom as d3Zoom } from "d3-zoom"; -import { select as d3Select, event as d3Event } from "d3-selection"; +import { select as d3Select } from "d3-selection"; import PropTypes from "prop-types"; import React from "react"; @@ -20,9 +20,9 @@ class ZoomX extends React.Component { this.zoom = d3Zoom() .scaleExtent(props.scaleExtent) .translateExtent(props.translateExtent) - .on("zoom", () => - props.onZoom(d3Event.transform.rescaleX(this.props.initialScale)) - ); + .on("zoom", (event) => { + props.onZoom(event.transform.rescaleX(this.props.initialScale)); + }); } componentDidMount() { diff --git a/lib/ReactViews/Custom/FeedbackLinkCustomComponent.tsx b/lib/ReactViews/Custom/FeedbackLinkCustomComponent.tsx index 8fb74270d6b..173d820e996 100644 --- a/lib/ReactViews/Custom/FeedbackLinkCustomComponent.tsx +++ b/lib/ReactViews/Custom/FeedbackLinkCustomComponent.tsx @@ -71,7 +71,7 @@ export default class FeedbackLinkCustomComponent extends CustomComponent { processNode( context: ProcessNodeContext, node: DomElement, - children: ReactElement[] + _children: ReactElement[] ) { if (!context.viewState) return undefined; diff --git a/lib/ReactViews/Custom/SOSChartCustomComponent.ts b/lib/ReactViews/Custom/SOSChartCustomComponent.ts index 22d5de61051..b73f75eba67 100644 --- a/lib/ReactViews/Custom/SOSChartCustomComponent.ts +++ b/lib/ReactViews/Custom/SOSChartCustomComponent.ts @@ -22,9 +22,9 @@ export default class SOSChartCustomComponent extends ChartCustomComponent this.createItemReference( context.catalogItem as SensorObservationServiceCatalogItem @@ -43,7 +43,7 @@ export default class SOSChartCustomComponent extends ChartCustomComponent terria.configParameters.cesiumIonOAuth2ApplicationID !== undefined, + () => { + addOrReplaceRemoteFileUploadType("cesium-ion", { + value: "cesium-ion", + name: "core.dataType.cesium-ion", + customComponent: CesiumIonConnector + }); + } + ); + } } diff --git a/lib/ReactViews/Disclaimer.d.ts b/lib/ReactViews/Disclaimer.d.ts new file mode 100644 index 00000000000..7c6ae10693c --- /dev/null +++ b/lib/ReactViews/Disclaimer.d.ts @@ -0,0 +1,5 @@ +import React from "react"; + +declare class Disclaimer extends React.Component<{}> {} + +export default Disclaimer; diff --git a/lib/ReactViews/DragDropNotification.d.ts b/lib/ReactViews/DragDropNotification.d.ts new file mode 100644 index 00000000000..33b2a353455 --- /dev/null +++ b/lib/ReactViews/DragDropNotification.d.ts @@ -0,0 +1,5 @@ +import React from "react"; + +declare class DragDropNotification extends React.Component<{}> {} + +export default DragDropNotification; diff --git a/lib/ReactViews/DragDropNotification.jsx b/lib/ReactViews/DragDropNotification.jsx index 63bf30c8c7d..5edcf1a9cfb 100644 --- a/lib/ReactViews/DragDropNotification.jsx +++ b/lib/ReactViews/DragDropNotification.jsx @@ -1,4 +1,3 @@ -"use strict"; import classNames from "classnames"; import { reaction } from "mobx"; import { observer } from "mobx-react"; diff --git a/lib/ReactViews/Errors/RaiseToUserErrorBoundary.tsx b/lib/ReactViews/Errors/RaiseToUserErrorBoundary.tsx index 595f8a9febc..f7fc07c9381 100644 --- a/lib/ReactViews/Errors/RaiseToUserErrorBoundary.tsx +++ b/lib/ReactViews/Errors/RaiseToUserErrorBoundary.tsx @@ -6,6 +6,7 @@ type PropsType = { viewState: ViewState; // Pass in options to customize the title and other presentation aspects of the error terriaErrorOptions?: TerriaErrorOverrides; + children?: React.ReactNode; }; /** @@ -14,13 +15,13 @@ type PropsType = { export default class RaiseToUserErrorBoundary extends React.Component { state = { hasError: false }; - static getDerivedStateFromError(error: any) { + static getDerivedStateFromError(_error: Error) { return { hasError: true }; } - componentDidCatch(error: Error, errorInfo: ErrorInfo) { + componentDidCatch(error: Error, _errorInfo: ErrorInfo) { this.props.viewState.terria.raiseErrorToUser( error, this.props.terriaErrorOptions diff --git a/lib/ReactViews/ExplorerWindow/ModalPopup.tsx b/lib/ReactViews/ExplorerWindow/ModalPopup.tsx index 2f5e991d476..1f05b8117f8 100644 --- a/lib/ReactViews/ExplorerWindow/ModalPopup.tsx +++ b/lib/ReactViews/ExplorerWindow/ModalPopup.tsx @@ -50,6 +50,7 @@ const ModalPopup: React.FC = (props) => { } else { slideOut(); } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.isVisible]); useEffect(() => { diff --git a/lib/ReactViews/ExplorerWindow/Tabs.d.ts b/lib/ReactViews/ExplorerWindow/Tabs.d.ts new file mode 100644 index 00000000000..8714b383e1c --- /dev/null +++ b/lib/ReactViews/ExplorerWindow/Tabs.d.ts @@ -0,0 +1,13 @@ +import React from "react"; +import Terria from "../../Models/Terria"; +import ViewState from "../../ReactViewModels/ViewState"; + +interface PropsType { + terria: Terria; + viewState: ViewState; + tabs?: unknown[]; +} + +declare class Tabs extends React.Component {} + +export default Tabs; diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/AddData.jsx b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/AddData.jsx index 6998dd16595..bd9d73ce2fc 100644 --- a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/AddData.jsx +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/AddData.jsx @@ -1,4 +1,5 @@ -import createReactClass from "create-react-class"; +import { observer } from "mobx-react"; +import { runInAction } from "mobx"; import PropTypes from "prop-types"; import React from "react"; import { Trans, withTranslation } from "react-i18next"; @@ -26,10 +27,9 @@ import TerriaError from "../../../../Core/TerriaError"; /** * Add data panel in modal window -> My data tab */ -const AddData = createReactClass({ - displayName: "AddData", - - propTypes: { +@observer +class AddData extends React.Component { + static propTypes = { terria: PropTypes.object, viewState: PropTypes.object, resetTab: PropTypes.func, @@ -42,9 +42,11 @@ const AddData = createReactClass({ onFileAddFinished: PropTypes.func.isRequired, onUrlAddFinished: PropTypes.func.isRequired, t: PropTypes.func.isRequired - }, + }; + + constructor(props) { + super(props); - getInitialState() { const remoteDataTypes = this.props.remoteDataTypes ?? getDataType().remoteDataType; @@ -58,27 +60,26 @@ const AddData = createReactClass({ return { ...dataType, name: `${dataType.name}${extensions}` }; }); - return { + this.state = { remoteDataTypes, localDataTypes, - remoteDataType: remoteDataTypes[0], // By default select the first item (auto) localDataType: localDataTypes[0], remoteUrl: "", // By default there's no remote url isLoading: false }; - }, + } selectLocalOption(option) { this.setState({ localDataType: option }); - }, + } selectRemoteOption(option) { - this.setState({ - remoteDataType: option + runInAction(() => { + this.props.viewState.remoteDataType = option; }); - }, + } handleUploadFile(e) { this.setState({ @@ -99,7 +100,7 @@ const AddData = createReactClass({ // reset active tab when file handling is done this.props.resetTab(); }); - }, + } async handleUrl(e) { const url = this.state.remoteUrl; @@ -113,14 +114,17 @@ const AddData = createReactClass({ isLoading: true }); let promise; - if (this.state.remoteDataType.value === "auto") { + if ( + !this.props.viewState.remoteDataType || + this.props.viewState.remoteDataType.value === "auto" + ) { promise = createCatalogItemFromFileOrUrl( this.props.terria, this.props.viewState, this.state.remoteUrl, - this.state.remoteDataType.value + this.props.viewState.remoteDataType?.value ); - } else if (this.state.remoteDataType.value === "json") { + } else if (this.props.viewState.remoteDataType.value === "json") { promise = loadJson(this.state.remoteUrl) .then((data) => { if (data.error) { @@ -151,7 +155,7 @@ const AddData = createReactClass({ this.props.terria, "", CommonStrata.defaults, - { type: this.state.remoteDataType.value, name: url }, + { type: this.props.viewState.remoteDataType.value, name: url }, {} ).throwIfUndefined({ message: `An error occurred trying to add data from URL: ${url}` @@ -183,13 +187,13 @@ const AddData = createReactClass({ }); this.props.resetTab(); }); - }, + } onRemoteUrlChange(event) { this.setState({ remoteUrl: event.target.value }); - }, + } renderPanels() { const { t } = this.props; @@ -214,6 +218,9 @@ const AddData = createReactClass({ }, []); + const remoteDataType = + this.props.viewState.remoteDataType ?? this.state.remoteDataTypes[0]; + return (
{this.props.activeTab === "local" && ( @@ -228,7 +235,7 @@ const AddData = createReactClass({ @@ -244,7 +251,7 @@ const AddData = createReactClass({ {this.state.isLoading && } @@ -261,51 +268,65 @@ const AddData = createReactClass({ - {this.state.remoteDataType?.description + {remoteDataType?.description ? parseCustomMarkdownToReactWithOptions( - this.state.remoteDataType?.description + remoteDataType?.description ) : null} - -
- - - {this.state.isLoading && } - + {remoteDataType?.customComponent + ? this.renderCustomComponent(remoteDataType?.customComponent) + : this.renderDefaultForWebDataType(t)} )}
); - }, + } + + renderCustomComponent(CustomComponent) { + return ; + } + + renderDefaultForWebDataType(t) { + return ( + <> + +
+ + + {this.state.isLoading && } + + + ); + } render() { return
{this.renderPanels()}
; } -}); +} /** * @param extensions - string[] diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/CesiumIonConnector.tsx b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/CesiumIonConnector.tsx new file mode 100644 index 00000000000..a19f0539fdf --- /dev/null +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/CesiumIonConnector.tsx @@ -0,0 +1,724 @@ +import React from "react"; +import { observer } from "mobx-react"; +import URI from "urijs"; +import { string } from "prop-types"; +import { Trans, useTranslation } from "react-i18next"; +import AddDataStyles from "./add-data.scss"; +import Styles from "./cesium-ion-connector.scss"; +import upsertModelFromJson from "../../../../Models/Definition/upsertModelFromJson"; +import CatalogMemberFactory from "../../../../Models/Catalog/CatalogMemberFactory"; +import CommonStrata from "../../../../Models/Definition/CommonStrata"; +import addUserCatalogMember from "../../../../Models/Catalog/addUserCatalogMember"; +import Dropdown from "../../../Generic/Dropdown"; +import Icon from "../../../../Styled/Icon"; +import classNames from "classnames"; +import { RawButton } from "../../../../Styled/Button"; +import styled from "styled-components"; +import { useViewState } from "../../../Context"; +import TimeVarying from "../../../../ModelMixins/TimeVarying"; +import isDefined from "../../../../Core/isDefined"; +import Terria from "../../../../Models/Terria"; + +interface CesiumIonToken { + id?: string; + name?: string; + uniqueName?: string; // we populate this one ourselves + token?: string; + dateAdded?: string; + dateModified?: string; + dateLastUsed?: string; + assetIds?: number[]; + isDefault?: boolean; + allowedUrls?: string[]; + scopes?: string[]; +} + +interface CesiumIonAsset { + id?: number; + name?: string; + uniqueName?: string; // we populate this one ourselves + description?: string; + type?: string; + bytes?: number; + dateAdded?: string; + status?: string; + percentComplete?: number; + archivable?: boolean; + exportable?: boolean; +} + +const ActionButton = styled(RawButton)` + svg { + height: 20px; + width: 20px; + margin: 5px; + fill: ${(p) => p.theme.charcoalGrey}; + } + + &:hover, + &:focus { + svg { + fill: ${(p) => p.theme.modalHighlight}; + } + } +`; + +/** + * An interface getting and setting the user's Cesium ion login token obtained via OAuth2. + */ +interface LoginTokenPersistence { + get(): string | null; + set(token: string): void; + clear(): void; +} + +class LoginTokenPersistenceInLocalStorage implements LoginTokenPersistence { + private storageName = "cesium-ion-login-token"; + + public get(): string | null { + return localStorage.getItem(this.storageName) ?? ""; + } + + public set(token: string) { + localStorage.setItem(this.storageName, token); + } + + public clear() { + localStorage.removeItem(this.storageName); + } +} + +class LoginTokenPersistenceInSessionStorage implements LoginTokenPersistence { + private storageName = "cesium-ion-login-token"; + + public get(): string | null { + return sessionStorage.getItem(this.storageName) ?? ""; + } + + public set(token: string) { + sessionStorage.setItem(this.storageName, token); + } + + public clear() { + sessionStorage.removeItem(this.storageName); + } +} + +class LoginTokenPersistenceInPage implements LoginTokenPersistence { + private loginToken: string | null = null; + + public get(): string | null { + return this.loginToken; + } + + public set(token: string) { + this.loginToken = token; + } + + public clear() { + this.loginToken = null; + } +} + +const loginTokenPersistenceTypes = { + page: new LoginTokenPersistenceInPage(), + sessionStorage: new LoginTokenPersistenceInSessionStorage(), + localStorage: new LoginTokenPersistenceInLocalStorage() +}; + +const loginTokenPersistenceLookup: { + [key: string]: LoginTokenPersistence | undefined; +} = loginTokenPersistenceTypes; + +const defaultUserProfile = { + id: 0, + scopes: [], + username: "", + email: "", + emailVerified: false, + avatar: string, + storage: {} +}; + +function CesiumIonConnector() { + const viewState = useViewState(); + const { t } = useTranslation(); + + const loginTokenPersistence = + loginTokenPersistenceLookup[ + viewState.terria.configParameters.cesiumIonLoginTokenPersistence ?? "" + ] ?? loginTokenPersistenceTypes.page; + + const [codeChallenge, setCodeChallenge] = React.useState({ + value: "", + hash: "" + }); + + const [tokens, setTokens] = React.useState([]); + const [isLoadingTokens, setIsLoadingTokens] = React.useState(false); + const [assets, setAssets] = React.useState([]); + const [isLoadingAssets, setIsLoadingAssets] = React.useState(false); + + // This is the Cesium ion token representing the currently logged-in user, as obtained via + // an OAuth2 Authorization Code Grant flow with Cesium ion. + const [loginToken, setLoginToken] = React.useState( + loginTokenPersistence.get() ?? "" + ); + + const [userProfile, setUserProfile] = React.useState(defaultUserProfile); + const [isLoadingUserProfile, setIsLoadingUserProfile] = + React.useState(false); + + React.useEffect(() => { + if (!crypto || !crypto.subtle) return; + + const codeChallenge = [...crypto.getRandomValues(new Uint8Array(32))] + .map((x) => x.toString(16).padStart(2, "0")) + .join(""); + + crypto.subtle + .digest("SHA-256", new TextEncoder().encode(codeChallenge)) + .then((hash) => { + setCodeChallenge({ + value: codeChallenge, + hash: btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/[=]/g, "") + }); + }); + }, []); + + React.useEffect(() => { + if (loginToken.length === 0) return; + + setIsLoadingUserProfile(true); + + fetch("https://api.cesium.com/v1/me", { + headers: { + Authorization: `Bearer ${loginToken}` + } + }) + .then((response) => { + return response.json(); + }) + .then((profile) => { + if (profile && profile.username) { + setUserProfile(profile); + } else { + setUserProfile(defaultUserProfile); + } + setIsLoadingUserProfile(false); + }); + }, [loginToken]); + + React.useEffect(() => { + if (loginToken.length === 0) return; + + setIsLoadingAssets(true); + + fetch("https://api.cesium.com/v1/assets", { + headers: { + Authorization: `Bearer ${loginToken}` + } + }) + .then((response) => response.json()) + .then((assets: { items: CesiumIonAsset[] }) => { + if (assets.items) { + assets.items.forEach((item) => { + item.uniqueName = `${item.name} (${item.id})`; + }); + setAssets(assets.items); + } + + setIsLoadingAssets(false); + }); + }, [loginToken]); + + React.useEffect(() => { + if ( + !viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets || + loginToken.length === 0 + ) { + return; + } + + setIsLoadingTokens(true); + + fetch("https://api.cesium.com/v2/tokens", { + headers: { + Authorization: `Bearer ${loginToken}` + } + }) + .then((response) => response.json()) + .then((tokens: { items: CesiumIonToken[] }) => { + if (tokens.items) { + tokens.items.forEach((item) => { + item.uniqueName = `${item.name} (${item.id})`; + }); + setTokens(tokens.items); + } + setIsLoadingTokens(false); + }); + }, [ + loginToken, + viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets + ]); + + let selectedToken = viewState.currentCesiumIonToken + ? tokens.find((token) => token.id === viewState.currentCesiumIonToken) + : undefined; + + if (selectedToken === undefined && tokens.length > 0) { + selectedToken = tokens[0]; + } + + const setSelectedToken = (token: CesiumIonToken) => { + viewState.currentCesiumIonToken = token.id; + }; + + if (!crypto || !crypto.subtle) { + return ( + + ); + } + + const dropdownTheme = { + list: AddDataStyles.dropdownList, + icon: , + dropdown: Styles.dropDown, + button: Styles.dropDownButton + }; + + return ( + <> + + {loginToken.length > 0 + ? renderConnectedOrConnecting() + : renderDisconnected()} + + ); + + function renderConnectedOrConnecting() { + return ( + <> + {isLoadingUserProfile ? ( + + ) : ( + + )} + + {userProfile.username.length > 0 && renderConnected()} + + ); + } + + function renderConnected() { + const isAssetAccessibleBySelectedToken = (asset: CesiumIonAsset) => { + if (!selectedToken) { + if (viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets) + return false; + else return true; + } + + if (asset.id === undefined) return true; + + if (selectedToken.assetIds === undefined) { + // Token allows access to all assets + return true; + } + + return selectedToken.assetIds.indexOf(asset.id) >= 0; + }; + + return ( + <> + {renderTokenSelector()} + {isLoadingAssets ? ( + + ) : ( +
+ + + + + + {assets + .filter(isAssetAccessibleBySelectedToken) + .map(renderAssetRow)} + +
+ NameType
+ )} + + ); + } + + function renderTokenSelector() { + if (!viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets) + return undefined; + + return ( + <> + + {isLoadingTokens ? ( + + ) : ( + + )} +
+ {renderTokenWarning()} +
+ + ); + } + + function renderDisconnected() { + return ( +
+ +
+ +
+
+ ); + } + + function renderTokenWarning() { + if (!selectedToken) return; + + const dangerousScopes = []; + for (const scope of selectedToken.scopes ?? []) { + // Only these scopes are "safe". + if (scope !== "assets:read" && scope !== "geocode") { + dangerousScopes.push(scope); + } + } + + if (dangerousScopes.length > 0) { + return ( + <> + DO NOT USE THIS TOKEN! It allows access to your{" "} +
+ Cesium ion account + {" "} + using the following scopes that provide potentially sensitive + information or allow changes to be made to your account:{" "} + {dangerousScopes.join(", ")} + + ); + } + + if (!siteMatchesAllowedUrls(selectedToken)) { + return ( + <> + This token cannot be used with this map because the map is not in the + token's list of allowed URLs in your{" "} + + Cesium ion account + + . + + ); + } + + let numberOfAssetsAccessible = -1; + if (selectedToken.assetIds) { + numberOfAssetsAccessible = selectedToken.assetIds.length; + } + + return ( + <> + This token allows access to{" "} + + {numberOfAssetsAccessible < 0 ? "every" : numberOfAssetsAccessible} + {" "} + {numberOfAssetsAccessible <= 1 ? "asset" : "assets"} in your{" "} + + Cesium ion account + + . + + ); + } + + function renderAssetRow(asset: CesiumIonAsset) { + return ( + + + + + + + {asset.name} + {asset.type} + + ); + } + + function connect() { + const clientID = + viewState.terria.configParameters.cesiumIonOAuth2ApplicationID; + const redirectUri = URI("build/TerriaJS/cesium-ion-oauth2.html") + .absoluteTo(window.location.href) + .fragment("") + .query("") + .toString(); + + const codeChallengeValue = codeChallenge.value; + const codeChallengeHash = codeChallenge.hash; + + const state = [...crypto.getRandomValues(new Uint8Array(16))] + .map((x) => x.toString(16).padStart(2, "0")) + .join(""); + + const uri = new URI("https://ion.cesium.com/oauth").addQuery({ + response_type: "code", + client_id: clientID, + scope: "assets:read assets:list tokens:read profile:read", + redirect_uri: redirectUri, + state: state, + code_challenge: codeChallengeHash, + code_challenge_method: "S256" + }); + + (window as any)["cesiumIonOAuth2_" + state] = function (code: string) { + fetch("https://api.cesium.com/oauth/token", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + grant_type: "authorization_code", + client_id: clientID, + code: code, + redirect_uri: redirectUri, + code_verifier: codeChallengeValue + }) + }) + .then((response) => { + return response.json(); + }) + .then((response) => { + loginTokenPersistence.set(response.access_token); + setLoginToken(response.access_token ?? ""); + }); + }; + + window.open(uri.toString(), "_blank", "popup=yes,width=600,height=600"); + } + + function disconnect() { + loginTokenPersistence.clear(); + setLoginToken(""); + setUserProfile(defaultUserProfile); + } + + function addToMap( + terria: Terria, + asset: CesiumIonAsset, + event: React.MouseEvent + ) { + // If the asset may be shared, the user must choose a suitable token. + // If not, we can access the asset using the login token. + const allowSharing = + viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets; + const assetToken = allowSharing + ? selectedToken + : { token: loginToken, name: `${userProfile.username}'s login token` }; + if (!assetToken) return; + + const definition = createCatalogItemDefinitionFromAsset( + terria, + asset, + assetToken + ); + if (!definition) return; + + const newItem = upsertModelFromJson( + CatalogMemberFactory, + viewState.terria, + "", + terria.configParameters.cesiumIonAllowSharingAddedAssets + ? CommonStrata.user + : CommonStrata.definition, + definition, + {} + ).throwIfUndefined({ + message: `An error occurred trying to add asset: ${asset.name}` + }); + + const keepCatalogOpen = event.shiftKey || event.ctrlKey; + + addUserCatalogMember(viewState.terria, Promise.resolve(newItem)).then( + (addedItem) => { + if (addedItem) { + if (TimeVarying.is(addedItem)) { + viewState.terria.timelineStack.addToTop(addedItem); + } + } + + if (!keepCatalogOpen) { + viewState.closeCatalog(); + } + } + ); + } + + function createCatalogItemDefinitionFromAsset( + terria: Terria, + asset: CesiumIonAsset, + token: CesiumIonToken + ) { + let type = ""; + const extras: any = {}; + switch (asset.type) { + case "3DTILES": + type = "3d-tiles"; + break; + case "GLTF": + type = "gltf"; + // TODO + extras.origin = { + longitude: 0.0, + latitude: 0.0, + height: 0.0 + }; + break; + case "IMAGERY": + type = "ion-imagery"; + break; + case "TERRAIN": + type = "cesium-terrain"; + break; + case "CZML": + type = "czml"; + break; + case "KML": + type = "kml"; + break; + case "GEOJSON": + type = "geojson"; + break; + } + + if (type === "") return undefined; + + return { + name: asset.name ?? "Unnamed", + type: type, + shareable: + terria.configParameters.cesiumIonAllowSharingAddedAssets ?? false, + description: asset.description ?? "", + ionAssetId: asset.id ?? 0, + ionAccessToken: token.token, + info: [ + { + name: "Cesium ion Account", + content: userProfile.username + }, + { + name: "Cesium ion Token", + content: token.name ?? token.id + } + ], + ...extras + }; + } +} + +function siteMatchesAllowedUrls(token: CesiumIonToken) { + if (!isDefined(token.allowedUrls)) { + return true; + } + + const current = new URI(window.location.href); + + for (const allowedUrl of token.allowedUrls) { + let allowed; + + try { + allowed = new URI(allowedUrl); + } catch (e) { + continue; + } + + const currentHostname = current.hostname(); + const allowedHostname = allowed.hostname(); + + // Current hostname must either match the allowed one exactly, or be a subdomain of the allowed one. + const hostnameValid = + currentHostname === allowedHostname || + (currentHostname.endsWith(allowedHostname) && + currentHostname[currentHostname.length - allowedHostname.length - 1] === + "."); + if (!hostnameValid) continue; + + // If the current has a port, the allowed must match. + if (current.port().length > 0 && current.port() !== allowed.port()) + continue; + + // The current path must start with the allowed path. + if (!current.path().startsWith(allowed.path())) continue; + + return true; + } + + return false; +} + +const CesiumIonConnectorObserver = observer(CesiumIonConnector); +export default CesiumIonConnectorObserver; diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/add-data.scss b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/add-data.scss index 10ac31ca230..7a572699645 100644 --- a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/add-data.scss +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/add-data.scss @@ -4,11 +4,12 @@ @include empty-module("inner"); .tab-panels { - width: 50%; + width: fit-content; + min-width: 50%; max-width: 600px; margin: 3vh auto; text-align: left; - padding-top: 10vh; + //padding-top: 10vh; } .tab-panel { text-align: left; @@ -59,6 +60,7 @@ .dropdown { &__list { height: 250px; + overflow-y: visible; } @media (max-height: 800px) { &__list { diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss new file mode 100644 index 00000000000..972d3134962 --- /dev/null +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss @@ -0,0 +1,35 @@ +@import "~terriajs-variables"; +@import "../../../../Sass/common/mixins"; + +.connect-button { + composes: btn from "../../../../Sass/common/_buttons.scss"; + composes: btn-transparent from "../../../../Sass/common/_buttons.scss"; + composes: btn-primary from "../../../../Sass/common/_buttons.scss"; + width: 200px; +} + +.token-warning { + color: $warning; + font-family: $font-base; + font-size: 0.9em; +} + +.token-warning-hidden { + visibility: hidden; +} + +.drop-down { + height: 40px; +} + +.drop-down-button { + height: 100%; +} + +.assets-list { + overflow-y: auto; +} + +.add-asset-button { + vertical-align: middle; +} diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss.d.ts b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss.d.ts new file mode 100644 index 00000000000..ddc99df9ca5 --- /dev/null +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/cesium-ion-connector.scss.d.ts @@ -0,0 +1,20 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'add-asset-button': string; + 'addAssetButton': string; + 'assets-list': string; + 'assetsList': string; + 'connect-button': string; + 'connectButton': string; + 'drop-down': string; + 'drop-down-button': string; + 'dropDown': string; + 'dropDownButton': string; + 'token-warning': string; + 'token-warning-hidden': string; + 'tokenWarning': string; + 'tokenWarningHidden': string; +} +declare var cssExports: CssExports; +export = cssExports; diff --git a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/my-data-tab.scss b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/my-data-tab.scss index a91d093dfec..f61a3073fcb 100644 --- a/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/my-data-tab.scss +++ b/lib/ReactViews/ExplorerWindow/Tabs/MyDataTab/my-data-tab.scss @@ -29,7 +29,7 @@ margin: 15px; height: calc(100% - 30px); width: calc(100% - 30px); - overflow: hidden; + overflow: auto; background: $modal-secondary-bg; text-align: center; color: $dark-lighter; diff --git a/lib/ReactViews/FeatureInfo/FeatureInfoDownload.tsx b/lib/ReactViews/FeatureInfo/FeatureInfoDownload.tsx index 437ed11cab6..6cc12daafbe 100644 --- a/lib/ReactViews/FeatureInfo/FeatureInfoDownload.tsx +++ b/lib/ReactViews/FeatureInfo/FeatureInfoDownload.tsx @@ -8,8 +8,7 @@ import ViewState from "../../ReactViewModels/ViewState"; import Icon from "../../Styled/Icon"; import { withViewState } from "../Context"; import Styles from "./feature-info-download.scss"; - -const Dropdown = require("../Generic/Dropdown"); +import Dropdown from "../Generic/Dropdown"; class FeatureInfoDownload extends React.Component<{ data: JsonObject; @@ -55,7 +54,6 @@ class FeatureInfoDownload extends React.Component<{ button: Styles.dropdownButton, icon: icon }} - buttonClassName={Styles.btn} > {t("featureInfo.download")} diff --git a/lib/ReactViews/FeatureInfo/FeatureInfoSection.tsx b/lib/ReactViews/FeatureInfo/FeatureInfoSection.tsx index 2b1a2bcf3ec..da5e71a2d5c 100644 --- a/lib/ReactViews/FeatureInfo/FeatureInfoSection.tsx +++ b/lib/ReactViews/FeatureInfo/FeatureInfoSection.tsx @@ -144,7 +144,7 @@ export class FeatureInfoSection extends React.Component { this.removeFeatureChangedSubscription?.(); this.removeFeatureChangedSubscription = feature.definitionChanged.addEventListener( - ((changedFeature: TerriaFeature) => { + ((_changedFeature: TerriaFeature) => { runInAction(() => { this.featureChangedCounter++; }); diff --git a/lib/ReactViews/FeatureInfo/getFeatureProperties.ts b/lib/ReactViews/FeatureInfo/getFeatureProperties.ts index 7a2e6b7a318..a2e60d1300a 100644 --- a/lib/ReactViews/FeatureInfo/getFeatureProperties.ts +++ b/lib/ReactViews/FeatureInfo/getFeatureProperties.ts @@ -84,7 +84,9 @@ function parseValues(properties: JsonObject) { try { val = JSON.parse(val as string); } catch (e) { - return; + console.warn( + `Error parsing JSON in feature properties "${key}": ${e}\nWill use string value instead: "${val}"` + ); } } result[key] = val; diff --git a/lib/ReactViews/FeatureInfo/mustacheExpressions.ts b/lib/ReactViews/FeatureInfo/mustacheExpressions.ts index cd48fb66418..43613ad0b88 100644 --- a/lib/ReactViews/FeatureInfo/mustacheExpressions.ts +++ b/lib/ReactViews/FeatureInfo/mustacheExpressions.ts @@ -87,7 +87,7 @@ export function mustacheRenderPartialByName( templateData: object ): MustacheFunction { return () => { - return mustacheJsonSubOptions((value, options) => { + return mustacheJsonSubOptions((value, _options) => { if (!isJsonString(value)) return `${value}`; if (partials && typeof partials[value] === "string") { return Mustache.render(partials[value], templateData); diff --git a/lib/ReactViews/Generic/CloseButton.tsx b/lib/ReactViews/Generic/CloseButton.tsx new file mode 100644 index 00000000000..f38f209e7bf --- /dev/null +++ b/lib/ReactViews/Generic/CloseButton.tsx @@ -0,0 +1,34 @@ +// for all the panels and modals we will eventually normalise +import React from "react"; +import styled from "styled-components"; +// import Box from "../../Styled/Box"; +import Icon from "../../Styled/Icon"; +import { ButtonProps, RawButton } from "../../Styled/Button"; + +const StyledCloseButton = styled(RawButton)` + ${(p: any) => !p.noAbsolute && `position: absolute;`} + // width: 20px; + // height: 20px; + width: 14px; + height: 14px; + ${(p: any) => + p.topRight && + ` + top: 15px; + right:15px; + `} + svg { + // fill: ${(p) => p.color || p.theme.darkWithOverlay}; + fill: ${(p) => p.color}; + } +`; + +const CloseButton = (props: ButtonProps) => { + return ( + + + + ); +}; + +export default CloseButton; diff --git a/lib/ReactViews/Generic/Dropdown.d.ts b/lib/ReactViews/Generic/Dropdown.d.ts new file mode 100644 index 00000000000..a1ea178b042 --- /dev/null +++ b/lib/ReactViews/Generic/Dropdown.d.ts @@ -0,0 +1,16 @@ +import React from "react"; + +interface DropdownProps { + theme?: any; + options?: any[]; + selected?: any; + selectOption?: (o: any, i: number) => void; + textProperty?: string; + matchWidth?: boolean; + children?: any; + disabled?: boolean; +} + +declare class Dropdown extends React.Component {} + +export default Dropdown; diff --git a/lib/ReactViews/Generic/Dropdown.jsx b/lib/ReactViews/Generic/Dropdown.jsx index 8fa71647881..427107c16ad 100644 --- a/lib/ReactViews/Generic/Dropdown.jsx +++ b/lib/ReactViews/Generic/Dropdown.jsx @@ -175,4 +175,4 @@ const Dropdown = createReactClass({ } }); -module.exports = Dropdown; +export default Dropdown; diff --git a/lib/ReactViews/Generic/Prompt.d.ts b/lib/ReactViews/Generic/Prompt.d.ts new file mode 100644 index 00000000000..29a61a8cca6 --- /dev/null +++ b/lib/ReactViews/Generic/Prompt.d.ts @@ -0,0 +1,20 @@ +import React from "react"; + +interface PropsType { + content: React.ReactNode; + isVisible: boolean; + displayDelay: number; + dismissText: string; + dismissAction: () => void; + centered?: boolean; + caretTopOffset?: number; + caretLeftOffset?: number; + caretSize?: number; + promptWidth?: number; + promptTopOffset?: number; + promptLeftOffset?: number; +} + +declare class Prompt extends React.PureComponent {} + +export default Prompt; diff --git a/lib/ReactViews/Generic/Timer/drawTimer.js b/lib/ReactViews/Generic/Timer/drawTimer.js index c1f89ebdf76..0d8fef7a8f3 100644 --- a/lib/ReactViews/Generic/Timer/drawTimer.js +++ b/lib/ReactViews/Generic/Timer/drawTimer.js @@ -155,7 +155,7 @@ export function createTimer( * @param {string} containerId The id of the element to insert the timer into. * @param {string} elapsedTimeClass A class for styling the animation that fills the timer as it runs. * @param {string} backgroundClass A class for styling the timer's background circle. - * @param {string} [elapsed=0] How much time (in seconds) has already passed. + * @param {number} [elapsed=0] How much time (in seconds) has already passed. */ export function startTimer( radius, diff --git a/lib/ReactViews/HelpScreens/SatelliteHelpPrompt.jsx b/lib/ReactViews/HelpScreens/SatelliteHelpPrompt.tsx similarity index 100% rename from lib/ReactViews/HelpScreens/SatelliteHelpPrompt.jsx rename to lib/ReactViews/HelpScreens/SatelliteHelpPrompt.tsx diff --git a/lib/ReactViews/Hooks/useRefForTerria.ts b/lib/ReactViews/Hooks/useRefForTerria.ts index 6045bf11063..3a0a2213842 100644 --- a/lib/ReactViews/Hooks/useRefForTerria.ts +++ b/lib/ReactViews/Hooks/useRefForTerria.ts @@ -17,6 +17,7 @@ export function useRefForTerria( return function removeRefFromTerria() { viewState.deleteAppRef(refName); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [ref]); return ref; } diff --git a/lib/ReactViews/Hooks/useWindowSize.ts b/lib/ReactViews/Hooks/useWindowSize.ts index 35a005de815..34dc84c8ddc 100644 --- a/lib/ReactViews/Hooks/useWindowSize.ts +++ b/lib/ReactViews/Hooks/useWindowSize.ts @@ -31,6 +31,7 @@ export function useWindowSize({ debounceOverride }: UseWindowSizeOptions) { return () => { window.removeEventListener("resize", resizeListener); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); return width; diff --git a/lib/ReactViews/Map/BottomBar/Credits/MapCredits.tsx b/lib/ReactViews/Map/BottomBar/Credits/MapCredits.tsx index 5800d416b4b..fa4637a80d4 100644 --- a/lib/ReactViews/Map/BottomBar/Credits/MapCredits.tsx +++ b/lib/ReactViews/Map/BottomBar/Credits/MapCredits.tsx @@ -42,6 +42,7 @@ export const MapCredits: FC = observer( } } ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [currentViewer]); if (currentViewer.type === "none") { diff --git a/lib/ReactViews/Map/BottomBar/DistanceLegend.tsx b/lib/ReactViews/Map/BottomBar/DistanceLegend.tsx index e59651efb26..4b5f4dd7456 100644 --- a/lib/ReactViews/Map/BottomBar/DistanceLegend.tsx +++ b/lib/ReactViews/Map/BottomBar/DistanceLegend.tsx @@ -7,7 +7,6 @@ import { useTheme } from "styled-components"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import EllipsoidGeodesic from "terriajs-cesium/Source/Core/EllipsoidGeodesic"; import CesiumEvent from "terriajs-cesium/Source/Core/Event"; -import getTimestamp from "terriajs-cesium/Source/Core/getTimestamp"; import Scene from "terriajs-cesium/Source/Scene/Scene"; import isDefined from "../../../Core/isDefined"; import Box from "../../../Styled/Box"; @@ -42,7 +41,7 @@ export const DistanceLegend: FC = observer( useEffect(() => { const viewerSubscriptions: CesiumEvent.RemoveCallback[] = []; - + /* eslint-disable-next-line react-hooks/exhaustive-deps */ removeUpdateSubscription = addUpdateSubscription(); return () => { @@ -91,8 +90,6 @@ export const DistanceLegend: FC = observer( }; const updateDistanceLegendCesium = (scene: Scene) => { - const now = getTimestamp(); - // Find the distance between two pixels at the bottom center of the screen. const width = scene.canvas.clientWidth; const height = scene.canvas.clientHeight; @@ -160,7 +157,7 @@ export const DistanceLegend: FC = observer( .distanceTo(map.containerPointToLatLng([maxPixelWidth, halfHeight])); runInAction(() => (terria.mainViewer.scale = maxMeters / 100)); - // @ts-expect-error + // @ts-expect-error Accessing private method const meters = L.control.scale()._getRoundNum(maxMeters); const label = meters < 1000 ? meters + " m" : meters / 1000 + " km"; diff --git a/lib/ReactViews/Map/MapColumn.tsx b/lib/ReactViews/Map/MapColumn.tsx index 0135fb2a5da..1d0f0f12fc1 100644 --- a/lib/ReactViews/Map/MapColumn.tsx +++ b/lib/ReactViews/Map/MapColumn.tsx @@ -16,7 +16,6 @@ import { TerriaViewerWrapper } from "./TerriaViewerWrapper"; import Toast from "./Toast"; interface IMapColumnProps { - customFeedbacks: any; animationDuration: number; customElements: any; } @@ -26,7 +25,7 @@ interface IMapColumnProps { * the timeline and charts. */ export const MapColumn: FC = observer( - ({ customFeedbacks, customElements, animationDuration }) => { + ({ customElements, animationDuration }) => { const viewState = useViewState(); const { t } = useTranslation(); @@ -60,7 +59,6 @@ export const MapColumn: FC = observer( `} > { `} > { // this.props.viewState.showHelpPanel(); diff --git a/lib/ReactViews/Map/MapNavigation/Items/Compass/GyroscopeGuidance.d.ts b/lib/ReactViews/Map/MapNavigation/Items/Compass/GyroscopeGuidance.d.ts new file mode 100644 index 00000000000..3ff142f46bc --- /dev/null +++ b/lib/ReactViews/Map/MapNavigation/Items/Compass/GyroscopeGuidance.d.ts @@ -0,0 +1,10 @@ +import ViewState from "../../../../../ReactViewModels/ViewState"; +import React from "react"; + +interface PropsType { + viewState: ViewState; + handleHelp?: () => void; + onClose: () => void; +} + +export declare const GyroscopeGuidance: React.FC; diff --git a/lib/ReactViews/Map/MapNavigation/Items/ZoomControl.tsx b/lib/ReactViews/Map/MapNavigation/Items/ZoomControl.tsx index fa8e9386e74..4697d8c1736 100644 --- a/lib/ReactViews/Map/MapNavigation/Items/ZoomControl.tsx +++ b/lib/ReactViews/Map/MapNavigation/Items/ZoomControl.tsx @@ -180,7 +180,7 @@ class ZoomControlBase extends React.Component { } render() { - const { t, theme } = this.props; + const { t } = this.props; return (
    ; +export default MenuBar; diff --git a/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.d.ts b/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.d.ts new file mode 100644 index 00000000000..807fe6c1d6c --- /dev/null +++ b/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.d.ts @@ -0,0 +1,4 @@ +import React from "react"; + +declare class HelpPanel extends React.Component<{}> {} +export default HelpPanel; diff --git a/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.jsx b/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.jsx index 6d31dba92ab..3d984b6f92a 100644 --- a/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.jsx +++ b/lib/ReactViews/Map/Panels/HelpPanel/HelpPanel.jsx @@ -76,7 +76,7 @@ class HelpPanel extends React.Component { diff --git a/lib/ReactViews/Map/Panels/HelpPanel/VideoGuide.d.ts b/lib/ReactViews/Map/Panels/HelpPanel/VideoGuide.d.ts new file mode 100644 index 00000000000..de29f4976d5 --- /dev/null +++ b/lib/ReactViews/Map/Panels/HelpPanel/VideoGuide.d.ts @@ -0,0 +1,14 @@ +import React from "react"; +import ViewState from "../../../../ReactViewModels/ViewState"; + +interface PropsType { + viewState: ViewState; + videoName: string; + videoLink: string; + background: string; + // A number between 0 and 1.0 + backgroundOpacity?: number; +} + +declare class VideoGuide extends React.Component {} +export default VideoGuide; diff --git a/lib/ReactViews/Map/Panels/SharePanel/BuildShareLink.ts b/lib/ReactViews/Map/Panels/SharePanel/BuildShareLink.ts index 308bafc2c89..9c4adee89dd 100644 --- a/lib/ReactViews/Map/Panels/SharePanel/BuildShareLink.ts +++ b/lib/ReactViews/Map/Panels/SharePanel/BuildShareLink.ts @@ -23,6 +23,7 @@ import { import Terria from "../../../../Models/Terria"; import ViewState from "../../../../ReactViewModels/ViewState"; import getDereferencedIfExists from "../../../../Core/getDereferencedIfExists"; +import CatalogMemberMixin from "../../../../ModelMixins/CatalogMemberMixin"; /** User properties (generated from URL hash parameters) to add to share link URL in PRODUCTION environment. * If in Dev, we add all user properties. @@ -253,12 +254,23 @@ export function isShareable(terria: Terria) { return function (modelId: string) { const model = terria.getModelById(BaseModel, modelId); + if (CatalogMemberMixin.isMixedInto(model) && !model.shareable) { + return false; + } + // If this is a Reference, then use the model.target, otherwise use the model const dereferenced = - typeof model === undefined + typeof model === "undefined" ? model : getDereferencedIfExists(terria.getModelById(BaseModel, modelId)!); + if ( + CatalogMemberMixin.isMixedInto(dereferenced) && + !dereferenced.shareable + ) { + return false; + } + return ( model && ((HasLocalData.is(dereferenced) && !dereferenced.hasLocalData) || diff --git a/lib/ReactViews/Map/Panels/SharePanel/Print/PrintView.tsx b/lib/ReactViews/Map/Panels/SharePanel/Print/PrintView.tsx index 03323501baf..b871d767610 100644 --- a/lib/ReactViews/Map/Panels/SharePanel/Print/PrintView.tsx +++ b/lib/ReactViews/Map/Panels/SharePanel/Print/PrintView.tsx @@ -145,10 +145,12 @@ const PrintView = (props: Props) => { props.window.document.head.appendChild(mkStyle(styles)); props.window.document.body.appendChild(rootNode.current); props.window.addEventListener("beforeunload", props.closeCallback); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.window]); useEffect(() => { setScreenshot(viewState.terria.currentViewer.captureScreenshot()); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.window]); useEffect(() => { diff --git a/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrl.tsx b/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrl.tsx index 3979f1cdb65..23b92372d74 100644 --- a/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrl.tsx +++ b/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrl.tsx @@ -1,5 +1,4 @@ import React, { - forwardRef, useEffect, useImperativeHandle, useState, @@ -39,7 +38,7 @@ export interface IShareUrlRef { shorteningInProgress: boolean; } -export const ShareUrl = forwardRef< +export const ShareUrl = React.forwardRef< IShareUrlRef, PropsWithChildren >(function ShareUrl( @@ -68,6 +67,7 @@ export const ShareUrl = forwardRef< url: shareUrl, shorteningInProgress: shorteningInProgress }), + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [forwardRef, shareUrl, shorteningInProgress] ); @@ -94,6 +94,7 @@ export const ShareUrl = forwardRef< }) ); } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [terria, viewState, shouldShorten, includeStories]); return ( diff --git a/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrlWarning.tsx b/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrlWarning.tsx index 4cc0199d510..34eb9301a57 100644 --- a/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrlWarning.tsx +++ b/lib/ReactViews/Map/Panels/SharePanel/ShareUrl/ShareUrlWarning.tsx @@ -60,8 +60,8 @@ export const ShareUrlWarning: FC = observer( The following data sources will NOT be shared because they include - data from this local system. To share these data sources, publish - their data on a web server and{" "} + data from this local system or from an authenticated online service. + To share these data sources, publish their data on a web server and{" "} add them using a url . diff --git a/lib/ReactViews/Map/ProgressBar.tsx b/lib/ReactViews/Map/ProgressBar.tsx index eedfef26599..82e0cae793d 100644 --- a/lib/ReactViews/Map/ProgressBar.tsx +++ b/lib/ReactViews/Map/ProgressBar.tsx @@ -31,6 +31,7 @@ export const ProgressBar: VFC = observer(() => { return () => { eventHelper.removeAll(); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); const backgroundColor = diff --git a/lib/ReactViews/Map/TerriaViewerWrapper/Splitter/dragHook.ts b/lib/ReactViews/Map/TerriaViewerWrapper/Splitter/dragHook.ts index 06bd9530428..c8f56bd8ed1 100644 --- a/lib/ReactViews/Map/TerriaViewerWrapper/Splitter/dragHook.ts +++ b/lib/ReactViews/Map/TerriaViewerWrapper/Splitter/dragHook.ts @@ -103,6 +103,7 @@ export const useDragHook = ( e.preventDefault(); e.stopPropagation(); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [viewState] ); @@ -119,6 +120,7 @@ export const useDragHook = ( e.preventDefault(); e.stopPropagation(); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [viewState] ); diff --git a/lib/ReactViews/Mobile/MobileHeader.d.ts b/lib/ReactViews/Mobile/MobileHeader.d.ts new file mode 100644 index 00000000000..68eb64b1f86 --- /dev/null +++ b/lib/ReactViews/Mobile/MobileHeader.d.ts @@ -0,0 +1,12 @@ +import { i18n } from "i18next"; +import React from "react"; + +interface PropsType { + version?: string; + menuLeftItems: React.ReactNode[]; + menuItems: React.ReactNode[]; + i18n?: i18n; +} + +declare class MobileHeader extends React.Component {} +export default MobileHeader; diff --git a/lib/ReactViews/Notification/notification-window.scss b/lib/ReactViews/Notification/notification-window.scss index f40c172ddaf..631bfbe316a 100644 --- a/lib/ReactViews/Notification/notification-window.scss +++ b/lib/ReactViews/Notification/notification-window.scss @@ -1,3 +1,5 @@ +@use "sass:color"; + @import "~terriajs-variables"; @import "../../Sass/common/mixins"; @@ -26,7 +28,7 @@ .notification { border-radius: $radius-small; - border: 1px solid darken($dark, 3%); + border: 1px solid color.adjust($dark, $lightness: -3%); // background: $dark; z-index: 300; pre { diff --git a/lib/ReactViews/Preview/Description.d.ts b/lib/ReactViews/Preview/Description.d.ts new file mode 100644 index 00000000000..0ada6130fcb --- /dev/null +++ b/lib/ReactViews/Preview/Description.d.ts @@ -0,0 +1,11 @@ +import React from "react"; +import { BaseModel } from "../../Models/Definition/Model"; + +interface PropsType { + item: BaseModel; + printView?: boolean; +} + +declare class Description extends React.Component {} + +export default Description; diff --git a/lib/ReactViews/Preview/metadata-table.scss b/lib/ReactViews/Preview/metadata-table.scss index ec2a0eb7126..d70de7c0031 100644 --- a/lib/ReactViews/Preview/metadata-table.scss +++ b/lib/ReactViews/Preview/metadata-table.scss @@ -1,3 +1,5 @@ +@use "sass:color"; + @import "~terriajs-variables"; @import "../../Sass/common/mixins"; @@ -6,7 +8,7 @@ } .root table tr:nth-child(odd) { - background-color: darken($faint-bg, 8%); + background-color: color.adjust($faint-bg, $lightness: -8%); } .root table tr:nth-child(even) { background-color: $faint-bg; diff --git a/lib/ReactViews/PrivateIndicator/PrivateIndicator.jsx b/lib/ReactViews/PrivateIndicator/PrivateIndicator.tsx similarity index 73% rename from lib/ReactViews/PrivateIndicator/PrivateIndicator.jsx rename to lib/ReactViews/PrivateIndicator/PrivateIndicator.tsx index 06c2cce9781..bb5295f34b2 100644 --- a/lib/ReactViews/PrivateIndicator/PrivateIndicator.jsx +++ b/lib/ReactViews/PrivateIndicator/PrivateIndicator.tsx @@ -1,28 +1,24 @@ import React from "react"; -import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; - import Icon from "../../Styled/Icon"; import IconWrapper from "../../Styled/IconWrapper"; -PrivateIndicator.propTypes = { - inWorkbench: PropTypes.bool -}; +interface PropsType { + inWorkbench?: boolean; +} -export default function PrivateIndicator(props) { +export default function PrivateIndicator(props: PropsType) { const { t } = useTranslation(); - return ( + fill: ${(p: any) => p.inWorkbench ? p.theme.textLight : p.theme.charcoalGrey}; } `} diff --git a/lib/ReactViews/Search/SearchBox.jsx b/lib/ReactViews/Search/SearchBox.jsx index df6b103daff..b27bb8ecb31 100644 --- a/lib/ReactViews/Search/SearchBox.jsx +++ b/lib/ReactViews/Search/SearchBox.jsx @@ -135,7 +135,7 @@ export const SearchBox = createReactClass({ glyph={Icon.GLYPHS.close} styledWidth={"15px"} fillColor={this.props.theme.charcoalGrey} - opacity={"0.5"} + opacity={0.5} /> @@ -167,7 +167,7 @@ export const SearchBox = createReactClass({ glyph={Icon.GLYPHS.search} styledWidth={"20px"} fillColor={this.props.theme.charcoalGrey} - opacity={"0.5"} + opacity={0.5} css={` position: absolute; `} diff --git a/lib/ReactViews/Search/SearchBoxAndResults.d.ts b/lib/ReactViews/Search/SearchBoxAndResults.d.ts new file mode 100644 index 00000000000..361eb03867c --- /dev/null +++ b/lib/ReactViews/Search/SearchBoxAndResults.d.ts @@ -0,0 +1,22 @@ +import { TFunction } from "i18next"; +import React from "react"; +import Terria from "../../Models/Terria"; +import ViewState from "../../ReactViewModels/ViewState"; + +interface SearchInDataCatalogPropsType { + viewState: ViewState; + handleClick: () => void; +} + +export declare const SearchInDataCatalog: React.FC; + +interface PropsType { + viewState: ViewState; + terria: Terria; + t?: TFunction; + placeholder?: string; +} + +declare class SearchBoxAndResults extends React.Component {} + +export default SearchBoxAndResults; diff --git a/lib/ReactViews/Search/SearchResult.tsx b/lib/ReactViews/Search/SearchResult.tsx index fb2a6c3465d..17a35e4012c 100644 --- a/lib/ReactViews/Search/SearchResult.tsx +++ b/lib/ReactViews/Search/SearchResult.tsx @@ -28,7 +28,7 @@ interface SearchResultProps { clickAction(): void; isLastResult: boolean; locationSearchText: string; - icon: string; + icon: keyof typeof Icon.GLYPHS; } const SearchResult: React.FC = ( @@ -69,7 +69,6 @@ const SearchResult: React.FC = ( fillColor={isLightTheme && theme.textDarker} light={isDarkTheme} styledWidth={"16px"} - // @ts-expect-error glyph={Icon.GLYPHS[props.icon]} /> )} diff --git a/lib/ReactViews/SelectableDimensions/Button.tsx b/lib/ReactViews/SelectableDimensions/Button.tsx index 5657be1280a..e276c5eb8fb 100644 --- a/lib/ReactViews/SelectableDimensions/Button.tsx +++ b/lib/ReactViews/SelectableDimensions/Button.tsx @@ -11,7 +11,7 @@ import AnimatedSpinnerIcon from "../../Styled/AnimatedSpinnerIcon"; export const SelectableDimensionButton: React.FC<{ id: string; dim: SelectableDimensionButtonModel; -}> = ({ id, dim }) => { +}> = ({ dim }) => { const iconGlyph = dim.icon; const iconProps = { light: true, styledWidth: "16px", styledHeight: "16px" }; return ( diff --git a/lib/ReactViews/SelectableDimensions/Color.tsx b/lib/ReactViews/SelectableDimensions/Color.tsx index 9330d6d237b..5881305a380 100644 --- a/lib/ReactViews/SelectableDimensions/Color.tsx +++ b/lib/ReactViews/SelectableDimensions/Color.tsx @@ -23,7 +23,7 @@ const debounceSetColorDimensionValue = debounce( export const SelectableDimensionColor: React.FC<{ id: string; dim: SelectableDimensionColorModel; -}> = observer(({ id, dim }) => { +}> = observer(({ dim }) => { const [open, setIsOpen] = useState(false); const { t } = useTranslation(); return ( diff --git a/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx b/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx index f07d1b14688..a5929b48bc3 100644 --- a/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx +++ b/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { lab, rgb } from "d3-color"; +import { rgb } from "d3-color"; import * as d3Scale from "d3-scale-chromatic"; import React from "react"; import StandardCssColors from "../../Core/StandardCssColors"; @@ -38,21 +38,18 @@ function ramp( let colors: string[]; /** This could be used to draw text on top of swatches/ramps */ - let dark; if ( n && (d3Scale as any)[`scheme${name}`] && (d3Scale as any)[`scheme${name}`][n] ) { colors = (d3Scale as any)[`scheme${name}`][n]; - dark = lab(colors[0]).l < 50; } else { const interpolate = (d3Scale as any)[`interpolate${name}`]; if (!interpolate) { return ; } colors = []; - dark = lab(interpolate(0)).l < 50; for (let i = 0; i < interpolateWidth; ++i) { colors.push(rgb(interpolate(i / (interpolateWidth - 1))).hex()); } @@ -111,7 +108,6 @@ function swatches(name: string | undefined) { } if (!colors) return ; const n = colors.length; - const dark = lab(colors[0]).l < 50; return ( = observer(({ id, dim }) => { +}> = observer(({ dim }) => { const theme = useTheme(); const undefinedOption = { @@ -96,7 +96,7 @@ export const SelectableDimensionEnum: React.FC<{ export const SelectableDimensionEnumMulti: React.FC<{ id: string; dim: SelectableDimensionEnumMultiModel; -}> = observer(({ id, dim }) => { +}> = observer(({ dim }) => { const theme = useTheme(); const options = dim.options?.map((option) => ({ diff --git a/lib/ReactViews/SidePanel/FullScreenButton.d.ts b/lib/ReactViews/SidePanel/FullScreenButton.d.ts new file mode 100644 index 00000000000..0498ca74692 --- /dev/null +++ b/lib/ReactViews/SidePanel/FullScreenButton.d.ts @@ -0,0 +1,13 @@ +import React from "react"; +import IElementConfig from "../../Models/IElementConfig"; + +interface PropsType { + btnText: string; + minified: boolean; + animationDuration?: number; + elementConfig?: IElementConfig; +} + +declare class FullScreenButton extends React.Component {} + +export default FullScreenButton; diff --git a/lib/ReactViews/StandardUserInterface/Portal.tsx b/lib/ReactViews/StandardUserInterface/Portal.tsx index 2a857553d5e..157d6b6f86c 100644 --- a/lib/ReactViews/StandardUserInterface/Portal.tsx +++ b/lib/ReactViews/StandardUserInterface/Portal.tsx @@ -22,6 +22,7 @@ type PortalProps = { */ export const Portal: React.FC = ({ id, className }) => { const viewState = useViewState(); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ useEffect( action(() => { viewState.portals.set(id, document.getElementById(id)); diff --git a/lib/ReactViews/StandardUserInterface/StandardTheme.jsx b/lib/ReactViews/StandardUserInterface/StandardTheme.tsx similarity index 78% rename from lib/ReactViews/StandardUserInterface/StandardTheme.jsx rename to lib/ReactViews/StandardUserInterface/StandardTheme.tsx index dca182dfab1..a065688d20e 100644 --- a/lib/ReactViews/StandardUserInterface/StandardTheme.jsx +++ b/lib/ReactViews/StandardUserInterface/StandardTheme.tsx @@ -1,8 +1,9 @@ import Variables from "../../Sass/exports/_variables-export.scss"; - import Mixins from "../../Styled/mixins"; export const terriaTheme = { ...Variables, ...Mixins }; + +export type TerriaTheme = typeof terriaTheme; diff --git a/lib/ReactViews/StandardUserInterface/StandardUserInterface.tsx b/lib/ReactViews/StandardUserInterface/StandardUserInterface.tsx index 495587487f5..727b9de2c43 100644 --- a/lib/ReactViews/StandardUserInterface/StandardUserInterface.tsx +++ b/lib/ReactViews/StandardUserInterface/StandardUserInterface.tsx @@ -6,7 +6,6 @@ import React, { ReactNode, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { DefaultTheme } from "styled-components"; import combine from "terriajs-cesium/Source/Core/combine"; -import arrayContains from "../../Core/arrayContains"; import ViewState from "../../ReactViewModels/ViewState"; import Disclaimer from "../Disclaimer"; import DragDropFile from "../DragDropFile"; @@ -68,10 +67,7 @@ const StandardUserInterfaceBase: React.FC = }); const handleDragOver = (e: React.DragEvent) => { - if ( - !e.dataTransfer.types || - !arrayContains(e.dataTransfer.types, "Files") - ) { + if (!e.dataTransfer.types || !e.dataTransfer.types.includes("Files")) { return; } e.preventDefault(); @@ -92,8 +88,10 @@ const StandardUserInterfaceBase: React.FC = return () => { window.removeEventListener("resize", resizeListener, false); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ useEffect(resizeListener, [props.minimumLargeScreenWidth]); useEffect(() => { @@ -118,6 +116,7 @@ const StandardUserInterfaceBase: React.FC = width: 300 }); } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.terria.storyPromptShown]); // Merge theme in order of highest priority: themeOverrides props -> theme config parameter -> default terriaTheme @@ -133,8 +132,6 @@ const StandardUserInterfaceBase: React.FC = props.children ); - const terria = props.terria; - const showStoryBuilder = props.viewState.storyBuilderShown && !props.viewState.useSmallScreenInterface; @@ -226,7 +223,6 @@ const StandardUserInterfaceBase: React.FC =
    diff --git a/lib/ReactViews/StandardUserInterface/processCustomElements.d.ts b/lib/ReactViews/StandardUserInterface/processCustomElements.d.ts new file mode 100644 index 00000000000..24f4b3d9edb --- /dev/null +++ b/lib/ReactViews/StandardUserInterface/processCustomElements.d.ts @@ -0,0 +1,15 @@ +import React from "react"; + +type GroupElementKeys = + | "menu" + | "menuLeft" + | "nav" + | "experimentalMenu" + | "feedback"; + +declare function processCustomElements( + isSmallScreen: boolean, + customUI: React.ReactNode +): Record; + +export default processCustomElements; diff --git a/lib/ReactViews/StandardUserInterface/standard-user-interface.scss b/lib/ReactViews/StandardUserInterface/standard-user-interface.scss index e8c8a154c58..60a491ec911 100644 --- a/lib/ReactViews/StandardUserInterface/standard-user-interface.scss +++ b/lib/ReactViews/StandardUserInterface/standard-user-interface.scss @@ -40,15 +40,15 @@ .ui-root { // This is a workaround for a bug in IE11 on Windows 7. // https://connect.microsoft.com/IE/feedback/details/796745/mouse-events-are-not-delivered-at-all-anymore-when-inside-an-svg-a-use-is-removed-from-the-dom - svg use { - pointer-events: none; - } position: relative; flex-basis: 100%; height: 100vh; @media (max-width: $sm) { position: unset; } + svg use { + pointer-events: none; + } } .ui { @@ -153,8 +153,6 @@ } @media (min-width: $sm) { - .ui { - } .uiInner { display: flex; overflow: hidden; diff --git a/lib/ReactViews/Story/Story.tsx b/lib/ReactViews/Story/Story.tsx index ca2d54c2980..1d48e90f6a9 100644 --- a/lib/ReactViews/Story/Story.tsx +++ b/lib/ReactViews/Story/Story.tsx @@ -1,6 +1,10 @@ import classNames from "classnames"; -import { TFunction } from "i18next"; -import React, { MouseEventHandler, useEffect, useRef } from "react"; +import React, { + MouseEventHandler, + useEffect, + useLayoutEffect, + useRef +} from "react"; import { sortable } from "react-anything-sortable"; import { useTranslation } from "react-i18next"; import styled, { useTheme } from "styled-components"; @@ -39,7 +43,7 @@ interface Props { } interface MenuProps extends Props { - t: TFunction; + storyRef: React.RefObject; } const findTextContent = (content: any): string => { @@ -144,70 +148,87 @@ const recaptureStory = hideList(props); }; -const calculateOffset = - (props: Props) => (storyRef: React.RefObject) => { - const offsetTop = storyRef.current?.offsetTop || 0; - const scrollTop = props.parentRef.current.scrollTop || 0; - const heightParent = - (storyRef.current?.offsetParent as HTMLElement)?.offsetHeight || 0; +const StoryMenu = (props: MenuProps) => { + const { t } = useTranslation(); + const menuRef = useRef(null); + useLayoutEffect(() => { + // Adjust the position of the menu so it stays inside the scroll container. - const offsetTopScroll = offsetTop - scrollTop + 25; - if (offsetTopScroll + 125 > heightParent) { - return `bottom ${offsetTopScroll + 125 - heightParent + 45}px;`; - } - return `top: ${offsetTopScroll}px;`; - }; + if (!menuRef.current) return; + if (!props.parentRef.current) return; -const renderMenu = (props: MenuProps) => { - const { t } = props; + // Grow downwards, by default: + Object.assign(menuRef.current.style, { top: "0px", bottom: "unset" }); + const selfRect = menuRef.current.getBoundingClientRect(); + const parentRect = props.parentRef.current.getBoundingClientRect(); + if (selfRect.bottom > parentRect.bottom) { + // Looks like there's no room to the bottom; grow upwards. + Object.assign(menuRef.current.style, { top: "unset", bottom: "0px" }); + } + }, [props.parentRef]); return ( -
      -
    • - - - - {t("story.view")} - - -
    • -
    • - - - - {t("story.edit")} - - -
    • -
    • - - - - {t("story.recapture")} - - -
    • -
    • - - - - {t("story.delete")} - - -
    • -
    + +
      +
    • + + + + {t("story.view")} + + +
    • +
    • + + + + {t("story.edit")} + + +
    • +
    • + + + + {t("story.recapture")} + + +
    • +
    • + + + + {t("story.delete")} + + +
    • +
    +
    ); }; @@ -237,7 +258,6 @@ const Story = (props: Props) => { cursor: move; float: none !important; `} - position="static" style={props.style} className={classNames(props.className)} onMouseDown={props.onMouseDown} @@ -245,7 +265,6 @@ const Story = (props: Props) => { > { /> - {props.menuOpen && ( - - {renderMenu({ ...props, t })} - - )} + {props.menuOpen && } {bodyText.length > 0 && ( diff --git a/lib/ReactViews/Story/StoryBuilder.tsx b/lib/ReactViews/Story/StoryBuilder.tsx index 4907ceb1b69..a42b7255099 100644 --- a/lib/ReactViews/Story/StoryBuilder.tsx +++ b/lib/ReactViews/Story/StoryBuilder.tsx @@ -30,12 +30,12 @@ import SharePanel from "../Map/Panels/SharePanel/SharePanel"; import { WithViewState, withViewState } from "../Context"; import Story from "./Story"; import Styles from "./story-builder.scss"; -import StoryEditor from "./StoryEditor.jsx"; +import StoryEditor from "./StoryEditor"; const dataStoriesImg = require("../../../wwwroot/images/data-stories-getting-started.jpg"); const STORY_VIDEO = "storyVideo"; -type StoryData = ViewState["terria"]["stories"][number]; +export type StoryData = ViewState["terria"]["stories"][number]; interface IProps { isVisible?: boolean; @@ -85,7 +85,7 @@ class StoryBuilder extends React.Component< storyWithOpenMenuId: undefined }; } - removeStory = (index: number, story: StoryData) => { + removeStory = (index: number, story?: StoryData) => { this.setState({ isSharing: false, isRemoving: true, @@ -293,13 +293,13 @@ class StoryBuilder extends React.Component< }); }; - renderPlayShare(hasStories: boolean | undefined) { + renderPlayShare() { const { t } = this.props; return ( + <> - + {this.state.isRemoving && ( } css={` margin-right: -10px; @@ -404,7 +403,6 @@ class StoryBuilder extends React.Component< direction="vertical" dynamic css={` - position: static; margin-right: 10px; `} > @@ -433,10 +431,10 @@ class StoryBuilder extends React.Component< disabled={this.state.isRemoving} onClickCapture={this.onClickCapture} /> + - - + ); } @@ -465,8 +463,6 @@ class StoryBuilder extends React.Component< ref={(component: HTMLElement) => (this.refToMeasure = component)} isVisible={this.props.isVisible} isHidden={!this.props.isVisible} - styledWidth={"320px"} - styledMinWidth={"320px"} charcoalGreyBg column > @@ -495,10 +491,10 @@ class StoryBuilder extends React.Component< {!hasStories && this.renderIntro()} - {hasStories && this.renderPlayShare(hasStories)} + {hasStories && this.renderPlayShare()} - {hasStories && this.renderStories(this.state.editingMode)} + {hasStories && this.renderStories()} {this.state.editingMode && ( & { const Panel = styled(Box)` transition: all 0.25s; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + width: 320px; + min-width: 320px; + height: 100vh; ${(props) => props.isVisible && ` @@ -531,7 +530,7 @@ const Panel = styled(Box)` props.isHidden && ` visibility: hidden; - margin-right: -${props.styledWidth ? props.styledWidth : "320px"}; + margin-right: -100%; `} `; diff --git a/lib/ReactViews/Story/StoryEditor.d.ts b/lib/ReactViews/Story/StoryEditor.d.ts new file mode 100644 index 00000000000..3bf04fdcc2a --- /dev/null +++ b/lib/ReactViews/Story/StoryEditor.d.ts @@ -0,0 +1,15 @@ +import React from "react"; +import Terria from "../../Models/Terria"; +import { StoryData } from "./StoryBuilder"; + +interface PropsType { + story?: StoryData; + removeStory: (id: number) => void; + saveStory: (story: StoryData) => void; + exitEditingMode: () => void; + terria: Terria; +} + +declare class StoryEditor extends React.Component {} + +export default StoryEditor; diff --git a/lib/ReactViews/Story/StoryEditor.jsx b/lib/ReactViews/Story/StoryEditor.jsx index a4601881125..aa001952d7b 100644 --- a/lib/ReactViews/Story/StoryEditor.jsx +++ b/lib/ReactViews/Story/StoryEditor.jsx @@ -136,7 +136,14 @@ class StoryEditor extends React.Component { this.setState({ text: value }); } - renderPopupEditor() { + removeStory() { + this.props.exitEditingMode(); + if (this.state.id) { + this.props.removeStory(this.state.id); + } + } + + render() { const { t } = this.props; const maxImageHeight = "350px"; // TODO: where to put this to reduce coupling? return ( @@ -198,17 +205,6 @@ class StoryEditor extends React.Component { ); } - - removeStory() { - this.props.exitEditingMode(); - if (this.state.id) { - this.props.removeStory(this.state.id); - } - } - - render() { - return
    {this.renderPopupEditor()}
    ; - } } StoryEditor.propTypes = { diff --git a/lib/ReactViews/Story/story-editor.scss b/lib/ReactViews/Story/story-editor.scss index 7d166eae92b..4d29155f8a7 100644 --- a/lib/ReactViews/Story/story-editor.scss +++ b/lib/ReactViews/Story/story-editor.scss @@ -2,10 +2,6 @@ @import "../../Sass/common/mixins"; .popupEditor { - &.is-mounted { - opacity: 1; - @include transform(none); - } @include transform(translateY(20%)); opacity: 0; @include transition(all 0.3s); @@ -19,6 +15,10 @@ display: flex; flex-direction: column; justify-content: center; + &.is-mounted { + opacity: 1; + @include transform(none); + } .inner { max-width: 800px; width: 80vw; @@ -44,21 +44,6 @@ } } -.editor { - background: $dark-with-overlay; - color: $text-light; - font-family: $font-pop; - margin: $padding-small; - border-radius: 4px; - .editorHeader { - padding: $padding; - border-bottom: 1px solid $overlay; - } - form { - padding: $padding; - } -} - .field { composes: field from "../../Sass/common/_form.scss"; border: 0; @@ -66,24 +51,9 @@ padding: 0 !important; } -.fieldBtn { - composes: btn from "../../Sass/common/_buttons.scss"; - composes: btn--map from "../../Sass/common/_buttons.scss"; - background: #9ca1aa; - border-radius: 4px; - padding: $padding; - margin-bottom: $padding + 3px; - box-shadow: none; - width: 100%; -} - -.doneBtn, .saveBtn { composes: btn from "../../Sass/common/_buttons.scss"; composes: btn-primary from "../../Sass/common/_buttons.scss"; -} - -.saveBtn { width: auto; padding: 5px 15px; } diff --git a/lib/ReactViews/Story/story-editor.scss.d.ts b/lib/ReactViews/Story/story-editor.scss.d.ts index ccb456e9f56..5f8d5b648f9 100644 --- a/lib/ReactViews/Story/story-editor.scss.d.ts +++ b/lib/ReactViews/Story/story-editor.scss.d.ts @@ -3,11 +3,7 @@ interface CssExports { 'body': string; 'cancelBtn': string; - 'doneBtn': string; - 'editor': string; - 'editorHeader': string; 'field': string; - 'fieldBtn': string; 'header': string; 'inner': string; 'is-mounted': string; diff --git a/lib/ReactViews/Story/story-panel.scss b/lib/ReactViews/Story/story-panel.scss index 3e922791415..4cc2b38dad1 100644 --- a/lib/ReactViews/Story/story-panel.scss +++ b/lib/ReactViews/Story/story-panel.scss @@ -2,10 +2,6 @@ @import "../../Sass/common/mixins"; .story-container { - &.is-mounted { - opacity: 1; - @include transform(none); - } @include transform(translateY(20%)); opacity: 0; @include transition(all 0.3s); @@ -20,6 +16,11 @@ box-sizing: border-box; box-shadow: 0 0 15px 6px rgba(100, 100, 100, 0.3); + &.is-mounted { + opacity: 1; + @include transform(none); + } + .left { border-right: 1px solid $field-border; } diff --git a/lib/ReactViews/Tools/ClippingBox/ClippingBoxToolLauncher.tsx b/lib/ReactViews/Tools/ClippingBox/ClippingBoxToolLauncher.tsx index 0cf499a6e04..8b7a83977bd 100644 --- a/lib/ReactViews/Tools/ClippingBox/ClippingBoxToolLauncher.tsx +++ b/lib/ReactViews/Tools/ClippingBox/ClippingBoxToolLauncher.tsx @@ -46,6 +46,7 @@ const ClippingBoxToolLauncher: React.FC = observer( } }); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [item, cesium] ); diff --git a/lib/ReactViews/Tools/ClippingBox/RepositionClippingBox.tsx b/lib/ReactViews/Tools/ClippingBox/RepositionClippingBox.tsx index d67f5162bb7..98749c3b1fd 100644 --- a/lib/ReactViews/Tools/ClippingBox/RepositionClippingBox.tsx +++ b/lib/ReactViews/Tools/ClippingBox/RepositionClippingBox.tsx @@ -119,6 +119,7 @@ const RepositionClippingBox: React.FC = observer( }; // Init effect that sets up the event handlers etc. + /* eslint-disable-next-line react-hooks/exhaustive-deps */ useEffect( action(function init() { const canvas = cesium.scene.canvas; @@ -206,10 +207,6 @@ function setCursor(el: HTMLElement, cursorName: string) { el.style.cursor = cursorName; } -function truncate(text: string, length: number) { - return text.length <= length ? text : `${text.slice(0, length)}...`; -} - function pickGlobePosition( screenCoords: Cartesian2, scene: Scene, diff --git a/lib/ReactViews/Tools/DiffTool/DiffTool.tsx b/lib/ReactViews/Tools/DiffTool/DiffTool.tsx index 4422e018fac..cddffb0d93e 100644 --- a/lib/ReactViews/Tools/DiffTool/DiffTool.tsx +++ b/lib/ReactViews/Tools/DiffTool/DiffTool.tsx @@ -52,6 +52,7 @@ import Loader from "../../Loader"; import DatePicker from "./DatePicker"; import LocationPicker from "./LocationPicker"; import { CLOSE_TOOL_ID } from "../../Map/MapNavigation/registerMapNavigations"; +import updateModelFromJson from "../../../Models/Definition/updateModelFromJson"; const dateFormat = require("dateformat"); @@ -355,7 +356,7 @@ class Main extends React.Component { } @action.bound - onUserPickingLocation(pickingLocation: LatLonHeight) { + onUserPickingLocation(_pickingLocation: LatLonHeight) { this._isPickingNewLocation = true; } @@ -364,7 +365,7 @@ class Main extends React.Component { pickedFeatures: PickedFeatures, pickedLocation: LatLonHeight ) { - const { leftItem, rightItem, t } = this.props; + const { leftItem, rightItem } = this.props; const feature = pickedFeatures.features.find( (f) => doesFeatureBelongToItem(f, leftItem) || @@ -404,10 +405,17 @@ class Main extends React.Component { const terria = this.props.terria; terria.overlays.remove(this.props.leftItem); terria.overlays.remove(this.props.rightItem); + terria.workbench.add(this.diffItem); this.diffItem.setTrait(CommonStrata.user, "name", this.diffItemName); this.diffItem.showDiffImage(this.leftDate, this.rightDate, this.diffStyle); + + // If given, appply additional properties for the diff item + const diffItemProperties = this.diffItem.diffItemProperties; + if (diffItemProperties) { + updateModelFromJson(this.diffItem, CommonStrata.user, diffItemProperties); + } terria.showSplitter = false; } @@ -877,6 +885,7 @@ const AreaFilterSelection = (props: { t: TFunction; location?: LatLonHeight; isPickingNewLocation: boolean; + theme?: any; }) => { const { t, location, isPickingNewLocation } = props; let locationText = "-"; @@ -959,10 +968,8 @@ const LegendImage = function (props: any) { {...props} // Show the legend only if it loads successfully, so we start out hidden style={{ display: "none", marginTop: "4px" }} - // @ts-expect-error - onLoad={(e) => (e.target.style.display = "block")} - // @ts-expect-error - onError={(e) => (e.target.style.display = "none")} + onLoad={(e) => (e.currentTarget.style.display = "block")} + onError={(e) => (e.currentTarget.style.display = "none")} /> ); }; @@ -993,6 +1000,13 @@ async function createSplitItem( newItem.setTrait(CommonStrata.user, "opacity", 0); } + // Override feature info template as the parent featureInfoTemplate might + // not be relevant for the difference item. This has to be done in the user + // stratum to override template set in definition stratum. + updateModelFromJson(newItem, CommonStrata.user, { + featureInfoTemplate: { template: "" } + }); + setDefaultDiffStyle(newItem); // Set the default style to true color style if it exists diff --git a/lib/ReactViews/Tools/ItemSearchTool/ItemSearchTool.tsx b/lib/ReactViews/Tools/ItemSearchTool/ItemSearchTool.tsx index 7cf2b4cb760..49381be5dfa 100644 --- a/lib/ReactViews/Tools/ItemSearchTool/ItemSearchTool.tsx +++ b/lib/ReactViews/Tools/ItemSearchTool/ItemSearchTool.tsx @@ -66,6 +66,7 @@ const ItemSearchTool: React.FC = observer((props) => { }) .finally(() => props.afterLoad?.()); }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [itemSearchProvider] ); @@ -78,6 +79,7 @@ const ItemSearchTool: React.FC = observer((props) => { ); return disposeListener; }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [item] ); diff --git a/lib/ReactViews/Tools/ItemSearchTool/SearchForm.tsx b/lib/ReactViews/Tools/ItemSearchTool/SearchForm.tsx index a86ff8f2081..fa521862d23 100644 --- a/lib/ReactViews/Tools/ItemSearchTool/SearchForm.tsx +++ b/lib/ReactViews/Tools/ItemSearchTool/SearchForm.tsx @@ -306,7 +306,8 @@ const Field = styled(Box).attrs({ })``; const ParameterName = styled(Text).attrs({ - semiBold: true + semiBold: true, + breakWord: true })``; const Label = styled.label``; diff --git a/lib/ReactViews/Tools/PedestrianMode/MiniMap.tsx b/lib/ReactViews/Tools/PedestrianMode/MiniMap.tsx index 40b1fd23dfe..de45180644c 100644 --- a/lib/ReactViews/Tools/PedestrianMode/MiniMap.tsx +++ b/lib/ReactViews/Tools/PedestrianMode/MiniMap.tsx @@ -35,6 +35,7 @@ const MiniMap: React.FC = (props) => { >(); const [locationMarker, setLocationMarker] = useState(); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ useEffect( action(() => { const marker = new Marker( diff --git a/lib/ReactViews/Tools/PedestrianMode/MovementControls.tsx b/lib/ReactViews/Tools/PedestrianMode/MovementControls.tsx index 97114e22fa9..e66dc654a6f 100644 --- a/lib/ReactViews/Tools/PedestrianMode/MovementControls.tsx +++ b/lib/ReactViews/Tools/PedestrianMode/MovementControls.tsx @@ -35,6 +35,7 @@ const MovementControls: React.FC = (props) => { ); const detach = movementsController.activate(); return detach; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [props.cesium]); return ( @@ -75,10 +76,10 @@ const Title = styled(Box).attrs({ border-bottom: 1px solid #c0c0c0; `; -const MinimizeMaximizeButton = styled(Button).attrs((props) => ({ +const MinimizeMaximizeButton = styled(Button).attrs(({ maximized }) => ({ renderIcon: () => ( ) }))<{ maximized: boolean }>` diff --git a/lib/ReactViews/Tools/PedestrianMode/PedestrianMode.tsx b/lib/ReactViews/Tools/PedestrianMode/PedestrianMode.tsx index 5d020221363..1f2eb5a918e 100644 --- a/lib/ReactViews/Tools/PedestrianMode/PedestrianMode.tsx +++ b/lib/ReactViews/Tools/PedestrianMode/PedestrianMode.tsx @@ -47,6 +47,7 @@ const PedestrianMode: React.FC = observer((props) => { return () => { viewState.terria.mapNavigationModel.enable(MeasureTool.id); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); useEffect(function closeOnZoomTo() { @@ -56,6 +57,7 @@ const PedestrianMode: React.FC = observer((props) => { if (isMapZooming) viewState.closeTool(); } ); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); return ( diff --git a/lib/ReactViews/Tools/ToolModal.tsx b/lib/ReactViews/Tools/ToolModal.tsx index 1a74fae8486..1eda6275bf1 100644 --- a/lib/ReactViews/Tools/ToolModal.tsx +++ b/lib/ReactViews/Tools/ToolModal.tsx @@ -52,6 +52,9 @@ export const Frame: React.FC = observer((props) => { ); }); +const TOP_MARGIN = 70; +const BOTTOM_MARGIN = 100; + export const Main = styled(Text)` display: flex; flex-direction: column; @@ -68,9 +71,10 @@ const Wrapper = styled(Box).attrs({ styledWidth: "340px" // charcoalGreyBg: true })<{ isMapFullScreen: boolean }>` - top: 70px; + top: ${TOP_MARGIN}px; left: 0px; min-height: 220px; + max-height: calc(100vh - ${TOP_MARGIN + BOTTOM_MARGIN}px); // background: ${(p) => p.theme.dark}; margin-left: ${(props) => props.isMapFullScreen diff --git a/lib/ReactViews/Tour/TourPortal.d.ts b/lib/ReactViews/Tour/TourPortal.d.ts new file mode 100644 index 00000000000..82d9ed2b400 --- /dev/null +++ b/lib/ReactViews/Tour/TourPortal.d.ts @@ -0,0 +1,8 @@ +import React from "react"; + +export declare const TourExplanation: React.FC; +export declare const TourPreface: React.FC; + +declare const TourPortal: React.FC<{}>; + +export default TourPortal; diff --git a/lib/ReactViews/Tour/tour-helpers.ts b/lib/ReactViews/Tour/tour-helpers.ts index 8de3185b18a..253c3def15b 100644 --- a/lib/ReactViews/Tour/tour-helpers.ts +++ b/lib/ReactViews/Tour/tour-helpers.ts @@ -2,10 +2,7 @@ import { TourPoint } from "../../ReactViewModels/defaultTourPoints"; import isDefined from "../../Core/isDefined"; -import { - RelativePosition, - TOUR_WIDTH -} from "../../ReactViewModels/defaultTourPoints"; +import { RelativePosition } from "../../ReactViewModels/defaultTourPoints"; export { RelativePosition, TOUR_WIDTH diff --git a/lib/ReactViews/Transitions/FadeIn/FadeIn.d.ts b/lib/ReactViews/Transitions/FadeIn/FadeIn.d.ts new file mode 100644 index 00000000000..e1e12a556b4 --- /dev/null +++ b/lib/ReactViews/Transitions/FadeIn/FadeIn.d.ts @@ -0,0 +1,11 @@ +import React from "react"; + +interface PropsType { + isVisible: boolean; + onEnter?: () => void; + onExited?: () => void; + transitionProps?: any; +} + +declare const FadeIn: React.FC; +export default FadeIn; diff --git a/lib/ReactViews/Transitions/SlideUpFadeIn/SlideUpFadeIn.d.ts b/lib/ReactViews/Transitions/SlideUpFadeIn/SlideUpFadeIn.d.ts new file mode 100644 index 00000000000..24fd61b8fa0 --- /dev/null +++ b/lib/ReactViews/Transitions/SlideUpFadeIn/SlideUpFadeIn.d.ts @@ -0,0 +1,11 @@ +import React from "react"; + +interface PropsType { + isVisible: boolean; + onEnter?: () => void; + onExited?: () => void; + transitionProps?: any; +} + +declare const SlideUpFadeIn: React.FC; +export default SlideUpFadeIn; diff --git a/lib/ReactViews/WelcomeMessage/WelcomeMessage.d.ts b/lib/ReactViews/WelcomeMessage/WelcomeMessage.d.ts new file mode 100644 index 00000000000..858b3816356 --- /dev/null +++ b/lib/ReactViews/WelcomeMessage/WelcomeMessage.d.ts @@ -0,0 +1,12 @@ +import React from "react"; + +interface WelcomeMessagePurePropsType { + showWelcomeMessage: boolean; + setShowWelcomeMessage: (show: boolean) => void; + isTopElement: boolean; +} + +export declare const WelcomeMessagePure: React.FC; + +declare class WelcomeMessage extends React.Component<{}> {} +export default WelcomeMessage; diff --git a/lib/ReactViews/WelcomeMessage/WelcomeMessage.jsx b/lib/ReactViews/WelcomeMessage/WelcomeMessage.jsx index 1df80969196..dc74e1dc486 100644 --- a/lib/ReactViews/WelcomeMessage/WelcomeMessage.jsx +++ b/lib/ReactViews/WelcomeMessage/WelcomeMessage.jsx @@ -262,7 +262,7 @@ export const WelcomeMessagePure = (props) => { viewState.terria.configParameters.welcomeMessageVideo .placeholderImage } - backgroundBlackOverlay={"50%"} + backgroundBlackOverlay={0.5} > {} +export default ColorScaleRangeSection; diff --git a/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx b/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx index 2d28defaa60..b2b9bb7dc63 100644 --- a/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx +++ b/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx @@ -13,7 +13,7 @@ const DisplayAsPercentSection: React.FC = ( props: IDisplayAsPercentSection ) => { const { t } = useTranslation(); - const theme = useTheme(); + useTheme(); const togglePercentage = () => { props.item.displayPercent = !props.item.displayPercent; }; diff --git a/lib/ReactViews/Workbench/Controls/FilterSection.d.ts b/lib/ReactViews/Workbench/Controls/FilterSection.d.ts new file mode 100644 index 00000000000..b599403cb16 --- /dev/null +++ b/lib/ReactViews/Workbench/Controls/FilterSection.d.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { BaseModel } from "../../../Models/Definition/Model"; + +interface PropsType { + item: BaseModel; +} + +declare class FilterSection extends React.Component {} +export default FilterSection; diff --git a/lib/ReactViews/Workbench/Controls/FilterSection.jsx b/lib/ReactViews/Workbench/Controls/FilterSection.jsx index 14cabf8452c..634a2acb4b2 100644 --- a/lib/ReactViews/Workbench/Controls/FilterSection.jsx +++ b/lib/ReactViews/Workbench/Controls/FilterSection.jsx @@ -27,7 +27,7 @@ class FilterSection extends React.Component { } return (
    - {item.filters.map(this.renderFilter)} + {item.filters.map(this.renderFilter, this)}
    ); } diff --git a/lib/ReactViews/Workbench/Controls/SatelliteImageryTimeFilterSection.d.ts b/lib/ReactViews/Workbench/Controls/SatelliteImageryTimeFilterSection.d.ts new file mode 100644 index 00000000000..35be383dbda --- /dev/null +++ b/lib/ReactViews/Workbench/Controls/SatelliteImageryTimeFilterSection.d.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { BaseModel } from "../../../Models/Definition/Model"; + +interface PropsType { + item: BaseModel; +} + +declare class SatelliteImageryTimeFilterSection extends React.Component {} +export default SatelliteImageryTimeFilterSection; diff --git a/lib/ReactViews/Workbench/Controls/ShortReport.tsx b/lib/ReactViews/Workbench/Controls/ShortReport.tsx index 7143a9eac89..de5dac79e52 100644 --- a/lib/ReactViews/Workbench/Controls/ShortReport.tsx +++ b/lib/ReactViews/Workbench/Controls/ShortReport.tsx @@ -1,5 +1,3 @@ -"use strict"; - import { runInAction } from "mobx"; import { observer } from "mobx-react"; import React from "react"; @@ -103,5 +101,3 @@ export default class ShortReport extends React.Component<{ ); } } - -module.exports = ShortReport; diff --git a/lib/ReactViews/Workbench/Controls/TimerSection.d.ts b/lib/ReactViews/Workbench/Controls/TimerSection.d.ts new file mode 100644 index 00000000000..1ae97eb7525 --- /dev/null +++ b/lib/ReactViews/Workbench/Controls/TimerSection.d.ts @@ -0,0 +1,5 @@ +import React from "react"; +import { BaseModel } from "../../../Models/Definition/Model"; + +declare class TimerSection extends React.Component<{ item: BaseModel }> {} +export default TimerSection; diff --git a/lib/ReactViews/Workbench/Controls/legend.scss b/lib/ReactViews/Workbench/Controls/legend.scss index 2d80290343c..6896f67a87f 100644 --- a/lib/ReactViews/Workbench/Controls/legend.scss +++ b/lib/ReactViews/Workbench/Controls/legend.scss @@ -4,15 +4,13 @@ composes: clearfix from "../../../Sass/common/_base.scss"; composes: list-reset from "../../../Sass/common/_base.scss"; font-family: $font-mono; - font-size: $font-size-small; + // Small font size prevents the font from dictating the table row height. + font-size: 5px; li { display: block; } - // Small font size prevents the font from dictating the table row height. - font-size: 5px; - .legendOpenExternally { font-size: $font-size-small; color: $text-light; @@ -25,11 +23,11 @@ } .legend__inner { + padding: $padding 0 0 0; a { // Only the actual legend should be clickable rather than the whole area display: inline-block; } - padding: $padding 0 0 0; } .legend__legendBoxImg { diff --git a/lib/ReactViews/Workflow/WorkflowPanel.tsx b/lib/ReactViews/Workflow/WorkflowPanel.tsx index 6eff618dffa..fcc104092c1 100644 --- a/lib/ReactViews/Workflow/WorkflowPanel.tsx +++ b/lib/ReactViews/Workflow/WorkflowPanel.tsx @@ -37,6 +37,7 @@ const WorkflowPanel: React.FC = observer((props) => { viewState.terria.isWorkflowPanelActive = false; }); }; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []); return ( @@ -72,6 +73,7 @@ const WorkflowPanel: React.FC = observer((props) => { type ErrorBoundaryProps = { viewState: ViewState; + children: React.ReactNode; }; class ErrorBoundary extends React.Component { @@ -118,10 +120,6 @@ const TitleBar = styled.div` border-bottom: 1px solid ${(p) => p.theme.darkLighter}; `; -const FooterBar = styled(TitleBar)` - border: none; -`; - const Title = styled(Text).attrs({ textLight: true, bold: true diff --git a/lib/Sass/common/_base.scss b/lib/Sass/common/_base.scss index 3ba9c9e415e..9d83ff804bc 100644 --- a/lib/Sass/common/_base.scss +++ b/lib/Sass/common/_base.scss @@ -429,6 +429,16 @@ // This is what happens when we mess up with CSS Modules so make it SUPER SUPER OBVIOUS // TODO: Remove in build .undefined { + position: relative; + background: #f00 !important; + color: magenta !important; + border: 3px orange dashed !important; + + animation-name: flash; + animation-duration: 1s; + animation-timing-function: step-start; + animation-iteration-count: infinite; + &:before { position: fixed !important; top: 0 !important; @@ -439,16 +449,6 @@ opacity: 1 !important; z-index: 100000 !important; } - - position: relative; - background: #f00 !important; - color: magenta !important; - border: 3px orange dashed !important; - - animation-name: flash; - animation-duration: 1s; - animation-timing-function: step-start; - animation-iteration-count: infinite; } @keyframes flash { diff --git a/lib/Sass/common/_buttons.scss b/lib/Sass/common/_buttons.scss index ccf7bfd0599..84684c7bfa7 100644 --- a/lib/Sass/common/_buttons.scss +++ b/lib/Sass/common/_buttons.scss @@ -153,23 +153,22 @@ $btn-setting-size: 35px; .btn--catalog { padding: $padding 30px + $padding; - @media (min-width: $sm) { - padding: $padding-small 30px + $padding; - } - position: relative; width: 100%; font-weight: bold; + @media (min-width: $sm) { + padding: $padding-small 30px + $padding; + } .btn--group-indicator { position: absolute; left: 0; padding: $padding $padding * 2.2 $padding $padding * 1.5; - @media (min-width: $sm) { - padding: $padding-small $padding * 2.2 $padding-small $padding * 1.5; - } top: 0; font-size: $font-size-mid-large; opacity: 0.5; + @media (min-width: $sm) { + padding: $padding-small $padding * 2.2 $padding-small $padding * 1.5; + } } &:before { position: absolute; diff --git a/lib/Sass/common/_variables.scss b/lib/Sass/common/_variables.scss index 26a01198883..25272c0ec8e 100644 --- a/lib/Sass/common/_variables.scss +++ b/lib/Sass/common/_variables.scss @@ -1,3 +1,5 @@ +@use "sass:color"; + @import url("https://fonts.googleapis.com/css?family=Nunito:300,400,500,400italic,600,700&display=swap"); $font-pop: "Nunito", sans-serif; @@ -9,7 +11,7 @@ $font: $font-base; $color-primary: #519ac2; $color-primary-disabled: #595b60; @function get-hover-color($base-color) { - @return lighten($base-color, 10%); + @return color.adjust($base-color, $lightness: 10%); } $color-secondary: #ff6f00; @@ -169,7 +171,7 @@ $chart-grid-color: rgba(#fff, 0.085); $chart-text-color: #fff; $chart-line-color: #ccc; $chart-preview-line-color: $chart-line-color; -$chart-darker: darken($dark, 8%); +$chart-darker: color.adjust($dark, $lightness: -8%); $chart-panel-background: $dark; $chart-panel-header: $chart-darker; diff --git a/lib/Styled/Box.tsx b/lib/Styled/Box.tsx index 0811606137f..4f6a3559278 100644 --- a/lib/Styled/Box.tsx +++ b/lib/Styled/Box.tsx @@ -1,4 +1,4 @@ -import { Ref } from "react"; +import React, { Ref } from "react"; import styled from "styled-components"; import { Overflow, WhiteSpace, WordBreak } from "./Styled.types"; @@ -243,7 +243,7 @@ export const Box = styled.div` `; export const BoxSpan = styled(Box).attrs( - (props: { as?: React.ElementType | keyof JSX.IntrinsicElements }) => ({ + (_props: { as?: React.ElementType | keyof JSX.IntrinsicElements }) => ({ as: "span" }) )``; diff --git a/lib/Styled/Button.tsx b/lib/Styled/Button.tsx index d6a362424c4..948b8066cbe 100644 --- a/lib/Styled/Button.tsx +++ b/lib/Styled/Button.tsx @@ -165,6 +165,10 @@ export const RawButton = styled.button` export type ButtonProps = { renderIcon?: () => React.ReactChild; iconProps?: any; + primary?: boolean; + secondary?: boolean; + warning?: boolean; + textLight?: boolean; rightIcon?: boolean; textProps?: any; children?: React.ReactChildren; diff --git a/lib/Styled/Checkbox/Checkbox.tsx b/lib/Styled/Checkbox/Checkbox.tsx index 30cc5a6078b..0c91c514107 100644 --- a/lib/Styled/Checkbox/Checkbox.tsx +++ b/lib/Styled/Checkbox/Checkbox.tsx @@ -14,115 +14,113 @@ import CheckboxIcon from "./Elements/CheckboxIcon"; import HiddenCheckbox from "./Elements/HiddenCheckbox"; import { ICheckboxProps } from "./types"; -const Checkbox = memo( - forwardRef(function Checkbox( - props: ICheckboxProps, - ref: Ref - ) { - const { - isChecked: isCheckedProp, - isDisabled = false, - defaultChecked = false, - isIndeterminate = false, - onChange: onChangeProps, - title, - name, - value, - children, - textProps, - className, - ...rest - } = props; +const Checkbox = forwardRef(function Checkbox( + props: ICheckboxProps, + ref: Ref +) { + const { + isChecked: isCheckedProp, + isDisabled = false, + defaultChecked = false, + isIndeterminate = false, + onChange: onChangeProps, + title, + name, + value, + children, + textProps, + className, + ..._rest + } = props; - const [isCheckedState, setIsCheckedState] = useState( - isCheckedProp !== undefined ? isCheckedProp : defaultChecked - ); + const [isCheckedState, setIsCheckedState] = useState( + isCheckedProp !== undefined ? isCheckedProp : defaultChecked + ); - const onChange = useCallback( - (e: ChangeEvent) => { - setIsCheckedState(e.target.checked); - if (onChangeProps) { - onChangeProps(e); - } - }, - [onChangeProps] - ); + const onChange = useCallback( + (e: ChangeEvent) => { + setIsCheckedState(e.target.checked); + if (onChangeProps) { + onChangeProps(e); + } + }, + [onChangeProps] + ); - // Use isChecked from the state if it is controlled - const isChecked = - isCheckedProp === undefined ? isCheckedState : isCheckedProp; - const id = useUID(); + // Use isChecked from the state if it is controlled + const isChecked = + isCheckedProp === undefined ? isCheckedState : isCheckedProp; + const id = useUID(); - // Add props to children - const childrenWithProps = React.Children.map(children, (child) => { - // Checking isValidElement is the safe way and avoids a typescript - // error too. - if (React.isValidElement(child)) { - return React.cloneElement( - child as ReactElement<{ - isDisabled: boolean; - isChecked: boolean; - style: any; - }>, - { - isDisabled, - isChecked, - style: { fontSize: "inherit" } - } - ); - } - return child; - }); + // Add props to children + const childrenWithProps = React.Children.map(children, (child) => { + // Checking isValidElement is the safe way and avoids a typescript + // error too. + if (React.isValidElement(child)) { + return React.cloneElement( + child as ReactElement<{ + isDisabled: boolean; + isChecked: boolean; + style: any; + }>, + { + isDisabled, + isChecked, + style: { fontSize: "inherit" } + } + ); + } + return child; + }); - return ( - - - - - {childrenWithProps} - - ); - }) -); + `} + {...textProps} + > + + + + {childrenWithProps} + + ); +}); Checkbox.displayName = "Checkbox"; -export default Checkbox; +export default memo(Checkbox); diff --git a/lib/Styled/Input.tsx b/lib/Styled/Input.tsx index af39408e4a0..86653d714cd 100644 --- a/lib/Styled/Input.tsx +++ b/lib/Styled/Input.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styled, { css, DefaultTheme, useTheme } from "styled-components"; +import styled, { css, useTheme } from "styled-components"; import Box, { IBoxProps } from "./Box"; export interface CommonProps { @@ -126,7 +126,7 @@ export const StyledInput = styled.input` const Input: React.FC = (props: InputProps) => { const { boxProps, ...rest }: InputProps = props; - const theme: DefaultTheme = useTheme(); + useTheme(); return ( diff --git a/lib/Styled/Text.tsx b/lib/Styled/Text.tsx index 73819e0ad4d..1f9b92a52c6 100644 --- a/lib/Styled/Text.tsx +++ b/lib/Styled/Text.tsx @@ -1,4 +1,4 @@ -import { ComponentProps } from "react"; +import React, { ComponentProps } from "react"; import styled from "styled-components"; interface ITextSize { diff --git a/lib/Styled/mixins.js b/lib/Styled/mixins.ts similarity index 81% rename from lib/Styled/mixins.js rename to lib/Styled/mixins.ts index ec3e61403f9..359691550c6 100644 --- a/lib/Styled/mixins.js +++ b/lib/Styled/mixins.ts @@ -1,5 +1,11 @@ // Doesn't result in composed classes that reuse these, but to save some typing +import cssExports from "../Sass/exports/_variables-export.scss"; + +interface MixinProps { + theme: typeof cssExports; +} + export const scrollBars = () => ` -webkit-overflow-scrolling: touch; @@ -41,16 +47,16 @@ export const removeListStyles = () => ` margin: 0; `; -export const borderRadiusTop = (radius) => ` +export const borderRadiusTop = (radius: number | string) => ` border-radius: ${radius}px ${radius}px 0 0; `; -export const borderRadiusRight = (radius) => ` +export const borderRadiusRight = (radius: number | string) => ` border-radius: 0 ${radius}px ${radius}px 0; `; -export const borderRadiusBottom = (radius) => ` +export const borderRadiusBottom = (radius: number | string) => ` border-radius: 0 0 ${radius}px ${radius}px; `; -export const borderRadiusLeft = (radius) => ` +export const borderRadiusLeft = (radius: number | string) => ` border-radius: ${radius}px 0 0 ${radius}px; `; @@ -65,7 +71,7 @@ export const addBasicHoverStyles = () => ` * unfortunately this means more classnames outputted, but gives us consistency * in the meantime. * */ -export const addTerriaPrimaryBtnStyles = (props) => ` +export const addTerriaPrimaryBtnStyles = (props: MixinProps) => ` background: ${props.theme.colorPrimary}; color: ${props.theme.textLight}; svg { @@ -79,7 +85,7 @@ export const addTerriaPrimaryBtnStyles = (props) => ` } `; -export const addTerriaSecondaryBtnStyles = (props) => ` +export const addTerriaSecondaryBtnStyles = (props: MixinProps) => ` color: ${props.theme.colorPrimary}; // Don't override border here on secondary, as it's set specifically on certain buttons e.g. story cancel button @@ -91,7 +97,7 @@ export const addTerriaSecondaryBtnStyles = (props) => ` ${addBasicHoverStyles()} `; -export const addTerriaTertiaryBtnStyles = (props) => ` +export const addTerriaTertiaryBtnStyles = (props: MixinProps) => ` color: ${props.theme.modalText}; background: ${props.theme.modalBg}; border: 2px solid ${props.theme.modalText}; @@ -103,7 +109,7 @@ export const addTerriaTertiaryBtnStyles = (props) => ` } `; -export const addTerriaMapBtnStyles = (props) => ` +export const addTerriaMapBtnStyles = (props: MixinProps) => ` color: ${props.theme.textLight}; background-color: ${props.theme.dark}; &:hover, @@ -123,7 +129,7 @@ export const addTerriaMapBtnStyles = (props) => ` } `; -export const addTerriaLightBtnStyles = (props) => ` +export const addTerriaLightBtnStyles = (props: MixinProps) => ` color: ${props.theme.textLight}; svg { fill: ${props.theme.textLight}; @@ -135,7 +141,7 @@ export const addTerriaLightBtnStyles = (props) => ` } `; -export const addTerriaScrollbarStyles = (props) => ` +export const addTerriaScrollbarStyles = () => ` -webkit-overflow-scrolling: touch; &::-webkit-scrollbar { diff --git a/lib/Table/MergedStyleMapLegend.ts b/lib/Table/MergedStyleMapLegend.ts index 4e9669c2b8b..40006a8e2ff 100644 --- a/lib/Table/MergedStyleMapLegend.ts +++ b/lib/Table/MergedStyleMapLegend.ts @@ -18,7 +18,7 @@ export class MergedStyleMapLegend extends LoadableStratum(LegendTraits) { makeObservable(this); } - duplicateLoadableStratum(newModel: BaseModel): this { + duplicateLoadableStratum(_newModel: BaseModel): this { return new MergedStyleMapLegend(this.legends) as this; } diff --git a/lib/Table/TableAutomaticStylesStratum.ts b/lib/Table/TableAutomaticStylesStratum.ts index 2833b0a5298..5428237cfb4 100644 --- a/lib/Table/TableAutomaticStylesStratum.ts +++ b/lib/Table/TableAutomaticStylesStratum.ts @@ -231,7 +231,7 @@ export default class TableAutomaticStylesStratum extends LoadableStratum( i < this.catalogItem.activeTableStyle.rowGroups.length; i++ ) { - const [rowGroupId, rowIds] = + const [_rowGroupId, rowIds] = this.catalogItem.activeTableStyle.rowGroups[i]; // Check if there is only 1 unique date in this rowGroup const dates = rowIds diff --git a/lib/Table/TableColumn.ts b/lib/Table/TableColumn.ts index 458575fdb4c..191401d4a37 100644 --- a/lib/Table/TableColumn.ts +++ b/lib/Table/TableColumn.ts @@ -786,35 +786,6 @@ export default class TableColumn { return this.values; } - @computed - get scaledValueFunctionForType(): (rowIndex: number) => number | null { - if (this.type === TableColumnType.scalar) { - const valuesAsNumbers = this.valuesAsNumbers; - const minimum = valuesAsNumbers.minimum; - const maximum = valuesAsNumbers.maximum; - - if (minimum === undefined || maximum === undefined) { - return nullFunction; - } - - const delta = maximum - minimum; - if (delta === 0.0) { - return nullFunction; - } - - const values = valuesAsNumbers.values; - return function (rowIndex: number) { - const value = values[rowIndex]; - if (value === null) { - return null; - } - return (value - minimum) / delta; - }; - } - - return nullFunction; - } - private guessColumnTypeFromValues(): TableColumnType { let type: TableColumnType | undefined; @@ -919,7 +890,3 @@ function toNumber(value: string): number | null { } return null; } - -function nullFunction(rowIndex: number) { - return null; -} diff --git a/lib/Table/TableFeatureInfoStratum.ts b/lib/Table/TableFeatureInfoStratum.ts index e5d178f4434..49a39855c32 100644 --- a/lib/Table/TableFeatureInfoStratum.ts +++ b/lib/Table/TableFeatureInfoStratum.ts @@ -1,11 +1,12 @@ +import { computed, makeObservable } from "mobx"; +import { FEATURE_ID_PROP } from "../ModelMixins/GeojsonMixin"; import TableMixin from "../ModelMixins/TableMixin"; import LoadableStratum from "../Models/Definition/LoadableStratum"; import { BaseModel } from "../Models/Definition/Model"; -import TableTraits from "../Traits/TraitsClasses/Table/TableTraits"; -import { computed, makeObservable } from "mobx"; +import StratumOrder from "../Models/Definition/StratumOrder"; import createStratumInstance from "../Models/Definition/createStratumInstance"; import { FeatureInfoTemplateTraits } from "../Traits/TraitsClasses/FeatureInfoTraits"; -import StratumOrder from "../Models/Definition/StratumOrder"; +import TableTraits from "../Traits/TraitsClasses/Table/TableTraits"; export default class TableFeatureInfoStratum extends LoadableStratum( TableTraits @@ -29,6 +30,7 @@ export default class TableFeatureInfoStratum extends LoadableStratum( let template = ''; template += this.catalogItem.tableColumns + ?.filter((col) => col.name !== FEATURE_ID_PROP) ?.map( (col) => `` diff --git a/lib/ThirdParty/proj4-fully-loaded/index.d.ts b/lib/ThirdParty/proj4-fully-loaded/index.d.ts new file mode 100644 index 00000000000..9cfa1dd2df9 --- /dev/null +++ b/lib/ThirdParty/proj4-fully-loaded/index.d.ts @@ -0,0 +1 @@ +declare module "proj4-fully-loaded"; diff --git a/lib/ThirdParty/terriajs-cesium-extra/index.d.ts b/lib/ThirdParty/terriajs-cesium-extra/index.d.ts index 0fabbcee06a..3b3c21e18ed 100644 --- a/lib/ThirdParty/terriajs-cesium-extra/index.d.ts +++ b/lib/ThirdParty/terriajs-cesium-extra/index.d.ts @@ -2,6 +2,7 @@ declare module "terriajs-cesium/Source/Scene/TweenCollection" { export default class TweenCollection { get length(): number; add(options: any): any; + update(time?: number): void; } } @@ -44,25 +45,24 @@ declare module "terriajs-cesium/Source/Core/PolygonGeometryLibrary"; declare module "terriajs-cesium/Source/DataSources/getElement"; -declare interface Axis { - X: number; - Y: number; - Z: number; - fromName(name: string): number; -} - -declare interface FeatureDetection { - isEdge(): boolean; - isInternetExplorer(): boolean; - internetExplorerVersion(): number[]; - chromeVersion(): number[]; -} - // This is a workaround for Cesium's incorrect type declaration for raiseEvent. declare module "terriajs-cesium" { export interface Event { raiseEvent(...arguments: any[]): void; } + + namespace FeatureDetection { + function isChrome(): boolean; + function isEdge(): boolean; + function isInternetExplorer(): boolean; + function isFirefox(): boolean; + function internetExplorerVersion(): number[]; + function chromeVersion(): number[]; + } + + namespace Axis { + function fromName(name: string): number; + } } // Begin Generated Declarations diff --git a/lib/ThirdParty/xml2json.d.ts b/lib/ThirdParty/xml2json.d.ts new file mode 100644 index 00000000000..7e663dfa272 --- /dev/null +++ b/lib/ThirdParty/xml2json.d.ts @@ -0,0 +1,4 @@ +export default function xml2json( + xml: XMLDocument | string | undefined, + extended?: boolean +): any; diff --git a/lib/ThirdParty/xml2json.js b/lib/ThirdParty/xml2json.js index fcd396f09c4..61ae6d160cd 100644 --- a/lib/ThirdParty/xml2json.js +++ b/lib/ThirdParty/xml2json.js @@ -180,4 +180,4 @@ function text2xml(str) { return parser.parseFromString(str, "text/xml"); } -module.exports = xml2json; +export default xml2json; diff --git a/lib/Traits/Decorators/modelReferenceArrayTrait.ts b/lib/Traits/Decorators/modelReferenceArrayTrait.ts index e46244e224a..c6aa430b976 100644 --- a/lib/Traits/Decorators/modelReferenceArrayTrait.ts +++ b/lib/Traits/Decorators/modelReferenceArrayTrait.ts @@ -13,7 +13,7 @@ export interface ModelArrayTraitOptions extends TraitOptions { factory?: ModelFactory; } -export default function modelReferenceArrayTrait( +export default function modelReferenceArrayTrait<_T>( options: ModelArrayTraitOptions ) { return function (target: any, propertyKey: string) { diff --git a/lib/Traits/Decorators/modelReferenceTrait.ts b/lib/Traits/Decorators/modelReferenceTrait.ts index 122ac276831..1cf346af379 100644 --- a/lib/Traits/Decorators/modelReferenceTrait.ts +++ b/lib/Traits/Decorators/modelReferenceTrait.ts @@ -12,7 +12,7 @@ export interface ModelTraitOptions extends TraitOptions { factory?: ModelFactory; } -export default function modelReferenceTrait(options: ModelTraitOptions) { +export default function modelReferenceTrait<_T>(options: ModelTraitOptions) { return function (target: any, propertyKey: string) { const constructor = target.constructor; if (!constructor.traits) { diff --git a/lib/Traits/Decorators/primitiveArrayTrait.ts b/lib/Traits/Decorators/primitiveArrayTrait.ts index e91a6cbebc8..68aa7f893f0 100644 --- a/lib/Traits/Decorators/primitiveArrayTrait.ts +++ b/lib/Traits/Decorators/primitiveArrayTrait.ts @@ -5,7 +5,7 @@ import Trait, { TraitOptions } from "../Trait"; type PrimitiveType = "string" | "number" | "boolean"; -export interface PrimitiveArrayTraitOptions extends TraitOptions { +export interface PrimitiveArrayTraitOptions<_T> extends TraitOptions { type: PrimitiveType; isNullable?: boolean; } diff --git a/lib/Traits/Decorators/primitiveTrait.ts b/lib/Traits/Decorators/primitiveTrait.ts index 19f49e47a5b..658261a2839 100644 --- a/lib/Traits/Decorators/primitiveTrait.ts +++ b/lib/Traits/Decorators/primitiveTrait.ts @@ -5,7 +5,7 @@ import Trait, { TraitOptions } from "../Trait"; type PrimitiveType = "string" | "number" | "boolean"; -export interface PrimitiveTraitOptions extends TraitOptions { +export interface PrimitiveTraitOptions<_T> extends TraitOptions { type: PrimitiveType; isNullable?: boolean; } diff --git a/lib/Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits.ts b/lib/Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits.ts new file mode 100644 index 00000000000..9826ac0a2c1 --- /dev/null +++ b/lib/Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits.ts @@ -0,0 +1,175 @@ +import { JsonObject } from "../../Core/Json"; +import anyTrait from "../Decorators/anyTrait"; +import objectArrayTrait from "../Decorators/objectArrayTrait"; +import objectTrait from "../Decorators/objectTrait"; +import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; +import primitiveTrait from "../Decorators/primitiveTrait"; +import ModelTraits from "../ModelTraits"; +import { traitClass } from "../Trait"; +import mixTraits from "../mixTraits"; +import CatalogMemberTraits from "./CatalogMemberTraits"; +import DiscretelyTimeVaryingTraits from "./DiscretelyTimeVaryingTraits"; +import ImageryProviderTraits from "./ImageryProviderTraits"; +import LayerOrderingTraits from "./LayerOrderingTraits"; +import LegendOwnerTraits from "./LegendOwnerTraits"; +import { MinMaxLevelTraits } from "./MinMaxLevelTraits"; +import UrlTraits from "./UrlTraits"; + +export class ArcGisImageServerRenderingRule extends ModelTraits { + @primitiveTrait({ + type: "string", + name: "Name", + description: "The name of the raster function." + }) + rasterFunction?: string; + + @primitiveTrait({ + type: "string", + name: "Description", + description: + 'optional for well known functions, default is "Raster" for raster function templates.' + }) + variableName?: string; + + @anyTrait({ + name: "Arguments", + description: + "Overwrite the raster function default configuration by specifying argument parameters (argument names and value types are defined by the author of each raster function template and are not discoverable through ArcGis REST API)." + }) + rasterFunctionArguments?: JsonObject; +} + +export class ArcGisImageServerAvailableRasterFunctionTraits extends ModelTraits { + @primitiveTrait({ + type: "string", + name: "Name", + description: "The name of the raster function." + }) + name?: string; + + @primitiveTrait({ + type: "string", + name: "Description", + description: "The description of the raster function." + }) + description?: string; + + @primitiveTrait({ + type: "string", + name: "Unit", + description: "Help text for the raster function" + }) + help?: string; +} + +@traitClass({ + example: { + url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/CharlotteLAS/ImageServer", + type: "esri-imageServer", + name: "CharlotteLAS" + } +}) +export default class ArcGisImageServerCatalogItemTraits extends mixTraits( + ImageryProviderTraits, + LayerOrderingTraits, + UrlTraits, + CatalogMemberTraits, + LegendOwnerTraits, + DiscretelyTimeVaryingTraits, + MinMaxLevelTraits +) { + @primitiveTrait({ + type: "number", + name: "Maximum scale", + description: + "Gets or sets the denominator of the largest scale (smallest denominator) for which tiles should be requested. For example, if this value is 1000, then tiles representing a scale larger than 1:1000 (i.e. numerically smaller denominator, when zooming in closer) will not be requested. Instead, tiles of the largest-available scale, as specified by this property, will be used and will simply get blurier as the user zooms in closer. Note: maximumLevel overrides this property." + }) + maximumScale?: number; + + @anyTrait({ + name: "Parameters", + description: + "Additional parameters to pass to the ImageServer when requesting images." + }) + parameters?: JsonObject; + + @primitiveTrait({ + name: "Token URL", + description: "URL to use for fetching request tokens", + type: "string" + }) + tokenUrl?: string; + + @primitiveTrait({ + name: "Token", + description: + "Token to use for fetching request tokens (if not using tokenUrl)", + type: "string" + }) + token?: string; + + @primitiveTrait({ + type: "number", + name: "Maximum Refresh Intervals", + description: + "The maximum number of discrete times that can be created by a single " + + "date range when layer in time-enabled." + }) + maxRefreshIntervals: number = 10000; + + @primitiveTrait({ + type: "number", + name: "WKID", + description: + "The well-known ID of the spatial reference of the image server. Only Web Mercator (102100 or 102113 or 3857) and WGS84 (4326) are supported." + }) + wkid?: number = 102100; + + @primitiveTrait({ + name: "Use Pre-Cached Tiles", + description: + "If true, the server's pre-cached tiles are used. If false, then the ImageServer exportImage endpoint will be used. This will default to false if parameters (including time) have been specified, otherwise it will default to true if a server supports pre-cached tiles.", + type: "boolean" + }) + usePreCachedTiles?: boolean; + + @primitiveArrayTrait({ + name: "Band IDs", + description: "The band IDs to use when requesting images.", + type: "number" + }) + bandIds?: number[]; + + @objectTrait({ + name: "Rendering Rule", + description: + 'The rendering rule to apply to the image service. This must be a JSON object - for example `{"rasterFunction": "RFTAspectColor"}`. Note `allowRasterFunction` must be true for this to be applied.', + type: ArcGisImageServerRenderingRule + }) + renderingRule?: ArcGisImageServerRenderingRule; + + @primitiveTrait({ + name: "Allow Raster Function", + description: + "If true, then the renderingRule will be applied to the image service. If false, the renderingRule will be ignored. This will default to true if an image service supports raster functions.", + type: "boolean" + }) + allowRasterFunction?: boolean; + + @objectArrayTrait({ + type: ArcGisImageServerAvailableRasterFunctionTraits, + name: "Available raster functions", + description: + "The available raster functions for the ImageServer. Defaults to all raster functions in the service if the server supports raster functions. Note: `allowRasterFunction` must be true. To set the default raster function, use the `renderingRule` property.", + idProperty: "name" + }) + availableRasterFunctions?: ArcGisImageServerAvailableRasterFunctionTraits[]; + + @primitiveTrait({ + name: "Disable raster functions selectors", + description: + "When true, disables the dimension selectors in the workbench. This will default to true if the server does not support raster functions.", + type: "boolean" + }) + disableRasterFunctionSelectors?: boolean; +} diff --git a/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts b/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts index 269ba456624..0778cbc4525 100644 --- a/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts @@ -67,7 +67,7 @@ export default class ArcGisMapServerCatalogItemTraits extends mixTraits( "The maximum number of discrete times that can be created by a single " + "date range when layer in time-enabled." }) - maxRefreshIntervals: number = 1000; + maxRefreshIntervals: number = 10000; @primitiveTrait({ name: "Time Window Duration", @@ -94,7 +94,7 @@ export default class ArcGisMapServerCatalogItemTraits extends mixTraits( isForwardTimeWindow: boolean = true; @primitiveTrait({ - name: "Is Forward Time Window", + name: "Use Pre-Cached Tiles If Available", description: "If true, the server's pre-cached tiles are used if they are available. If false, then the MapServer export endpoint will be used. This will default to true if no specific layers are fetched (i.e. all layers are fetched). Otherwise, it will default to false. This will also default to false if parameters have been specified", type: "boolean" diff --git a/lib/Traits/TraitsClasses/CatalogMemberTraits.ts b/lib/Traits/TraitsClasses/CatalogMemberTraits.ts index 9ddf821411d..60515d25038 100644 --- a/lib/Traits/TraitsClasses/CatalogMemberTraits.ts +++ b/lib/Traits/TraitsClasses/CatalogMemberTraits.ts @@ -100,6 +100,7 @@ export class ShortReportTraits extends ModelTraits { show = true; } +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ class CatalogMemberTraits extends ModelTraits { @primitiveTrait({ type: "string", @@ -238,8 +239,17 @@ class CatalogMemberTraits extends ModelTraits { description: "Disables the 'About Data' button in the workbench." }) disableAboutData?: boolean; + + @primitiveTrait({ + type: "boolean", + name: "Shareable", + description: + "True (default) if this catalog member may be included in share links. False to exclude it from share links." + }) + shareable: boolean = true; } +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ interface CatalogMemberTraits { // Add traits here that you want to override from some Mixin or Model class // without generating TS2611 type error. diff --git a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts index 6abd74fb01f..69b2143e288 100644 --- a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts @@ -12,7 +12,6 @@ import UrlTraits from "./UrlTraits"; @traitClass({ description: `Creates a 3d tiles item in the catalog from an ION Asset ID. - Note: Instead of specifying ionAssetId property, you can also provide a URL, for example, "url": "https://storage.googleapis.com/vic-datasets-public/1ce41fe7-aed2-4ad3-be4d-c38b715ce9af/v1/tileset.json".`, example: { type: "3d-tiles", diff --git a/lib/Traits/TraitsClasses/Cesium3dTilesTraits.ts b/lib/Traits/TraitsClasses/Cesium3dTilesTraits.ts index b3e88b8b9cb..c83849c49a5 100644 --- a/lib/Traits/TraitsClasses/Cesium3dTilesTraits.ts +++ b/lib/Traits/TraitsClasses/Cesium3dTilesTraits.ts @@ -18,6 +18,7 @@ import SplitterTraits from "./SplitterTraits"; import TransformationTraits from "./TransformationTraits"; import UrlTraits from "./UrlTraits"; import FeaturePickingTraits from "./FeaturePickingTraits"; +import CesiumIonTraits from "./CesiumIonTraits"; export class FilterTraits extends ModelTraits { @primitiveTrait({ @@ -123,29 +124,9 @@ export default class Cesium3DTilesTraits extends mixTraits( LegendOwnerTraits, ShadowTraits, ClippingPlanesTraits, - SplitterTraits + SplitterTraits, + CesiumIonTraits ) { - @primitiveTrait({ - type: "number", - name: "Ion asset ID", - description: "The Cesium Ion asset id." - }) - ionAssetId?: number; - - @primitiveTrait({ - type: "string", - name: "Ion access token", - description: "Cesium Ion access token id." - }) - ionAccessToken?: string; - - @primitiveTrait({ - type: "string", - name: "Ion server", - description: "URL of the Cesium Ion API server." - }) - ionServer?: string; - @objectTrait({ type: OptionsTraits, name: "options", diff --git a/lib/Traits/TraitsClasses/CesiumIonTraits.ts b/lib/Traits/TraitsClasses/CesiumIonTraits.ts new file mode 100644 index 00000000000..72a1ff7a2cc --- /dev/null +++ b/lib/Traits/TraitsClasses/CesiumIonTraits.ts @@ -0,0 +1,28 @@ +import primitiveTrait from "../Decorators/primitiveTrait"; +import ModelTraits from "../ModelTraits"; + +export default class CesiumIonTraits extends ModelTraits { + @primitiveTrait({ + type: "number", + name: "Cesium ion asset ID", + description: + "The Cesium ion asset id. If this is set then the `url` is ignored" + }) + ionAssetId?: number; + + @primitiveTrait({ + type: "string", + name: "Cesium ion access token", + description: + "Cesium ion access token. If not specified, the default token is used." + }) + ionAccessToken?: string; + + @primitiveTrait({ + type: "string", + name: "Ion server", + description: + "URL of the Cesium ion API server. If not specified, the default Ion server, `https://api.cesium.com/`, is used." + }) + ionServer?: string; +} diff --git a/lib/Traits/TraitsClasses/CesiumTerrainCatalogItemTraits.ts b/lib/Traits/TraitsClasses/CesiumTerrainCatalogItemTraits.ts index 7558d3cfed8..60a4de6bef1 100644 --- a/lib/Traits/TraitsClasses/CesiumTerrainCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/CesiumTerrainCatalogItemTraits.ts @@ -1,9 +1,9 @@ import CatalogMemberTraits from "./CatalogMemberTraits"; import MappableTraits from "./MappableTraits"; import mixTraits from "../mixTraits"; -import primitiveTrait from "../Decorators/primitiveTrait"; import UrlTraits from "./UrlTraits"; import { traitClass } from "../Trait"; +import CesiumIonTraits from "./CesiumIonTraits"; @traitClass({ description: `Creates a Cesium terrain item in the catalog from url.`, @@ -19,28 +19,6 @@ import { traitClass } from "../Trait"; export default class CesiumTerrainCatalogItemTraits extends mixTraits( UrlTraits, MappableTraits, - CatalogMemberTraits -) { - @primitiveTrait({ - name: "Ion Asset ID", - type: "number", - description: "The ID of the Cesium Ion Asset. If this is set url is ignored" - }) - ionAssetId?: number; - - @primitiveTrait({ - name: "Ion Access Token", - type: "string", - description: - "The Cesium Ion access token to use to access the terrain. If not specified, the token" - }) - ionAccessToken?: string; - - @primitiveTrait({ - name: "Ion Server", - type: "string", - description: - "the Cesium Ion access token to use to access the terrain. If not specified, the default Ion server, `https://api.cesium.com/`" - }) - ionServer?: string; -} + CatalogMemberTraits, + CesiumIonTraits +) {} diff --git a/lib/Traits/TraitsClasses/CogCatalogItemTraits.ts b/lib/Traits/TraitsClasses/CogCatalogItemTraits.ts new file mode 100644 index 00000000000..d6929891005 --- /dev/null +++ b/lib/Traits/TraitsClasses/CogCatalogItemTraits.ts @@ -0,0 +1,89 @@ +import objectTrait from "../Decorators/objectTrait"; +import primitiveTrait from "../Decorators/primitiveTrait"; +import ModelTraits from "../ModelTraits"; +import { traitClass } from "../Trait"; +import mixTraits from "../mixTraits"; +import CatalogMemberTraits from "./CatalogMemberTraits"; +import ImageryProviderTraits from "./ImageryProviderTraits"; +import LayerOrderingTraits from "./LayerOrderingTraits"; +import LegendOwnerTraits from "./LegendOwnerTraits"; +import MappableTraits from "./MappableTraits"; +import UrlTraits from "./UrlTraits"; + +export class CogRenderOptionsTraits extends ModelTraits { + @primitiveTrait({ + type: "number", + name: "No Data Value", + description: "No data value, default read from tiff meta" + }) + nodata?: number; + + @primitiveTrait({ + type: "boolean", + name: "Convert to RGB", + description: "Try to render multi band cog to RGB, priority 1" + }) + convertToRGB?: boolean; + + @primitiveTrait({ + type: "string", + name: "Resample Method", + description: "Geotiff resample method. Defaults to `bilinear`." + }) + resampleMethod?: "nearest" | "bilinear" = "nearest"; +} + +@traitClass({ + description: + "Creates a Cloud Optimised Geotiff item in the catalog from a url pointing to a TIFF that is a valid COG.", + example: { + name: "COG Test Uluru", + description: + "This is a COG from Sentinel-2 L2A, in EPSG:32752. Does it display in correct location? Does it display correctly?", + type: "cog", + url: "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/52/J/FS/2023/5/S2A_52JFS_20230501_0_L2A/TCI.tif" + } +}) +export default class CogCatalogItemTraits extends mixTraits( + ImageryProviderTraits, + LayerOrderingTraits, + UrlTraits, + MappableTraits, + CatalogMemberTraits, + LegendOwnerTraits +) { + @objectTrait({ + type: CogRenderOptionsTraits, + name: "Render Options", + description: "Render options for COGs" + }) + renderOptions?: CogRenderOptionsTraits; + + @primitiveTrait({ + type: "string", + name: "Credit", + description: "Credit for the imagery provider." + }) + credit?: string; + + @primitiveTrait({ + type: "number", + name: "Tile Size", + description: "The size of the tile." + }) + tileSize?: number; + + @primitiveTrait({ + type: "boolean", + name: "Has Alpha Channel", + description: "Whether the imagery has an alpha channel." + }) + hasAlphaChannel?: boolean; + + @primitiveTrait({ + type: "number", + name: "Cache", + description: "Cache survival time in milliseconds." + }) + cache?: number; +} diff --git a/lib/Traits/TraitsClasses/CzmlCatalogItemTraits.ts b/lib/Traits/TraitsClasses/CzmlCatalogItemTraits.ts index cfd267a0f6f..51fb9b0cd1c 100644 --- a/lib/Traits/TraitsClasses/CzmlCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/CzmlCatalogItemTraits.ts @@ -5,6 +5,7 @@ import { traitClass } from "../Trait"; import mixTraits from "../mixTraits"; import AutoRefreshingTraits from "./AutoRefreshingTraits"; import CatalogMemberTraits from "./CatalogMemberTraits"; +import CesiumIonTraits from "./CesiumIonTraits"; import LegendOwnerTraits from "./LegendOwnerTraits"; import MappableTraits from "./MappableTraits"; import TimeVaryingTraits from "./TimeVaryingTraits"; @@ -12,7 +13,7 @@ import UrlTraits from "./UrlTraits"; @traitClass({ description: `Creates one catalog item from url that points to a czml file. - + Note: If the model is not visible, try to disable the terrain by unchecking the box "Terrain hides underground features".`, example: { type: "czml", @@ -27,7 +28,8 @@ export default class CzmlCatalogItemTraits extends mixTraits( UrlTraits, CatalogMemberTraits, LegendOwnerTraits, - MappableTraits + MappableTraits, + CesiumIonTraits ) { @anyTrait({ name: "CZML Data", diff --git a/lib/Traits/TraitsClasses/DiffableTraits.ts b/lib/Traits/TraitsClasses/DiffableTraits.ts index 28d20559a91..07a32251890 100644 --- a/lib/Traits/TraitsClasses/DiffableTraits.ts +++ b/lib/Traits/TraitsClasses/DiffableTraits.ts @@ -1,3 +1,5 @@ +import { JsonObject } from "../../Core/Json"; +import anyTrait from "../Decorators/anyTrait"; import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; import primitiveTrait from "../Decorators/primitiveTrait"; import mixTraits from "../mixTraits"; @@ -39,4 +41,10 @@ export default class DiffableTraits extends mixTraits(TimeFilterTraits) { description: "The ID of the style used to compute the difference image" }) diffStyleId?: string; + + @anyTrait({ + name: "Difference item properties", + description: "Additional properties to set on the difference output item." + }) + diffItemProperties?: JsonObject; } diff --git a/lib/Traits/TraitsClasses/GeoJsonCatalogItemTraits.ts b/lib/Traits/TraitsClasses/GeoJsonCatalogItemTraits.ts index 2d8ec988934..fcfbe57b1ba 100644 --- a/lib/Traits/TraitsClasses/GeoJsonCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/GeoJsonCatalogItemTraits.ts @@ -5,6 +5,7 @@ import primitiveTrait from "../Decorators/primitiveTrait"; import { traitClass } from "../Trait"; import mixTraits from "../mixTraits"; import ApiRequestTraits from "./ApiRequestTraits"; +import CesiumIonTraits from "./CesiumIonTraits"; import { GeoJsonTraits } from "./GeoJsonTraits"; @traitClass({ @@ -18,7 +19,8 @@ import { GeoJsonTraits } from "./GeoJsonTraits"; }) export default class GeoJsonCatalogItemTraits extends mixTraits( GeoJsonTraits, - ApiRequestTraits + ApiRequestTraits, + CesiumIonTraits ) { @objectArrayTrait({ type: ApiRequestTraits, diff --git a/lib/Traits/TraitsClasses/GltfCatalogItemTraits.ts b/lib/Traits/TraitsClasses/GltfCatalogItemTraits.ts index a20aec67960..d0371b4017f 100644 --- a/lib/Traits/TraitsClasses/GltfCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/GltfCatalogItemTraits.ts @@ -1,6 +1,7 @@ import { traitClass } from "../Trait"; import mixTraits from "../mixTraits"; import AutoRefreshingTraits from "./AutoRefreshingTraits"; +import CesiumIonTraits from "./CesiumIonTraits"; import GltfTraits from "./GltfTraits"; import PlaceEditorTraits from "./PlaceEditorTraits"; import UrlTraits from "./UrlTraits"; @@ -24,5 +25,6 @@ export default class GltfCatalogItemTraits extends mixTraits( UrlTraits, AutoRefreshingTraits, PlaceEditorTraits, - GltfTraits + GltfTraits, + CesiumIonTraits ) {} diff --git a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts new file mode 100644 index 00000000000..7604da9845d --- /dev/null +++ b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts @@ -0,0 +1,17 @@ +import { traitClass } from "../Trait"; +import mixTraits from "../mixTraits"; +import I3STraits from "./I3STraits"; +import Cesium3DTilesCatalogItemTraits from "./Cesium3DTilesCatalogItemTraits"; + +@traitClass({ + description: `Creates an I3S item in the catalog from an slpk.`, + example: { + type: "I3S", + name: "CoM Melbourne 3D Photo Mesh", + id: "some-unique-id" + } +}) +export default class I3SCatalogItemTraits extends mixTraits( + Cesium3DTilesCatalogItemTraits, + I3STraits +) {} diff --git a/lib/Traits/TraitsClasses/I3STraits.ts b/lib/Traits/TraitsClasses/I3STraits.ts new file mode 100644 index 00000000000..715efaa8cfb --- /dev/null +++ b/lib/Traits/TraitsClasses/I3STraits.ts @@ -0,0 +1,20 @@ +import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; +import primitiveTrait from "../Decorators/primitiveTrait"; +import ModelTraits from "../ModelTraits"; + +export default class I3STraits extends ModelTraits { + @primitiveTrait({ + type: "string", + name: "Terrain URL", + description: + "URL to construct ArcGISTiledElevationTerrainProvider for I3S geometry." + }) + terrainURL?: string; + + @primitiveArrayTrait({ + type: "number", + name: "Image based lighting factor", + description: "Cartesian2 of lighting factor for imageBasedLightingFactor" + }) + lightingFactor?: [number, number]; +} diff --git a/lib/Traits/TraitsClasses/KmlCatalogItemTraits.ts b/lib/Traits/TraitsClasses/KmlCatalogItemTraits.ts index 76713ee85c2..482e5ee974c 100644 --- a/lib/Traits/TraitsClasses/KmlCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/KmlCatalogItemTraits.ts @@ -1,6 +1,7 @@ import primitiveTrait from "../Decorators/primitiveTrait"; import mixTraits from "../mixTraits"; import CatalogMemberTraits from "./CatalogMemberTraits"; +import CesiumIonTraits from "./CesiumIonTraits"; import LegendOwnerTraits from "./LegendOwnerTraits"; import MappableTraits from "./MappableTraits"; import UrlTraits from "./UrlTraits"; @@ -9,7 +10,8 @@ export default class KmlCatalogItemTraits extends mixTraits( UrlTraits, MappableTraits, CatalogMemberTraits, - LegendOwnerTraits + LegendOwnerTraits, + CesiumIonTraits ) { @primitiveTrait({ type: "string", diff --git a/lib/Traits/TraitsClasses/MappableTraits.ts b/lib/Traits/TraitsClasses/MappableTraits.ts index f50d45e7207..a46678a3290 100644 --- a/lib/Traits/TraitsClasses/MappableTraits.ts +++ b/lib/Traits/TraitsClasses/MappableTraits.ts @@ -199,6 +199,7 @@ export class InitialMessageTraits extends ModelTraits { height?: number; } +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ class MappableTraits extends mixTraits(AttributionTraits) { @objectTrait({ type: RectangleTraits, @@ -280,6 +281,7 @@ class MappableTraits extends mixTraits(AttributionTraits) { maximumShownFeatureInfos?: number; } +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ interface MappableTraits { // Add traits here that you want to override from some Mixin or Model class // without generating TS2611 type error. diff --git a/lib/Traits/TraitsClasses/UrlTraits.ts b/lib/Traits/TraitsClasses/UrlTraits.ts index f878e92a742..52520d2b8e1 100644 --- a/lib/Traits/TraitsClasses/UrlTraits.ts +++ b/lib/Traits/TraitsClasses/UrlTraits.ts @@ -1,6 +1,7 @@ import ModelTraits from "../ModelTraits"; import primitiveTrait from "../Decorators/primitiveTrait"; +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ class UrlTraits extends ModelTraits { @primitiveTrait({ type: "string", @@ -25,6 +26,7 @@ class UrlTraits extends ModelTraits { cacheDuration?: string; } +/* eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging */ interface UrlTraits { // Add traits here that you want to override from some Mixin or Model class // without generating TS2611 type error. diff --git a/lib/Traits/TraitsClasses/WebMapServiceCatalogItemTraits.ts b/lib/Traits/TraitsClasses/WebMapServiceCatalogItemTraits.ts index deb3ddffc32..459858448ae 100644 --- a/lib/Traits/TraitsClasses/WebMapServiceCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/WebMapServiceCatalogItemTraits.ts @@ -239,7 +239,7 @@ export default class WebMapServiceCatalogItemTraits extends mixTraits( "date range, when specified in the format time/time/periodicity. E.g. " + "`2015-04-27T16:15:00/2015-04-27T18:45:00/PT15M` has 11 times." }) - maxRefreshIntervals: number = 1000; + maxRefreshIntervals: number = 10000; @primitiveTrait({ type: "boolean", diff --git a/package.json b/package.json index 8602e5459a8..fd43252e2a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "terriajs", - "version": "8.7.3", + "version": "8.7.7", "description": "Geospatial data visualization platform.", "license": "Apache-2.0", "engines": { @@ -42,12 +42,11 @@ "@turf/meta": "^6.5.0", "@types/arcgis-rest-api": "^10.4.5", "@types/create-react-class": "^15.6.2", - "@types/d3-array": "^2.0.0", - "@types/d3-axis": "^1.0.12", + "@types/d3-array": "^3.2.1", "@types/d3-color": "^3.0.0", - "@types/d3-scale-chromatic": "^2.0.0", - "@types/d3-selection": "^1.4.1", - "@types/d3-transition": "^1.1.4", + "@types/d3-scale-chromatic": "^3.0.3", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", "@types/dateformat": "^3.0.1", "@types/dompurify": "^2.3.1", "@types/file-saver": "^1.3.0", @@ -55,17 +54,18 @@ "@types/fs-extra": "^7.0.0", "@types/jasmine": "^2.8.8", "@types/jasmine-ajax": "^3.3.0", - "@types/json5": "^0.0.30", - "@types/leaflet": "^1.7.10", + "@types/leaflet": "^1.9.12", + "@types/linkify-it": "^3.0.5", "@types/lodash-es": "^4.17.3", + "@types/markdown-it": "^14.0.1", "@types/math-expression-evaluator": "^1.2.0", "@types/ms": "^0.7.31", "@types/mustache": "^0.8.32", "@types/node-fetch": "^2.6.2", "@types/papaparse": "^4.5.9", "@types/pbf": "^3.0.1", + "@types/proj4": "^2.5.5", "@types/rbush": "^3.0.0", - "@types/rc-slider": "^8.6.6", "@types/react": "^17.0.3", "@types/react-color": "^3.0.6", "@types/react-dom": "^17.0.2", @@ -97,21 +97,18 @@ "commander": "^11.1.0 ", "copy-webpack-plugin": "^6.4.0", "core-js": "^3.1.4", + "create-react-class": "^15.7.0", "css-loader": "^2.1.0", "css-modules-typescript-loader": "^2.0.4", - "d3-array": "^1.0.0", - "d3-axis": "^1.0.0", - "d3-collection": "^1.0.0", - "d3-color": "^3.0.1", - "d3-dispatch": "^1.0.5", - "d3-ease": "^1.0.5", - "d3-interpolate": "^1.3.2", - "d3-scale": "^2.2.2", - "d3-scale-chromatic": "^2.0.0", - "d3-selection": "^1.0.0", - "d3-shape": "^1.0.0", - "d3-transition": "^1.0.0", - "d3-zoom": "^1.8.3", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale-chromatic": "^3.1.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-transition": "^3.0.1", + "d3-zoom": "^3.0.0", "dateformat": "^3.0.3", "dompurify": "^2.3.3", "fetch-mock": "^9.11.0", @@ -133,9 +130,9 @@ "javascript-natural-sort": "^0.7.1", "json5": "^2.1.0", "leaflet": "^1.8.0", - "linkify-it": "^2.0.0", + "linkify-it": "^5.0.0", "lodash-es": "^4.17.11", - "markdown-it": "^11.0.0", + "markdown-it": "^14.1.0", "math-expression-evaluator": "^1.3.7", "mini-css-extract-plugin": "^0.5.0", "minisearch": "^3.0.2", @@ -150,6 +147,7 @@ "pbf": "^3.0.1", "point-in-polygon": "^1.0.1", "proj4": "^2.4.4", + "proj4-fully-loaded": "^0.2.0", "prop-types": "^15.6.0", "protomaps": "1.19.0", "raw-loader": "^1.0.0", @@ -169,7 +167,6 @@ "react-virtual": "~2.3.2", "resolve-url-loader": "^5.0.0", "retry": "^0.12.0", - "rollbar": "^2.24.0", "sass-loader": "^10", "shpjs": "^4.0.4", "simple-statistics": "^7.0.1", @@ -177,9 +174,10 @@ "style-loader": "^0.23.1", "styled-components": "^5.3.9", "svg-sprite-loader": "^6.0.11", - "terriajs-cesium": "8.0.0", + "terriajs-cesium": "8.0.1", "terriajs-cesium-widgets": "5.0.0", "terriajs-html2canvas": "1.0.0-alpha.12-terriajs-1", + "terriajs-tiff-imagery-provider": "2.13.3", "thredds-catalog-crawler": "0.0.7", "ts-essentials": "^5.0.0", "ts-loader": "^5.3.3", @@ -235,7 +233,7 @@ "react-shallow-testutils": "^3.0.0", "react-test-renderer": "^16.3.2", "regenerator-runtime": "^0.13.2", - "sass": "^1.66.1", + "sass": "^1.79.1", "terriajs-server": "^4.0.0", "utf-8-validate": "^6.0.3", "yaml": "^1.10.0" diff --git a/test/Core/markdownToHtmlSpec.ts b/test/Core/markdownToHtmlSpec.ts index 57d03723a42..4050bf8adcd 100644 --- a/test/Core/markdownToHtmlSpec.ts +++ b/test/Core/markdownToHtmlSpec.ts @@ -1,15 +1,9 @@ import registerCustomComponentTypes from "../../lib/ReactViews/Custom/registerCustomComponentTypes"; import markdownToHtml from "../../lib/Core/markdownToHtml"; -import Terria from "../../lib/Models/Terria"; import CustomComponent from "../../lib/ReactViews/Custom/CustomComponent"; describe("markdownToHtml", function () { - let terria: Terria; - beforeEach(function () { - terria = new Terria({ - baseUrl: "./" - }); registerCustomComponentTypes(); }); it("correctly injects terria's custom tooltips", function () { diff --git a/test/Map/TableColumnSpec.js b/test/Map/TableColumnSpec.js deleted file mode 100644 index eb3d1981b7e..00000000000 --- a/test/Map/TableColumnSpec.js +++ /dev/null @@ -1,288 +0,0 @@ -"use strict"; - -var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default; -var TableColumn = require("../../lib/Map/TableColumn"); -var VarType = require("../../lib/Map/VarType"); -var VarSubType = require("../../lib/Map/VarSubType"); - -describe("TableColumn", function () { - it("can make a new object and detect scalar type", function () { - // Use a copy of data to make the column, because knockout adds stuff to data. - // Also, test a "slice" of the column's values, to remove knockout stuff. - var data = [1, 3, 4]; - var tableColumn = new TableColumn("x", data.slice()); - expect(tableColumn.name).toEqual("x"); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.type).toEqual(VarType.SCALAR); - }); - - it("treats null, NA and hyphens as null in numeric data", function () { - var data = [1, "NA", 4, "-", null, 3]; - var tableColumn = new TableColumn("x", data.slice()); - expect(tableColumn.name).toEqual("x"); - expect(tableColumn.values.slice()).toEqual([1, null, 4, null, null, 3]); - expect(tableColumn.type).toEqual(VarType.SCALAR); - }); - - it("replaces null values before generating numericalValues", function () { - var data = [0, 0, 0]; - var tableColumn = new TableColumn("x", data.slice(), { - replaceWithNullValues: [0] - }); - expect(tableColumn.numericalValues.slice()).toEqual([]); - }); - - it("treats hyphens, blanks and NA as strings in string data", function () { - var data = ["%", "-", "!", "NA", ""]; - var tableColumn = new TableColumn("x", data.slice()); - expect(tableColumn.name).toEqual("x"); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.type).toEqual(VarType.ENUM); - }); - - it("provides a null index for a null value in string data", function () { - var data = ["small", "medium", null, "big"]; - var tableColumn = new TableColumn("size", data.slice()); - expect(tableColumn.type).toEqual(VarType.ENUM); - expect(tableColumn.isEnum).toBe(true); - expect(tableColumn.values[1]).not.toBe(null); - expect(tableColumn.values[2]).toBe(null); - }); - - it("ignores missing values when calculating min/max", function () { - var data = [130.3, 131.3, null, 133.3]; - var tableColumn = new TableColumn("lat", data.slice()); - expect(tableColumn.maximumValue).toBe(133.3); - expect(tableColumn.minimumValue).toBe(130.3); - }); - - it("can detect latitude type", function () { - var data = [30.3, 31.3, 33.3]; - var tableColumn = new TableColumn("lat", data.slice()); - expect(tableColumn.type).toEqual(VarType.LAT); - }); - - it("can detect longitude type", function () { - var data = [130.3, 131.3, 133.3]; - var tableColumn = new TableColumn("lon", data.slice()); - expect(tableColumn.type).toEqual(VarType.LON); - }); - - it("can detect address type", function () { - var data = ["7 London Circuit Canberra City ACT 2601"]; - var tableColumn = new TableColumn("address", data); - expect(tableColumn.type).toEqual(VarType.ADDR); - }); - - it("can detect time type from yyyy-mm-dd", function () { - // Dates in this format are interpreted as midnight _UTC_ on the date. - var data = ["2016-01-03", null, "2016-01-04"]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - - var expected = JulianDate.toDate(JulianDate.fromIso8601("2016-01-03")); - expect(tableColumn.dates[0]).toEqual(expected); - }); - - it("can detect time type from dd-mm-yyyy", function () { - // Dates in this format are interpreted as midnight _local time_ on the date. - var data = ["31-12-2015", "04-01-2016", null]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[1].getDate()).toEqual(4); - expect(tableColumn.dates[1].getMonth()).toEqual(0); // January is month 0 - expect(tableColumn.dates[1].getFullYear()).toEqual(2016); - }); - - it("can detect time type from mm-dd-yyyy", function () { - // Dates in this format are interpreted as midnight _local time_ on the date. - var data = ["12-31-2015", "01-04-2016", null]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[1].getDate()).toEqual(4); - expect(tableColumn.dates[1].getMonth()).toEqual(0); // January is month 0 - expect(tableColumn.dates[1].getFullYear()).toEqual(2016); - }); - - it("can detect ISO8601 UTC time type", function () { - var data = ["2016-01-03T12:15:59.1234Z", null, "2016-01-03T12:25:00Z"]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[0].getUTCDate()).toEqual(3); - expect(tableColumn.dates[0].getUTCMonth()).toEqual(0); // January is month 0 - expect(tableColumn.dates[0].getUTCFullYear()).toEqual(2016); - expect(tableColumn.dates[0].getUTCHours()).toEqual(12); - expect(tableColumn.dates[0].getUTCMinutes()).toEqual(15); - expect(tableColumn.dates[0].getUTCSeconds()).toEqual(59); - expect(tableColumn.dates[0].getUTCMilliseconds()).toEqual(123); - }); - - it("can detect time type and year subtype from yyyy", function () { - var data = ["2010", "2011", "2012", null, "2013"]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.subtype).toEqual(VarSubType.YEAR); - expect(tableColumn.values.slice()).toEqual(data); - // don't test equality using new Date() because different browsers handle timezones differently - // so just check the date is right. - expect(tableColumn.dates[0].getDate()).toEqual(1); - expect(tableColumn.dates[0].getMonth()).toEqual(0); // January is month 0 - expect(tableColumn.dates[0].getFullYear()).toEqual(2010); - }); - - it("can detect time type from yyyy-mm", function () { - // Dates in this format are interpreted as midnight _UTC_ on the date. - var data = ["2010-01", "2010-02", "2010-03", null, "2010-04"]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - var expected = JulianDate.toDate(JulianDate.fromIso8601("2010-02")); - expect(tableColumn.dates[1]).toEqual(expected); - }); - - // This format can actually work, but we don't want to encourage it. - // it('can detect time type from yyyy/mm/dd h:mm:ss', function() { - // var data = ['2010/02/12 12:34:56', '2010/02/13 1:23:45']; - // var tableColumn = new TableColumn('date', data); - // expect(tableColumn.type).toEqual(VarType.TIME); - // expect(tableColumn.values).toEqual(data); - // expect(tableColumn.dates[1].getDate()).toEqual(13); - // expect(tableColumn.dates[1].getMonth()).toEqual(1); // January is month 0 - // expect(tableColumn.dates[1].getFullYear()).toEqual(2010); - // expect(tableColumn.dates[1].getHours()).toEqual(1); - // expect(tableColumn.dates[1].getMinutes()).toEqual(23); - // expect(tableColumn.dates[1].getSeconds()).toEqual(45); - // }); - - it("can detect time type from yyyy-mm-dd h:mm", function () { - var data = ["2010-02-12 12:34", "2010-02-13 1:23", null]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[1].getDate()).toEqual(13); - expect(tableColumn.dates[1].getMonth()).toEqual(1); // January is month 0 - expect(tableColumn.dates[1].getFullYear()).toEqual(2010); - expect(tableColumn.dates[1].getHours()).toEqual(1); - expect(tableColumn.dates[1].getMinutes()).toEqual(23); - expect(tableColumn.dates[1].getSeconds()).toEqual(0); - }); - - it("can detect time type from yyyy-mm-dd h:mm:ss", function () { - var data = ["2010-02-12 12:34:56", "2010-02-13 1:23:45", null]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[1].getDate()).toEqual(13); - expect(tableColumn.dates[1].getMonth()).toEqual(1); // January is month 0 - expect(tableColumn.dates[1].getFullYear()).toEqual(2010); - expect(tableColumn.dates[1].getHours()).toEqual(1); - expect(tableColumn.dates[1].getMinutes()).toEqual(23); - expect(tableColumn.dates[1].getSeconds()).toEqual(45); - }); - - it("can detect time type from yyyy-Qx", function () { - var data = ["2010-Q1", "2010-Q2", "2010-Q3", null, "2010-Q4"]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.values.slice()).toEqual(data); - expect(tableColumn.dates[1].getDate()).toEqual(1); - expect(tableColumn.dates[1].getMonth()).toEqual(3); // January is month 0 - expect(tableColumn.dates[1].getFullYear()).toEqual(2010); - }); - - it("can detect year subtype using year title", function () { - var data = ["1066", "1776", "1788", "1901", null, "2220"]; - var tableColumn = new TableColumn("year", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.subtype).toEqual(VarSubType.YEAR); - }); - - it("detects years from numerical data in a column named time", function () { - var data = [730, 1230, null, 130]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.subtype).toEqual(VarSubType.YEAR); - expect(tableColumn.values.slice()).toEqual(data); - }); - - it("can handle missing times", function () { - var data = ["2016-01-03T12:15:59.1234Z", "-", "2016-01-04T12:25:00Z", null]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.TIME); - expect(tableColumn.dates[0].getUTCDate()).toEqual(3); - expect(tableColumn.dates[1]).toBeUndefined(); - expect(tableColumn.dates[2].getUTCDate()).toEqual(4); - }); - - it("treats numerical data >= 9999 in a column named time as scalars", function () { - var data = [9999, 1230, null, 130]; - var tableColumn = new TableColumn("date", data.slice()); - expect(tableColumn.type).toEqual(VarType.SCALAR); - expect(tableColumn.values.slice()).toEqual(data); - }); - - it("can detect tag type from ", function () { - var data = ['', '']; - var tableColumn = new TableColumn("image", data.slice()); - expect(tableColumn.type).toEqual(VarType.TAG); - }); - - it("can detect tag type from
    ", function () { - var data = ["
    ", "
    "]; - var tableColumn = new TableColumn("bar", data); - expect(tableColumn.type).toEqual(VarType.TAG); - }); - - it("can detect tag type from
    ", function () { - var data = ["
    Foo
    ", "
    Bar
    "]; - var tableColumn = new TableColumn("foo", data); - expect(tableColumn.type).toEqual(VarType.TAG); - }); - - it("does not use tag type for <<...>>", function () { - var data = ["<>", "<>"]; - var tableColumn = new TableColumn("who", data); - expect(tableColumn.type).toEqual(VarType.ENUM); - }); - - it("does not use tag type for ", function () { - var data = ["", ""]; - var tableColumn = new TableColumn("fee", data); - expect(tableColumn.type).toEqual(VarType.ENUM); - }); - - it("can sum three columns from array", function () { - var tableColumns = [ - new TableColumn("one", [10, 1]), - new TableColumn("two", [25, 2.5]), - new TableColumn("three", [-2, 6]) - ]; - var result = TableColumn.sumValues(tableColumns); - var target = [10 + 25 - 2, 1 + 2.5 + 6]; - expect(result).toEqual(target); - }); - - it("can sum three columns as arguments", function () { - var result = TableColumn.sumValues( - new TableColumn("one", [10, 1]), - new TableColumn("two", [25, 2.5]), - new TableColumn("three", [-2, 6]) - ); - var target = [10 + 25 - 2, 1 + 2.5 + 6]; - expect(result).toEqual(target); - }); - - it("can divide two columns' values", function () { - var result = TableColumn.divideValues( - new TableColumn("num", [40, 3, 8]), - new TableColumn("den", [10, 6, 0]), - "bad" - ); - var target = [4, 0.5, "bad"]; - expect(result).toEqual(target); - }); -}); diff --git a/test/Map/TableStructureSpec.js b/test/Map/TableStructureSpec.js deleted file mode 100644 index 55b4b24fc6b..00000000000 --- a/test/Map/TableStructureSpec.js +++ /dev/null @@ -1,921 +0,0 @@ -"use strict"; - -var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default; -var TableStructure = require("../../lib/Map/TableStructure"); -var TimeInterval = require("terriajs-cesium/Source/Core/TimeInterval").default; -var VarType = require("../../lib/Map/VarType"); - -var separator = ","; -var decimalPoint = "."; -if (typeof Intl === "object" && typeof Intl.NumberFormat === "function") { - separator = Intl.NumberFormat().format(1000)[1]; - decimalPoint = Intl.NumberFormat().format(0.5)[1]; -} - -describe("TableStructure", function () { - it("can read from json object", function () { - // Use a copy of data to make the column, because knockout adds stuff to data. - // Also, test a "slice" of the column's values, to remove knockout stuff. - var data = [ - ["x", "y"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data.slice()); - expect(tableStructure.columns.length).toEqual(2); - expect(tableStructure.columns[0].name).toEqual("x"); - expect(tableStructure.columns[0].values.slice()).toEqual([1, 3, 4]); - expect(tableStructure.columns[1].name).toEqual("y"); - expect(tableStructure.columns[1].values.slice()).toEqual([5, 8, -3]); - }); - - it("can read from csv string", function () { - var csvString = "x,y\r\n1,5\r\n3,8\r\n4,-3\r\n"; - var tableStructure = TableStructure.fromCsv(csvString); - expect(tableStructure.columns.length).toEqual(2); - expect(tableStructure.columns[0].name).toEqual("x"); - expect(tableStructure.columns[0].values.slice()).toEqual([1, 3, 4]); - expect(tableStructure.columns[1].name).toEqual("y"); - expect(tableStructure.columns[1].values.slice()).toEqual([5, 8, -3]); - }); - - it("can read from json object into existing structure", function () { - var data = [ - ["x", "y"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = new TableStructure(); - tableStructure.loadFromJson(data); - expect(tableStructure.columns.length).toEqual(2); - expect(tableStructure.columns[0].name).toEqual("x"); - expect(tableStructure.columns[0].values.slice()).toEqual([1, 3, 4]); - expect(tableStructure.columns[1].name).toEqual("y"); - expect(tableStructure.columns[1].values.slice()).toEqual([5, 8, -3]); - }); - - it("can read from csv string into existing structure", function () { - var csvString = "x,y\r\n1,5\r\n3,8\r\n4,-3\r\n"; - var tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns.length).toEqual(2); - expect(tableStructure.columns[0].name).toEqual("x"); - expect(tableStructure.columns[0].values.slice()).toEqual([1, 3, 4]); - expect(tableStructure.columns[1].name).toEqual("y"); - expect(tableStructure.columns[1].values.slice()).toEqual([5, 8, -3]); - }); - - it("can convert to ArrayOfColumns", function () { - var data = [ - ["x", "y"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data); - var columns = tableStructure.toArrayOfColumns(); - expect(columns.length).toEqual(2); - expect(columns[0]).toEqual(["x", 1, 3, 4]); - expect(columns[1]).toEqual(["y", 5, 8, -3]); - }); - - it("can convert to ArrayOfRows", function () { - var data = [ - ["x", "y"], - ["1", "5"], - ["3", "8"], - ["4", "-3"] - ]; - var tableStructure = TableStructure.fromJson(data); - var rows = tableStructure.toArrayOfRows(); - expect(rows.length).toEqual(4); - expect(rows).toEqual(data); - }); - - it("can convert to ArrayOfRows with formatting", function () { - var data = [ - ["x", "y"], - [1.678, 9.883], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - x: { format: { maximumFractionDigits: 0 } }, - y: { - name: "new y (,000)", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var target = [ - ["x", "new y (,000)"], - ["2", "9.9"], - ["54321", "12" + separator + "345"], - ["4", "-3"] - ]; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var rows = tableStructure.toArrayOfRows(); - expect(rows.length).toEqual(4); - expect(rows).toEqual(target); - }); - - it("can convert to ArrayOfRows with formatting and quotes if containing commas", function () { - var data = [ - ["x", "y"], - [1.678, 9.883], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - x: { format: { maximumFractionDigits: 0 } }, - y: { - name: "new y (,000)", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var target = [ - ["x", '"new y (,000)"'], - ["2", "9.9"], - ["54321", '"12' + separator + '345"'], - ["4", "-3"] - ]; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var rows = tableStructure.toArrayOfRows(undefined, undefined, true, true); // 4th argument requests the quotes. - expect(rows.length).toEqual(4); - expect(rows).toEqual(target); - }); - - it("can convert to ArrayOfRows with formatting and quotes if containing quotes", function () { - var data = [ - ["x", "y"], - [1.678, 9.883], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - x: { format: { maximumFractionDigits: 0 } }, - y: { - name: 'new y ("000")', - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var target = [ - ["x", '"new y (""000"")"'], - ["2", "9.9"], - ["54321", '"12' + separator + '345"'], - ["4", "-3"] - ]; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var rows = tableStructure.toArrayOfRows(undefined, undefined, true, true); // 4th argument requests the quotes. - expect(rows.length).toEqual(4); - expect(rows).toEqual(target); - }); - - it("can convert to csv", function () { - var data = [ - ["lat", "y"], - [1.678, 9.883], - [54321, 12345], - [4, -3] - ]; - var tableStructure = new TableStructure(); - tableStructure = tableStructure.loadFromJson(data); - var csvString = tableStructure.toCsvString(); - expect(csvString).toEqual("lat,y\n1.678,9.883\n54321,12345\n4,-3"); - }); - - it("can create a data URI", function () { - var data = [ - ["lat", "y"], - [1.6, -9.8] - ]; - var tableStructure = new TableStructure(); - // From json - tableStructure = tableStructure.loadFromJson(data); - var uri = tableStructure.toDataUri(); - expect(uri).toEqual("data:attachment/csv,lat%2Cy%0A1.6%2C-9.8"); - // From csv - var csvString = "lat,y\n1.6,-9.8"; - tableStructure.loadFromCsv(csvString); - uri = tableStructure.toDataUri(); - expect(uri).toEqual("data:attachment/csv,lat%2Cy%0A1.6%2C-9.8"); - }); - - it("can convert to row objects", function () { - var data = [ - ["lat", "y"], - [1, 5.12345], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data); - var rowObjects = tableStructure.toRowObjects(); - expect(rowObjects.length).toEqual(3); - // Scalar fields are converted to strings using toLocaleString, but not lat/lon. - // We could convert lat/lons too, if there's a reason to do it. - expect(rowObjects[0]).toEqual({ lat: 1, y: "5.12345" }); - expect(rowObjects[1]).toEqual({ lat: 3, y: "8" }); - expect(rowObjects[2]).toEqual({ lat: 4, y: "-3" }); - }); - - it("can convert to string and number row objects", function () { - var data = [ - ["x", "y"], - [1.678, -9.883], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - x: { format: { maximumFractionDigits: 0 } }, - y: { - name: "newy", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var rowObjects = tableStructure.toStringAndNumberRowObjects(); - expect(rowObjects.length).toEqual(3); - expect(rowObjects[0]).toEqual({ - string: { x: "2", newy: "-9" + decimalPoint + "9" }, - number: { x: 1.678, newy: -9.883 } - }); - expect(rowObjects[1]).toEqual({ - string: { x: "54321", newy: "12" + separator + "345" }, - number: { x: 54321, newy: 12345 } - }); - }); - - it("can convert to point arrays", function () { - var data = [ - ["a", "b", "c"], - [1, 2, 3], - [4, 5, 6], - [7, 8, 9] - ]; - var tableStructure = TableStructure.fromJson(data); - var xy = tableStructure.toPointArrays(); - expect(xy.length).toEqual(2); - expect(xy[0]).toEqual([ - { x: 1, y: 2 }, - { x: 4, y: 5 }, - { x: 7, y: 8 } - ]); - expect(xy[1]).toEqual([ - { x: 1, y: 3 }, - { x: 4, y: 6 }, - { x: 7, y: 9 } - ]); - }); - - it("can get column names", function () { - var data = [ - ["lat", "y"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data); - expect(tableStructure.getColumnNames()).toEqual(["lat", "y"]); - }); - - it("can get column with name", function () { - var data = [ - ["x", "y"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data); - expect(tableStructure.getColumnWithName("y")).toEqual( - tableStructure.columns[1] - ); - expect(tableStructure.getColumnWithName("z")).toBeUndefined(); - }); - - it("sets column types", function () { - var data = [ - ["x", "lat"], - [1, 5], - [3, 8], - [4, -3] - ]; - var tableStructure = TableStructure.fromJson(data); - expect(tableStructure.columnsByType[VarType.SCALAR].length).toEqual(1); - expect(tableStructure.columnsByType[VarType.SCALAR][0].name).toEqual("x"); - expect(tableStructure.columnsByType[VarType.LAT].length).toEqual(1); - expect(tableStructure.columnsByType[VarType.LAT][0].name).toEqual("lat"); - }); - - it("counts the final row of CSV files with no trailing linefeed(s)", function () { - var csvString = "postcode,value\n0800,1\n0885,2"; - var tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - - csvString = csvString + "\n"; - tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - - // The ABS returns a csv data file for Australia with two final linefeeds. - csvString = csvString + "\n"; - tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - }); - - it("ignores final blank rows of CSV files", function () { - var csvString = "postcode,value\n0800,1,\n0885,2,"; - var tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - - csvString = csvString + "\n"; - tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - - csvString = csvString + "\n\n\n\n\n"; - tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].values.length).toEqual(2); - expect(tableStructure.columns[1].values.length).toEqual(2); - }); - - it("can read csv string where column names are numbers", function () { - var csvString = "1,2\n9,8\n7,6"; - var tableStructure = new TableStructure(); - tableStructure.loadFromCsv(csvString); - expect(tableStructure.columns[0].name).toEqual("1"); - expect(tableStructure.columns[1].name).toEqual("2"); - }); - - it("can describe rows with dates with and without timezones nicely", function () { - var csvString = - "date,value\r\n2015-10-15T12:34:56,5\r\n2015-10-02T12:34:56Z,8\r\n2015-11-03\r\n"; - var tableStructure = TableStructure.fromCsv(csvString); - var htmls = tableStructure.toRowDescriptions(); - expect(htmls[0]).toContain("Thu Oct 15 2015 12:34:56"); // Thu 15 Oct would be nicer outside USA. - expect(htmls[0]).not.toContain("2015-10-15T12:34:56"); - var expectedDate1 = JulianDate.toDate( - JulianDate.fromIso8601("2015-10-02T12:34:56Z") - ); - expect(htmls[1]).toContain("" + expectedDate1); - expect(htmls[1]).not.toContain("2015-10-02T12:34:56"); - expect(htmls[2]).toContain(">2015-11-03<"); // No time is added when only the date is given. - }); - - it("can describe rows with formatting", function () { - var data = [ - ["x", "y"], - [1.678, 5.123], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - y: { - name: "new y", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var htmls = tableStructure.toRowDescriptions(); - expect(htmls[0]).toContain("new y"); - expect(htmls[0]).toContain("1.678"); - expect(htmls[0]).toContain("5.1"); - expect(htmls[0]).not.toContain("5.12"); - expect(htmls[1]).toContain("54321"); - expect(htmls[1]).toContain("12" + separator + "345"); - }); - - it("can tell if it has address data", function () { - var data = [ - ["x", "y", "Address"], - [1.678, 5.123, "25 Gozzard Street, GUNGAHLIN TOWN CENTRE, ACT"], - [54321, 12345, "137 Reed Street, TUGGERANONG, ACT"], - [4, -3, "81 Mildura Street, FYSHWICK, ACT"] - ]; - var options = { - columnOptions: { - y: { - name: "new y", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - expect(tableStructure.hasAddress).toBe(true); - - var dataNoAddr = [ - ["x", "y"], - [1.678, 5.123], - [54321, 12345], - [4, -3] - ]; - var optionsNoAddr = { - columnOptions: { - y: { - name: "new y", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var tableStructureNoAddr = new TableStructure("foo", optionsNoAddr); - tableStructureNoAddr = tableStructure.loadFromJson(dataNoAddr); - expect(tableStructureNoAddr.hasAddress).toBe(false); - }); - - it("can get feature id mapping", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2], - [1971, "A", 67.8, 1.2], - [1972, "B", 68.2, 2.2] - ]; - var options = { idColumnNames: ["id"] }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var map = tableStructure.getIdMapping(); - expect(map["A"]).toEqual([0, 2]); - expect(map["B"]).toEqual([1, 3]); - }); - - it("can handle idColumnNames = []", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2], - [1971, "A", 67.8, 1.2], - [1972, "B", 68.2, 2.2] - ]; - var options = { idColumnNames: [] }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(data); - var map = tableStructure.getIdMapping(); - expect(map).toEqual({}); - }); - - it("can append a table", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2] - ]; - var dat2 = [ - ["year", "id", "lat", "lon"], - [1980, "C", 16.8, 5.2], - [1981, "D", 16.2, 5.2] - ]; - var table1 = new TableStructure("foo"); - var table2 = new TableStructure("bar"); - table1 = table1.loadFromJson(data); - table2 = table2.loadFromJson(dat2); - table1.append(table2); - expect(table1.columns[0].values.slice()).toEqual([1970, 1971, 1980, 1981]); - expect(table1.columns[1].values.slice()).toEqual(["A", "B", "C", "D"]); - }); - - it("can append part of a table", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2] - ]; - var dat2 = [ - ["year", "id", "lat", "lon"], - [1980, "C", 16.8, 5.2], - [1981, "D", 16.2, 5.2], - [1982, "E", 16, 5], - [1983, "F", 15, 6] - ]; - var table1 = new TableStructure("foo"); - var table2 = new TableStructure("bar"); - table1 = table1.loadFromJson(data); - table2 = table2.loadFromJson(dat2); - table1.append(table2, [1, 3]); - expect(table1.columns[0].values.slice()).toEqual([1970, 1971, 1981, 1983]); - expect(table1.columns[1].values.slice()).toEqual(["A", "B", "D", "F"]); - }); - - it("can replace rows", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2] - ]; - var dat2 = [ - ["year", "id", "lat", "lon"], - [1980, "C", 16.8, 5.2], - [1981, "D", 16.2, 5.2] - ]; - var table1 = new TableStructure("foo"); - var table2 = new TableStructure("bar"); - table1 = table1.loadFromJson(data); - table2 = table2.loadFromJson(dat2); - table1.replaceRows(table2, { 1: 0 }); - expect(table1.columns[0].values.slice()).toEqual([1970, 1980]); - expect(table1.columns[1].values.slice()).toEqual(["A", "C"]); - }); - - it("can merge tables with dates", function () { - var data = [ - ["year", "id", "lat", "lon"], - [1970, "A", 16.8, 5.2], - [1971, "B", 16.2, 5.2] - ]; - var dat2 = [ - ["year", "id", "lat", "lon"], - [1975, "C", 15, 5.5], - [1970, "A", 12, 8], - [1971, "A", 13, 9] - ]; - var options = { idColumnNames: ["id"] }; - var table1 = new TableStructure("foo", options); - var table2 = new TableStructure("bar"); // Only uses idColumnNames on table1. - table1 = table1.loadFromJson(data); - table2 = table2.loadFromJson(dat2); - table1.setActiveTimeColumn(0); - table1.columns[1].isActive = true; - table1.columns[1].color = "blue"; - table1.merge(table2); - expect(table1.columns[0].values.slice()).toEqual([1970, 1971, 1975, 1971]); - expect(table1.activeTimeColumn.dates.length).toEqual(4); // ie. activeTimeColumn updates too. - expect(table1.columns[1].values.slice()).toEqual(["A", "B", "C", "A"]); - expect(table1.columns[2].values.slice()).toEqual([12, 16.2, 15, 13]); - expect(table1.columns[1].isActive).toBe(true); // ie. Don't lose options on the columns. - expect(table1.columns[1].color).toEqual("blue"); - }); - - it("can merge tables without dates", function () { - var data = [ - ["id", "lat", "lon"], - ["A", 16.8, 5.2], - ["B", 16.2, 5.2] - ]; - var dat2 = [ - ["id", "lat", "lon"], - ["A", 12, 8], - ["C", 15, 5.5] - ]; - var options = { idColumnNames: ["id"] }; - var table1 = new TableStructure("foo", options); - var table2 = new TableStructure("bar"); // Only uses idColumnNames on table1. - table1 = table1.loadFromJson(data); - table2 = table2.loadFromJson(dat2); - table1.merge(table2); - expect(table1.columns[0].values.slice()).toEqual(["A", "B", "C"]); - expect(table1.columns[1].values.slice()).toEqual([12, 16.2, 15]); - }); - - it("can add columns", function () { - var dataNoAddr = [ - ["x", "y"], - [1.678, 5.123], - [54321, 12345], - [4, -3] - ]; - var options = { - columnOptions: { - y: { - name: "new y", - format: { useGrouping: true, maximumFractionDigits: 1 } - } - } - }; - var tableStructure = new TableStructure("foo", options); - tableStructure = tableStructure.loadFromJson(dataNoAddr); - var longValues = [44.0, 55.0, 66.0]; - var latValues = [11.0, 22.0, 33.0]; - expect(tableStructure.hasLatitudeAndLongitude).toBe(false); - tableStructure.addColumn("lat", latValues); - tableStructure.addColumn("lon", longValues); - expect(tableStructure.hasLatitudeAndLongitude).toBe(true); - expect(tableStructure.columns[VarType.LAT].values).toBe(latValues); - expect(tableStructure.columns[VarType.LON].values).toBe(longValues); - }); - - it("can sort columns", function () { - var data = [ - ["x", "y", "z"], - [3, 5, "a"], - [1, 8, "c"], - [4, -3, "b"] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.sortBy(tableStructure.getColumnWithName("x")); - expect(tableStructure.getColumnWithName("x").values.slice()).toEqual([ - 1, 3, 4 - ]); - expect(tableStructure.getColumnWithName("y").values.slice()).toEqual([ - 8, 5, -3 - ]); - expect(tableStructure.getColumnWithName("z").values.slice()).toEqual([ - "c", - "a", - "b" - ]); - tableStructure.sortBy(tableStructure.getColumnWithName("z")); - expect(tableStructure.getColumnWithName("x").values.slice()).toEqual([ - 3, 4, 1 - ]); - expect(tableStructure.getColumnWithName("y").values.slice()).toEqual([ - 5, -3, 8 - ]); - expect(tableStructure.getColumnWithName("z").values.slice()).toEqual([ - "a", - "b", - "c" - ]); - tableStructure.sortBy( - tableStructure.getColumnWithName("x"), - function (a, b) { - return b - a; - } - ); // descending - expect(tableStructure.getColumnWithName("x").values.slice()).toEqual([ - 4, 3, 1 - ]); - expect(tableStructure.getColumnWithName("y").values.slice()).toEqual([ - -3, 5, 8 - ]); - expect(tableStructure.getColumnWithName("z").values.slice()).toEqual([ - "b", - "a", - "c" - ]); - }); - - it("can sort columns by date, even with null dates", function () { - // Note the last date occurs before the first, but a string compare would disagree. - var data = [ - ["date", "v"], - ["2010-06-20T10:00:00.0+1000", "a"], - ["2010-06-19T10:00:00.0+1000", "b"], - [null, "n"], - ["2010-06-20T10:00:00.0+1100", "c"] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.sortBy(tableStructure.columns[0]); - expect(tableStructure.columns[1].values.slice()).toEqual([ - "b", - "c", - "a", - "n" - ]); - }); - - it("can calculate finish dates", function () { - var data = [["date"], ["2016-01-03T12:15:00Z"], ["2016-01-03T12:15:30Z"]]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.setActiveTimeColumn(); - expect(tableStructure.finishJulianDates).toEqual([ - JulianDate.fromIso8601("2016-01-03T12:15:30Z"), - JulianDate.fromIso8601("2016-01-03T12:16:00Z") // Final one should have the average spacing, 30 sec. - ]); - }); - - it("can calculate sub-second finish dates", function () { - var data = [ - ["date"], - ["2016-01-03T12:15:00Z"], - ["2016-01-03T12:15:00.4Z"], - ["2016-01-03T12:15:01Z"] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.setActiveTimeColumn(); - expect(tableStructure.finishJulianDates).toEqual([ - JulianDate.fromIso8601("2016-01-03T12:15:00.40Z"), - JulianDate.fromIso8601("2016-01-03T12:15:01Z"), - JulianDate.fromIso8601("2016-01-03T12:15:01.5Z") // Average spacing is 0.5 second. - ]); - }); - - it("supports displayDuration", function () { - var data = [["date"], ["2016-01-03"], ["2016-01-04"], ["2016-01-05"]]; - var sevenDaysInMinutes = 60 * 24 * 7; - var tableStructure = new TableStructure("test", { - displayDuration: sevenDaysInMinutes - }); - TableStructure.fromJson(data, tableStructure); - tableStructure.setActiveTimeColumn(); - var interval = tableStructure.timeIntervals[0]; - expect( - TimeInterval.contains(interval, JulianDate.fromIso8601("2016-01-09")) - ).toBe(true); - expect( - TimeInterval.contains(interval, JulianDate.fromIso8601("2016-01-11")) - ).toBe(false); - var durationInSeconds = JulianDate.secondsDifference( - interval.stop, - interval.start - ); - expect(durationInSeconds).toEqual(sevenDaysInMinutes * 60); - }); - - it("uses start_date and end_date", function () { - // Note these end dates overlap (12:15:00-12:16:10, 12:15:30-12:16:40). - var data = [ - ["start_date", "end_date"], - ["2016-01-03T12:15:00Z", "2016-01-03T12:16:10Z"], - ["2016-01-03T12:15:30Z", "2016-01-03T12:16:40Z"] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.setActiveTimeColumn(); - expect(tableStructure.finishJulianDates).toEqual([ - JulianDate.fromIso8601("2016-01-03T12:16:10Z"), - JulianDate.fromIso8601("2016-01-03T12:16:40Z") - ]); - }); - - it("calculates id-specific date periods", function () { - // A and B both have two two-day observations, but they are interspersed. - // Without an id column, they would have one-day observations. - var data = [ - ["date", "id"], - ["2016-01-01T00:00:00Z", "A"], - ["2016-01-02T00:00:00Z", "B"], - ["2016-01-03T00:00:00Z", "A"], - ["2016-01-04T00:00:00Z", "B"] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.idColumnNames = ["id"]; - tableStructure.shaveSeconds = 0; - tableStructure.setActiveTimeColumn(); - expect(tableStructure.finishJulianDates).toEqual([ - JulianDate.fromIso8601("2016-01-03T00:00:00Z"), - JulianDate.fromIso8601("2016-01-04T00:00:00Z"), - JulianDate.fromIso8601("2016-01-05T00:00:00Z"), - JulianDate.fromIso8601("2016-01-06T00:00:00Z") - ]); - }); - - it("can add feature rows at start and end dates", function () { - var data = [ - ["date", "id", "value"], - ["2016-01-01T00:00:00Z", "A", 10], - ["2016-01-02T00:00:00Z", "B", 15], - ["2016-01-03T00:00:00Z", "A", 12], - ["2016-01-04T00:00:00Z", "B", 17] - ]; - var tableStructure = TableStructure.fromJson(data); - tableStructure.idColumnNames = ["id"]; - tableStructure.columns = - tableStructure.getColumnsWithFeatureRowsAtStartAndEndDates( - "date", - "value" - ); - tableStructure.setActiveTimeColumn(); - expect(tableStructure.columns[1].values.slice()).toEqual([ - "A", - "B", - "B", - "A", - "B", - "A" - ]); - expect(tableStructure.columns[2].values.slice()).toEqual([ - 10, - null, - 15, - 12, - 17, - null - ]); - expect(tableStructure.activeTimeColumn.julianDates).toEqual([ - JulianDate.fromIso8601("2016-01-01T00:00:00Z"), // A, 10 - JulianDate.fromIso8601("2016-01-01T00:00:00Z"), // The new B, null - JulianDate.fromIso8601("2016-01-02T00:00:00Z"), // B, 15 - JulianDate.fromIso8601("2016-01-03T00:00:00Z"), // A, 12 - JulianDate.fromIso8601("2016-01-04T00:00:00Z"), // B, 17 - JulianDate.fromIso8601("2016-01-04T00:00:00Z") // The new A, null - ]); - }); - - describe("Time slider initial time as specified by initialTimeSource ", function () { - // Future developers take note: some of these tests will stop working sometime after August 3015. - it('should be start if "start" set', function () { - var tableStructure = new TableStructure("test", { - initialTimeSource: "start" - }); - // Note: Specifying the time in this way means that the end date will be after 2015-08-09, but since we don't care particularly about the end date this is enough precision for this test. - var data = [ - ["date"], - ["2013-08-07T00:00:00.00Z"], - ["2015-08-09T00:00:00.00Z"] - ]; - TableStructure.fromJson(data, tableStructure); - tableStructure.setActiveTimeColumn(); - - var currentTime = JulianDate.toIso8601( - tableStructure.clock.currentTime, - 3 - ); - // Do not compare time, because on some systems the second could have ticked over between getting the two times. - currentTime = currentTime.substr(0, 10); - expect(currentTime).toBe("2013-08-07"); - }); - - it('should be current time if "present" set', function () { - var tableStructure = new TableStructure("test", { - initialTimeSource: "present" - }); - // Note: Specifying the time in this way means that the end date will be after 2015-08-09, but since we don't care particularly about the end date this is enough precision for this test. - var data = [ - ["date"], - ["2013-08-07T00:00:00.00Z"], - ["3115-08-09T00:00:00.00Z"] - ]; - TableStructure.fromJson(data, tableStructure); - tableStructure.setActiveTimeColumn(); - - var dateNow = new Date().toISOString(); - var currentTime = JulianDate.toIso8601( - tableStructure.clock.currentTime, - 3 - ); - // Do not compare time, because on some systems the second could have ticked over between getting the two times. - dateNow = dateNow.substr(0, 10); - currentTime = currentTime.substr(0, 10); - expect(currentTime).toBe(dateNow); - }); - - it('should be last time if "end" set', function () { - var tableStructure = new TableStructure("test", { - initialTimeSource: "end", - finalEndJulianDate: JulianDate.fromIso8601("2015-08-09T00:00:00.00Z") - }); - var data = [["date"], ["2013-08-07T00:00:00.00Z"]]; - TableStructure.fromJson(data, tableStructure); - tableStructure.setActiveTimeColumn(); - - var currentTime = JulianDate.toIso8601( - tableStructure.clock.currentTime, - 3 - ); - // Do not compare time, because on some systems the second could have ticked over between getting the two times. - currentTime = currentTime.substr(0, 10); - expect(currentTime).toBe("2015-08-09"); - }); - - it("should be set to date specified if date is specified", function () { - var tableStructure = new TableStructure("test", { - initialTimeSource: "2015-08-08T00:00:00.00Z" - }); - // Note: Specifying the time in this way means that the end date will be after 2015-08-11, but since we don't care particularly about the end date this is enough precision for this test. - var data = [ - ["date"], - ["2013-08-07T00:00:00.00Z"], - ["2015-08-11T00:00:00.00Z"] - ]; - TableStructure.fromJson(data, tableStructure); - tableStructure.setActiveTimeColumn(); - - var currentTime = JulianDate.toIso8601( - tableStructure.clock.currentTime, - 3 - ); - // Do not compare time, because on some systems the second could have ticked over between getting the two times. - currentTime = currentTime.substr(0, 10); - expect(currentTime).toBe("2015-08-08"); - }); - - it("should throw if a rubbish string is specified", function () { - var tableStructure = new TableStructure("test", { - initialTimeSource: "2015z08-08" - }); - var data = [ - ["date"], - ["2013-08-07T00:00:00.00Z"], - ["2015-08-11T00:00:00.00Z"] - ]; - TableStructure.fromJson(data, tableStructure); - - expect(function () { - tableStructure.setActiveTimeColumn(); - }).toThrow(); - }); - }); -}); diff --git a/test/ModelMixins/DiffableMixinSpec.ts b/test/ModelMixins/DiffableMixinSpec.ts index c9114dd9ef2..f5782695203 100644 --- a/test/ModelMixins/DiffableMixinSpec.ts +++ b/test/ModelMixins/DiffableMixinSpec.ts @@ -62,17 +62,17 @@ class TestDiffableItem extends DiffableMixin( } showDiffImage( - firstDate: JulianDate, - secondDate: JulianDate, - diffStyleId: string + _firstDate: JulianDate, + _secondDate: JulianDate, + _diffStyleId: string ) {} clearDiffImage() {} getLegendUrlForStyle( - diffStyleId: string, - firstDate: JulianDate, - secondDate: JulianDate + _diffStyleId: string, + _firstDate: JulianDate, + _secondDate: JulianDate ) { return "test-legend-url"; } diff --git a/test/ModelMixins/DiscretelyTimeVaryingMixinSpec.ts b/test/ModelMixins/DiscretelyTimeVaryingMixinSpec.ts index 18502dddbb0..b235c8a4b32 100644 --- a/test/ModelMixins/DiscretelyTimeVaryingMixinSpec.ts +++ b/test/ModelMixins/DiscretelyTimeVaryingMixinSpec.ts @@ -23,10 +23,10 @@ describe("DiscretelyTimeVaryingMixin", () => { await wmsItem.loadMapItems(); const years = wmsItem.objectifiedDates[20]; - expect(years.dates.length).toBe(1000); + expect(years.dates.length).toBe(10000); expect(years.index[0]).toBe(2015); const months = years[years.index[0]]; - expect(months.dates.length).toBe(1000); + expect(months.dates.length).toBe(10000); expect(months.index[0]).toBe(3); }); diff --git a/test/ModelMixins/TableMixinSpec.ts b/test/ModelMixins/TableMixinSpec.ts index 256e51d3284..4e17baf4ebb 100644 --- a/test/ModelMixins/TableMixinSpec.ts +++ b/test/ModelMixins/TableMixinSpec.ts @@ -2139,4 +2139,14 @@ describe("TableMixin", function () { expect(item.activeStyle).toBe("parkflag"); }); }); + + describe("applies default featureInfoTemplate", function () { + it("removes _id_ from template", async function () { + item.setTrait(CommonStrata.user, "csvString", LatLonValCsv); + + (await item.loadMapItems()).throwIfError(); + + expect(item.featureInfoTemplate.template?.indexOf("_id_")).toBe(-1); + }); + }); }); diff --git a/test/ModelMixins/TileErrorHandlerMixinSpec.ts b/test/ModelMixins/TileErrorHandlerMixinSpec.ts index 93ecc5525ff..58f3be67c66 100644 --- a/test/ModelMixins/TileErrorHandlerMixinSpec.ts +++ b/test/ModelMixins/TileErrorHandlerMixinSpec.ts @@ -62,7 +62,10 @@ describe("TileErrorHandlerMixin", function () { let item: TestCatalogItem; let imageryProvider: ImageryProvider; - const newError = (statusCode: number | undefined, timesRetried = 0) => { + const newError = ( + statusCode: number | undefined, + timesRetried = 0 + ): TileProviderError => { const httpError = new RequestErrorEvent(statusCode) as any as Error; return new TileProviderError( imageryProvider, @@ -210,9 +213,12 @@ describe("TileErrorHandlerMixin", function () { it("fails with bad image error if the error defines a target element", async function () { try { - const tileProviderError = newError(undefined); - // @ts-expect-error - tileProviderError.error = { ...tileProviderError.error, target: {} }; + const tileProviderError: TileProviderError = newError(undefined); + + tileProviderError.error = { + ...tileProviderError.error, + target: {} + } as Error; await onTileLoadError(item, tileProviderError); } catch { /* eslint-disable-line no-empty */ diff --git a/test/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionSpec.ts b/test/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionSpec.ts index d36b51abe64..6a346d7f737 100644 --- a/test/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionSpec.ts +++ b/test/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionSpec.ts @@ -79,7 +79,7 @@ describe("YDYRCatalogFunction", function () { csv.setTrait(CommonStrata.user, "csvString", lga11Csv); await csv.loadRegionProviderList(); await csv.loadMapItems(); - addUserCatalogMember(terria, csv, { enable: true }); + await addUserCatalogMember(terria, csv, { enable: true }); ydyr = new YDYRCatalogFunction("testYdyr", terria); ydyr.setTrait(CommonStrata.definition, "parameters", { @@ -92,7 +92,7 @@ describe("YDYRCatalogFunction", function () { }); // A few reactions will happen, while setting default values for functionParameters - await new Promise((resolve, reject) => { + await new Promise((resolve) => { reaction( () => ydyr.functionParameters, () => { diff --git a/test/Models/Catalog/CatalogGroupSpec.ts b/test/Models/Catalog/CatalogGroupSpec.ts index 08c2db64ece..4c473886baf 100644 --- a/test/Models/Catalog/CatalogGroupSpec.ts +++ b/test/Models/Catalog/CatalogGroupSpec.ts @@ -7,7 +7,6 @@ import StubCatalogItem from "../../../lib/Models/Catalog/CatalogItems/StubCatalo import CatalogMemberFactory from "../../../lib/Models/Catalog/CatalogMemberFactory"; import SplitItemReference from "../../../lib/Models/Catalog/CatalogReferences/SplitItemReference"; import WebMapServiceCatalogItem from "../../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem"; -import { getUniqueStubName } from "../../../lib/Models/Catalog/createStubCatalogItem"; import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; import upsertModelFromJson from "../../../lib/Models/Definition/upsertModelFromJson"; import Terria from "../../../lib/Models/Terria"; @@ -74,7 +73,6 @@ describe("CatalogGroup", function () { const item = terria.getModelById(CatalogGroup, "mama") as CatalogGroup; const child1 = terria.getModelById(CatalogGroup, "child1") as CatalogGroup; - const child2 = terria.getModelById(CatalogGroup, "child2") as CatalogGroup; expect(item).toBeDefined(); expect(item.type).toBe("group"); expect(item.members).toEqual(["child1", "child2", "child3"]); @@ -138,7 +136,6 @@ describe("CatalogGroup", function () { expect(invalidType).toBeDefined(); expect(noType).toBeDefined(); expect(nothingness).toBeDefined(); - const stubName = getUniqueStubName(terria); if ( member0 !== undefined && member1 !== undefined && diff --git a/test/Models/Catalog/CatalogGroups/OpenDataSoftCatalogGroupSpec.ts b/test/Models/Catalog/CatalogGroups/OpenDataSoftCatalogGroupSpec.ts index 93f2fe70f68..5dc0aa03af4 100644 --- a/test/Models/Catalog/CatalogGroups/OpenDataSoftCatalogGroupSpec.ts +++ b/test/Models/Catalog/CatalogGroups/OpenDataSoftCatalogGroupSpec.ts @@ -34,7 +34,6 @@ describe("OpenDataSoftCatalogGroup", function () { }); afterEach(function () { - console.log(fetchMock.calls()); fetchMock.restore(); }); diff --git a/test/Models/Catalog/CatalogGroups/ThreddsCatalogGroupSpec.ts b/test/Models/Catalog/CatalogGroups/ThreddsCatalogGroupSpec.ts index 3608c265b61..b7a5aba66d0 100644 --- a/test/Models/Catalog/CatalogGroups/ThreddsCatalogGroupSpec.ts +++ b/test/Models/Catalog/CatalogGroups/ThreddsCatalogGroupSpec.ts @@ -1,8 +1,6 @@ import { configure } from "mobx"; import Terria from "../../../../lib/Models/Terria"; -import ThreddsCatalogGroup, { - ThreddsStratum -} from "../../../../lib/Models/Catalog/CatalogGroups/ThreddsCatalogGroup"; +import ThreddsCatalogGroup from "../../../../lib/Models/Catalog/CatalogGroups/ThreddsCatalogGroup"; import i18next from "i18next"; import CatalogGroup from "../../../../lib/Models/Catalog/CatalogGroup"; import WebMapServiceCatalogGroup from "../../../../lib/Models/Catalog/Ows/WebMapServiceCatalogGroup"; @@ -15,7 +13,6 @@ configure({ describe("ThreddsCatalogGroup", function () { let terria: Terria; let threddsCatalogGroup: ThreddsCatalogGroup; - let threddsStratum: ThreddsStratum; beforeEach(async function () { terria = new Terria({ @@ -26,9 +23,6 @@ describe("ThreddsCatalogGroup", function () { threddsCatalogGroup.setTrait("definition", "url", url); await threddsCatalogGroup.loadMembers(); - threddsStratum = threddsCatalogGroup.strata.get( - ThreddsStratum.stratumName - ) as ThreddsStratum; }); it("has a type and typeName", function () { diff --git a/test/Models/Catalog/CatalogItems/ApiTableCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/ApiTableCatalogItemSpec.ts index f5c3e130e32..d7c5a1f682e 100644 --- a/test/Models/Catalog/CatalogItems/ApiTableCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/ApiTableCatalogItemSpec.ts @@ -31,8 +31,6 @@ describe("ApiTableCatalogItem", function () { }); it("creates a table from api calls", async function () { - const valueApiIdx = 0; - const positionApiIdx = 1; runInAction(() => { updateModelFromJson(apiCatalogItem, CommonStrata.definition, { idKey: "id", diff --git a/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts index aeefd613142..e5ac166db8e 100644 --- a/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts @@ -88,7 +88,6 @@ describe("CartoMapV3CatalogItemSpec", function () { jasmine.Ajax.stubRequest( "https://BASE_URL/v3/maps/CONNECTION_NAME/query" ).andCallFunction((req) => { - req.data; const body = req.data(); // Only respond if correct body parameters if ( diff --git a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts index 3e8bc68ed7c..5e39179b1cb 100644 --- a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts @@ -316,10 +316,13 @@ describe("Cesium3DTilesCatalogItemSpec", function () { }); }); - it("correctly builds `Feature` from picked Cesium3DTileFeature", function () { + it("correctly builds `Feature` from picked Cesium3DTileFeature", async function () { const picked = new Cesium3DTileFeature(); spyOn(picked, "getPropertyIds").and.returnValue([]); - const feature = item.buildFeatureFromPickResult(Cartesian2.ZERO, picked); + const feature = await item.buildFeatureFromPickResult( + Cartesian2.ZERO, + picked + ); expect(feature).toBeDefined(); if (feature) { expect(feature._cesium3DTileFeature).toBe(picked); @@ -337,7 +340,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { "color" ]); item.setFeatureVisibility(feature, false); - // @ts-expect-error + // @ts-expect-error JsonValue expect(item.style.show.conditions).toEqual([ ['${color} === "red" && ${doorNumber} === 10', false], ["true", true] // fallback rule diff --git a/test/Models/Catalog/CatalogItems/CogCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/CogCatalogItemSpec.ts new file mode 100644 index 00000000000..593b45d78b7 --- /dev/null +++ b/test/Models/Catalog/CatalogItems/CogCatalogItemSpec.ts @@ -0,0 +1,183 @@ +import { reaction, when } from "mobx"; +import { TIFFImageryProvider } from "terriajs-tiff-imagery-provider"; +import { ImageryParts } from "../../../../lib/ModelMixins/MappableMixin"; +import CogCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/CogCatalogItem"; +import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; +import updateModelFromJson from "../../../../lib/Models/Definition/updateModelFromJson"; +import Terria from "../../../../lib/Models/Terria"; + +const TEST_URLS = { + "4326": "/test/cogs/4326.tif", // a 1x1 tif in native EPSG:4326 projection + "32756": "/test/cogs/32756.tif" // a 1x1 tif in non-native 32756 projection +}; + +describe("CogCatalogItem", function () { + let item: CogCatalogItem; + + beforeEach(function () { + item = new CogCatalogItem("test", new Terria()); + }); + + it("should have a type 'cog'", function () { + expect(item.type).toEqual("cog"); + }); + + it("can be instantiated", function () { + expect(item).toBeDefined(); + }); + + describe("mapItems", function () { + // Test for each valid URL + it(`should return a TIFFImageryProvider`, async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + await item.loadMapItems(); + const mapItem = item.mapItems[0]; + expect(mapItem).toBeDefined(); + expect(ImageryParts.is(mapItem)).toBe(true); + if (ImageryParts.is(mapItem)) { + expect(mapItem.imageryProvider instanceof TIFFImageryProvider).toBe( + true + ); + } + }); + }); + + describe("TIFFImageryProvider initialization", function () { + it("uses the correct url", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + + const fromUrl = spyOn(TIFFImageryProvider, "fromUrl").and.callThrough(); + await item.loadMapItems(); + expect(item.mapItems[0]).toBeDefined(); + + const [url] = fromUrl.calls.first().args; + expect(url).toBe(testUrl); + }); + + it("correctly initializes the basic TIFFImageryProvider options", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + item.setTrait(CommonStrata.user, "credit", "Y"); + item.setTrait(CommonStrata.user, "tileSize", 1); + item.setTrait(CommonStrata.user, "minimumLevel", 5); + item.setTrait(CommonStrata.user, "maximumLevel", 10); + item.setTrait(CommonStrata.user, "hasAlphaChannel", false); + await item.loadMapItems(); + + const imageryProvider = getImageryProvider(item); + expect(imageryProvider).toBeDefined(); + expect(imageryProvider.credit.html).toBe("Y"); + expect(imageryProvider.tileSize).toBe(1); + expect(imageryProvider.minimumLevel).toBe(5); + expect(imageryProvider.maximumLevel).toBe(10); + expect(imageryProvider.hasAlphaChannel).toBe(false); + }); + + describe("renderOptions", function () { + it("is set correctly", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + updateModelFromJson(item.renderOptions, CommonStrata.user, { + nodata: 42, + convertToRGB: true, + resampleMethod: "bilinear" + }); + await item.loadMapItems(); + const renderOptions = getImageryProvider(item)?.renderOptions; + expect(renderOptions).toBeDefined(); + + expect(renderOptions.nodata).toBe(42); + expect(renderOptions.convertToRGB).toBe(true); + expect(renderOptions.resampleMethod).toBe("bilinear"); + }); + + it("uses a default value of `bilinear` for `resampleMethod'", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + updateModelFromJson(item.renderOptions, CommonStrata.user, {}); + await item.loadMapItems(); + const renderOptions = getImageryProvider(item)?.renderOptions; + expect(renderOptions).toBeDefined(); + expect(renderOptions.resampleMethod).toBe("nearest"); + }); + }); + }); + + describe("reprojection", function () { + it("reprojects non native projections", async function () { + const testUrl = TEST_URLS["32756"]; + item.setTrait(CommonStrata.user, "url", testUrl); + + const mockReprojector = jasmine.createSpy(); + item.reprojector = () => mockReprojector; + await item.loadMapItems(); + + expect(mockReprojector.calls.count()).toBe(1); + expect(mockReprojector.calls.first().args[0]).toBe(32756); + }); + }); + + describe("imageryProvider destruction", function () { + it("destroys the imageryProvider when `mapItems` is no longer observed", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + const destroyReaction = reaction( + () => item.mapItems, + () => {}, + { fireImmediately: true } + ); + await when(() => item.mapItems.length > 0); + const imageryProvider = getImageryProvider(item); + expect(imageryProvider).toBeDefined(); + const destroy = spyOn(imageryProvider, "destroy"); + + destroyReaction(); + expect(destroy).toHaveBeenCalledTimes(1); + }); + + it("once destroyed, it re-creates imageryProvider if `mapItems` is observed again", async function () { + const testUrl = TEST_URLS["4326"]; + item.setTrait(CommonStrata.user, "url", testUrl); + const destroyReaction1 = reaction( + () => item.mapItems, + () => {}, + { fireImmediately: true } + ); + await when(() => item.mapItems.length > 0); + const imageryProvider1 = getImageryProvider(item); + expect(imageryProvider1).toBeDefined(); + destroyReaction1(); + expect(item.mapItems.length).toBe(0); + + const destroyReaction2 = reaction( + () => item.mapItems, + () => {}, + { fireImmediately: true } + ); + await when(() => item.mapItems.length > 0); + const imageryProvider2 = getImageryProvider(item); + expect(imageryProvider2).toBeDefined(); + destroyReaction2(); + }); + }); +}); + +/** + * Utility function to get the imagery provider from a CogCatalogItem. + * + * @param item The CogCatalogItem to get the imagery provider from. + * @returns The TIFFImageryProvider if found, otherwise throws an error. + */ +function getImageryProvider(item: CogCatalogItem): TIFFImageryProvider { + const mapItem = item.mapItems[0]; + if ( + ImageryParts.is(mapItem) && + mapItem.imageryProvider instanceof TIFFImageryProvider + ) { + return mapItem.imageryProvider; + } else { + throw new Error("Load failed"); + } +} diff --git a/test/Models/Catalog/CatalogItems/GeoJsonCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/GeoJsonCatalogItemSpec.ts index c623c3892c3..bf5edb5f51b 100644 --- a/test/Models/Catalog/CatalogItems/GeoJsonCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/GeoJsonCatalogItemSpec.ts @@ -73,12 +73,12 @@ describe("GeoJsonCatalogItemSpec", () => { type: "Polygon", coordinates: [ [ - [144.80667114257812, -32.96258644191746], + [144.806671142578125, -32.96258644191746], [145.008544921875, -33.19273094190691], [145.557861328125, -32.659031913817685], - [145.04287719726562, -32.375322284319346], + [145.042877197265625, -32.375322284319346], [144.7998046875, -32.96719522935591], - [144.80667114257812, -32.96258644191746] + [144.806671142578125, -32.96258644191746] ] ] } @@ -1558,5 +1558,29 @@ describe("GeoJsonCatalogItemSpec", () => { const terriaFeatureData = features[0].data as TerriaFeatureData; expect(terriaFeatureData.rowIds).toEqual([4, 5, 6, 7, 8]); }); + + describe("applies default featureInfoTemplate", function () { + let terria: Terria; + let geojson: GeoJsonCatalogItem; + + beforeEach(async function () { + terria = new Terria({ + baseUrl: "./" + }); + geojson = new GeoJsonCatalogItem("test-geojson", terria); + }); + + it("removes _id_ from template", async function () { + geojson.setTrait( + CommonStrata.user, + "url", + "test/GeoJSON/height.geojson" + ); + + await geojson.loadMapItems(); + + expect(geojson.featureInfoTemplate.template?.indexOf("_id_")).toBe(-1); + }); + }); }); }); diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts new file mode 100644 index 00000000000..f731f5cbb03 --- /dev/null +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -0,0 +1,173 @@ +import "../../../SpecMain"; +import { reaction, runInAction } from "mobx"; +import i18next from "i18next"; +import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; +import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; +import Terria from "../../../../lib/Models/Terria"; +import I3SCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/I3SCatalogItem"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; +import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; +import Resource from "terriajs-cesium/Source/Core/Resource"; +import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; + +const mockLayerData = { + href: "layers/0/", + layerType: "3DObject", + attributeStorageInfo: [], + store: { rootNode: "mockRootNodeUrl", version: "1.6" }, + fullExtent: { xmin: 0, ymin: 1, xmax: 2, ymax: 3 }, + spatialReference: { wkid: 4326 }, + id: 0 +}; + +const mockProviderData = { + name: "mockProviderName", + serviceVersion: "1.6", + layers: [mockLayerData] +}; + +describe("I3SCatalogItemSpec", function () { + let item: I3SCatalogItem; + const testUrl = "/test/Cesium3DTiles/tileset.json"; + + beforeAll(function () { + spyOn(Resource.prototype, "fetchJson").and.callFake(function fetch() { + return Promise.resolve(mockProviderData); + }); + spyOn(Cesium3DTileset, "fromUrl").and.callFake(async () => { + const tileset = new Cesium3DTileset({}); + /* @ts-expect-error Mock the root tile so that i3s property can be appended */ + tileset._root = {}; + return tileset; + }); + }); + + beforeEach(function () { + item = new I3SCatalogItem("test", new Terria()); + runInAction(() => { + item.setTrait("definition", "url", testUrl); + item.setTrait("definition", "allowFeaturePicking", true); + }); + }); + + it("should have a type and a typeName", function () { + expect(I3SCatalogItem.type).toBe("i3s"); + expect(item.type).toBe("i3s"); + expect(item.typeName).toBe(i18next.t("core.dataType.i3s")); + }); + + it("supports zooming", function () { + expect(item.disableZoomTo).toBeFalsy(); + }); + + it("supports show info", function () { + expect(item.disableAboutData).toBeFalsy(); + }); + + it("is mappable", function () { + expect(item.isMappable).toBeTruthy(); + }); + + describe("after loading", function () { + let dispose: () => void; + beforeEach(async function () { + try { + await item.loadMapItems(); + } catch { + /* eslint-disable-line no-empty */ + } + dispose = reaction( + () => item.mapItems, + () => {} + ); + }); + + afterEach(function () { + dispose(); + }); + + describe("mapItems", function () { + it("has exactly 1 mapItem", function () { + expect(item.mapItems.length).toBe(1); + }); + + describe("the mapItem", function () { + it("should be a I3SDataProvider", function () { + expect(item.mapItems[0] instanceof I3SDataProvider).toBeTruthy(); + }); + + describe("the tileset", function () { + it("sets `show`", function () { + runInAction(() => item.setTrait("definition", "show", false)); + expect(item.mapItems[0].show).toBe(false); + }); + + it("sets the shadow mode", function () { + runInAction(() => item.setTrait("definition", "shadows", "CAST")); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.shadows).toBe(ShadowMode.CAST_ONLY); + }); + + it("sets the color blend mode", function () { + runInAction(() => { + item.setTrait("definition", "colorBlendMode", "REPLACE"); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.colorBlendMode).toBe( + Cesium3DTileColorBlendMode.REPLACE + ); + }); + }); + + it("sets the color blend amount", function () { + runInAction(() => { + item.setTrait("user", "colorBlendAmount", 0.42); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.colorBlendAmount).toBe(0.42); + }); + }); + + it("sets the shadow mode", function () { + runInAction(() => item.setTrait("definition", "shadows", "CAST")); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.shadows).toBe(ShadowMode.CAST_ONLY); + }); + + it("sets the style", function () { + runInAction(() => + item.setTrait("definition", "style", { + show: "${ZipCode} === '19341'" + }) + ); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.style).toBe(item.cesiumTileStyle); + }); + }); + }); + }); + it("correctly builds `Feature` from picked Cesium3DTileFeature", async function () { + const picked = new Cesium3DTileFeature(); + /* @ts-expect-error - mock i3sNode */ + picked._content = { + tile: { + i3sNode: { + parent: undefined, + loadFields: () => new Promise((f) => f(null)), + getFieldsForFeature: () => ({}) + } + } + }; + /* @ts-expect-error - mock featureId */ + picked._batchId = 0; + + const feature = await item.buildFeatureFromPickResult( + Cartesian2.ZERO, + picked + ); + expect(feature).toBeDefined(); + if (feature) { + expect(feature._cesium3DTileFeature).toBe(picked); + } + }); + }); +}); diff --git a/test/Models/Catalog/CatalogItems/SensorObservationServiceCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/SensorObservationServiceCatalogItemSpec.ts index 6e641df2606..3399b6c45f7 100644 --- a/test/Models/Catalog/CatalogItems/SensorObservationServiceCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/SensorObservationServiceCatalogItemSpec.ts @@ -21,7 +21,7 @@ describe("SensorObservationServiceCatalogItem", function () { beforeEach(function () { jasmine.Ajax.install(); jasmine.Ajax.addCustomParamParser({ - // @ts-expect-error + // @ts-expect-error mock xhr object test: (xhr) => /^application\/soap\+xml/.test(xhr.contentType()), parse: (paramString) => paramString }); diff --git a/test/Models/Catalog/Ckan/CkanCatalogGroupSpec.ts b/test/Models/Catalog/Ckan/CkanCatalogGroupSpec.ts index a3f22dd32fe..1ece1d358de 100644 --- a/test/Models/Catalog/Ckan/CkanCatalogGroupSpec.ts +++ b/test/Models/Catalog/Ckan/CkanCatalogGroupSpec.ts @@ -29,7 +29,6 @@ interface ExtendedLoadWithXhr { const loadWithXhr: ExtendedLoadWithXhr = _loadWithXhr as any; describe("CkanCatalogGroup", function () { - const ckanServerUrl = "http://data.gov.au"; let terria: Terria; let ckanCatalogGroup: CkanCatalogGroup; let ckanServerStratum: CkanServerStratum; diff --git a/test/Models/Catalog/Ckan/CkanItemReferenceSpec.ts b/test/Models/Catalog/Ckan/CkanItemReferenceSpec.ts index f15fca0d4cd..9b5b45c132d 100644 --- a/test/Models/Catalog/Ckan/CkanItemReferenceSpec.ts +++ b/test/Models/Catalog/Ckan/CkanItemReferenceSpec.ts @@ -1,8 +1,6 @@ import i18next from "i18next"; import { runInAction } from "mobx"; -import CkanItemReference, { - CkanDatasetStratum -} from "../../../../lib/Models/Catalog/Ckan/CkanItemReference"; +import CkanItemReference from "../../../../lib/Models/Catalog/Ckan/CkanItemReference"; import WebMapServiceCatalogItem from "../../../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem"; import Terria from "../../../../lib/Models/Terria"; import WebMapServiceCatalogGroup from "../../../../lib/Models/Catalog/Ows/WebMapServiceCatalogGroup"; @@ -15,7 +13,6 @@ const wmsNoLayerResource = require("../../../../wwwroot/test/CKAN/wms-no-layer-r describe("CkanItemReference", function () { let terria: Terria; let ckanItemReference: CkanItemReference; - let ckanDatasetStratum: CkanDatasetStratum; let ckanItemTarget: any; beforeEach(async function () { @@ -75,9 +72,6 @@ describe("CkanItemReference", function () { ); }); (await ckanItemReference.loadReference()).throwIfError(); - ckanDatasetStratum = ckanItemReference.strata.get( - CkanDatasetStratum.stratumName - ) as CkanDatasetStratum; ckanItemTarget = ckanItemReference.target; }); @@ -161,9 +155,6 @@ describe("CkanItemReference", function () { "tax-stats-wms-resource" ); await ckanItemReference.loadReference(); - ckanDatasetStratum = ckanItemReference.strata.get( - CkanDatasetStratum.stratumName - ) as CkanDatasetStratum; ckanItemTarget = ckanItemReference.target; expect(ckanItemReference._ckanResource).toBeDefined(); @@ -191,9 +182,6 @@ describe("CkanItemReference", function () { "wms-no-layers-resource" ); await ckanItemReference.loadReference(); - ckanDatasetStratum = ckanItemReference.strata.get( - CkanDatasetStratum.stratumName - ) as CkanDatasetStratum; ckanItemTarget = ckanItemReference.target; expect(ckanItemReference._ckanResource).toBeDefined(); @@ -229,9 +217,6 @@ describe("CkanItemReference", function () { ); }); await ckanItemReference.loadReference(); - ckanDatasetStratum = ckanItemReference.strata.get( - CkanDatasetStratum.stratumName - ) as CkanDatasetStratum; ckanItemTarget = ckanItemReference.target; }); it("uses LAYERS from url query string for WMS item", function () { diff --git a/test/Models/Catalog/Ows/WebMapServiceCatalogItemSpec.ts b/test/Models/Catalog/Ows/WebMapServiceCatalogItemSpec.ts index ad896a1f5b9..e748448a353 100644 --- a/test/Models/Catalog/Ows/WebMapServiceCatalogItemSpec.ts +++ b/test/Models/Catalog/Ows/WebMapServiceCatalogItemSpec.ts @@ -460,8 +460,9 @@ describe("WebMapServiceCatalogItem", function () { }); try { await wms.loadMetadata(); - //@ts-expect-error - expect(mapItems[0].imageryProvider.layers).toBe("landsat_barest_earth"); + expect( + (mapItems[0].imageryProvider as WebMapServiceImageryProvider).layers + ).toBe("landsat_barest_earth"); } finally { cleanup(); } diff --git a/test/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionSpec.ts b/test/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionSpec.ts index 55c3d9208a4..8cabc518986 100644 --- a/test/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionSpec.ts +++ b/test/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionSpec.ts @@ -148,7 +148,7 @@ describe("WebProcessingServiceCatalogFunction", function () { let dispose: any; job = (await wps.submitJob()) as WebProcessingServiceCatalogFunctionJob; - await new Promise((resolve, reject) => { + await new Promise((resolve) => { dispose = reaction( () => job.downloadedResults, () => { @@ -220,7 +220,7 @@ describe("WebProcessingServiceCatalogFunction", function () { let dispose2: any; // Wait for job to finish polling, then check if finished - await new Promise((resolve, reject) => { + await new Promise((resolve) => { dispose2 = reaction( () => job.refreshEnabled, () => { @@ -307,7 +307,7 @@ describe("WebProcessingServiceCatalogFunction", function () { let dispose2: any; // Wait for job to finish polling, then check if failed - await new Promise((resolve, reject) => { + await new Promise((resolve) => { dispose2 = reaction( () => job.refreshEnabled, () => { diff --git a/test/Models/Catalog/allCatalogModelsSpec.ts b/test/Models/Catalog/allCatalogModelsSpec.ts index c676620df9d..addf1abf5a9 100644 --- a/test/Models/Catalog/allCatalogModelsSpec.ts +++ b/test/Models/Catalog/allCatalogModelsSpec.ts @@ -48,7 +48,7 @@ describe("All Catalog models", () => { // The only correct way to provide a legend is through legendOwnerTraits. models .filter( - ([modelName, model]) => !hasTraits(model, LegendOwnerTraits, "legends") + ([_modelName, model]) => !hasTraits(model, LegendOwnerTraits, "legends") ) .forEach(([modelName, model]) => { expect((model as any).legends).toBeUndefined( diff --git a/test/Models/Catalog/esri/ArcGisImageServerCatalogItemSpec.ts b/test/Models/Catalog/esri/ArcGisImageServerCatalogItemSpec.ts new file mode 100644 index 00000000000..55b7ba2870c --- /dev/null +++ b/test/Models/Catalog/esri/ArcGisImageServerCatalogItemSpec.ts @@ -0,0 +1,780 @@ +import { reaction, runInAction } from "mobx"; +import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme"; +import loadWithXhr from "../../../../lib/Core/loadWithXhr"; +import ArcGisImageServerImageryProvider from "../../../../lib/Map/ImageryProvider/ArcGisImageServerImageryProvider"; +import ArcGisImageServerCatalogItem from "../../../../lib/Models/Catalog/Esri/ArcGisImageServerCatalogItem"; +import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; +import createStratumInstance from "../../../../lib/Models/Definition/createStratumInstance"; +import { SelectableDimensionEnum } from "../../../../lib/Models/SelectableDimensions/SelectableDimensions"; +import Terria from "../../../../lib/Models/Terria"; +import { ArcGisImageServerRenderingRule } from "../../../../lib/Traits/TraitsClasses/ArcGisImageServerCatalogItemTraits"; + +const rasterFnImageServer = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/rasterFns/imageserver.json") +); + +const rasterFnLegend = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/rasterFns/legend.json") +); + +const timeImageServer = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/time/imageserver.json") +); + +const timeLegend = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/time/legend.json") +); + +const timeIdentify = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/time/identify.json") +); + +const tileImageServer = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/tile/imageserver.json") +); + +const tileLegend = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/tile/legend.json") +); + +const tileIdentify = JSON.stringify( + require("../../../../wwwroot/test/ArcGisImageServer/tile/identify.json") +); + +let spyOnLoad: any; + +describe("ArcGisImageServer", function () { + let terria: Terria; + let imageServerItem: ArcGisImageServerCatalogItem; + + beforeEach(async function () { + spyOnLoad = spyOn(loadWithXhr as any, "load").and.callThrough(); + jasmine.Ajax.install(); + jasmine.Ajax.stubRequest(/.*/).andCallFunction((r) => { + console.error(r); + throw new Error("Unhandled request: " + r.url); + }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/rasterfns\/ImageServer\?.+/ + ).andReturn({ responseText: rasterFnImageServer }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/rasterfns\/ImageServer\/legend\?.+/ + ).andReturn({ responseText: rasterFnLegend }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/time\/ImageServer\?.+/ + ).andReturn({ responseText: timeImageServer }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/time\/ImageServer\/legend\?.+/ + ).andReturn({ responseText: timeLegend }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/tile\/ImageServer\?.+/ + ).andReturn({ responseText: tileImageServer }); + + jasmine.Ajax.stubRequest( + /http:\/\/example\.com\/agsimage\/rest\/services\/tile\/ImageServer\/legend\?.+/ + ).andReturn({ responseText: tileLegend }); + + jasmine.Ajax.stubRequest("http://example.com/token").andReturn({ + responseText: JSON.stringify({ + token: "fakeToken" + }) + }); + + jasmine.Ajax.stubRequest( + /http:\/\/example.com\/agsimage\/rest\/services\/.+\/ImageServer\/exportImage\?.+/ + ).andReturn({ + responseText: "fakeImage" + }); + + jasmine.Ajax.stubRequest( + /http:\/\/example.com\/agsimage\/rest\/services\/.+\/ImageServer\/tile.+/ + ).andReturn({ + responseText: "fakeImage" + }); + + terria = new Terria(); + imageServerItem = new ArcGisImageServerCatalogItem( + "test", + terria, + undefined + ); + imageServerItem.setTrait( + "definition", + "url", + "http://example.com/agsimage/rest/services/time/ImageServer" + ); + }); + + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + + it("has a type", function () { + expect(imageServerItem.type).toBe("esri-imageServer"); + }); + + it("supports splitting", function () { + expect(imageServerItem.disableSplitter).toBeFalsy(); + }); + + it("supports zooming to extent", function () { + expect(imageServerItem.disableZoomTo).toBeFalsy(); + }); + + it("supports preview", function () { + expect(imageServerItem.disableAboutData).toBeFalsy(); + }); + + describe("when tokenUrl is set", function () { + beforeEach(() => { + imageServerItem.setTrait( + CommonStrata.definition, + "tokenUrl", + "http://example.com/token" + ); + }); + + it("fetches the token", async function () { + await imageServerItem.loadMapItems(); + expect(spyOnLoad.calls.argsFor(0)[0]).toBe("http://example.com/token"); + expect(imageServerItem.token).toBe("fakeToken"); + }); + + it("adds the token to subsequent requests", async function () { + await imageServerItem.loadMapItems(); + const tokenre = /token=fakeToken/; + expect(tokenre.test(spyOnLoad.calls.argsFor(1)[0])).toBeTruthy(); + expect(tokenre.test(spyOnLoad.calls.argsFor(2)[0])).toBeTruthy(); + }); + + it("passes the token to the imageryProvider", async function () { + await imageServerItem.loadMapItems(); + const imageryProvider = imageServerItem.mapItems[0] + .imageryProvider as ArcGisImageServerImageryProvider; + + expect(imageryProvider.baseResource.queryParameters.token).toBe( + "fakeToken" + ); + }); + }); + + describe("basic image server", function () { + it("correctly sets `alpha`", function () { + runInAction(() => + imageServerItem.setTrait(CommonStrata.definition, "opacity", 0.42) + ); + expect(imageServerItem.mapItems[0].alpha).toBe(0.42); + }); + + it("correctly sets `show`", function () { + runInAction(() => + imageServerItem.setTrait(CommonStrata.definition, "show", false) + ); + expect(imageServerItem.mapItems[0].show).toBe(false); + }); + }); + + describe("bandIds", function () { + it("adds to parameters", async function () { + runInAction(() => + imageServerItem.setTrait(CommonStrata.definition, "bandIds", [2, 3]) + ); + expect(imageServerItem.flattenedParameters).toEqual({ + bandIds: "2,3" + }); + + await imageServerItem.loadMapItems(); + + // Check legend URL + expect(spyOnLoad.calls.argsFor(1)[0]).toEqual( + "http://example.com/agsimage/rest/services/time/ImageServer/legend?bandIds=2%2C3&f=json" + ); + + // Check imagery provider + const imageryProvider = imageServerItem.mapItems[0] + .imageryProvider as ArcGisImageServerImageryProvider; + + expect(imageryProvider.baseResource.queryParameters.bandIds).toEqual( + "2,3" + ); + }); + }); + + describe("image server with time", function () { + beforeEach(async function () { + imageServerItem.setTrait( + "definition", + "url", + "http://example.com/agsimage/rest/services/time/ImageServer" + ); + + await imageServerItem.loadMetadata(); + }); + + it("sets basic traits", async function () { + console.log(imageServerItem); + expect(imageServerItem.name).toBe("Some name"); + expect(imageServerItem.description).toBe("Some description"); + expect(imageServerItem.rectangle.east).toBe(179.6875); + expect(imageServerItem.rectangle.north).toBe(90.25); + expect(imageServerItem.rectangle.west).toBe(-180.3125); + expect(imageServerItem.rectangle.south).toBe(-90.25); + expect(imageServerItem.attribution).toBe("Some copyright"); + expect(imageServerItem.maximumScale).toBe(0); + expect(imageServerItem.maximumLevel).toBeUndefined(); + expect(imageServerItem.minimumLevel).toBeUndefined(); + expect(imageServerItem.allowRasterFunction).toBe(true); + expect(imageServerItem.availableRasterFunctions.length).toBe(0); + expect(imageServerItem.disableRasterFunctionSelectors).toBe(false); + expect(imageServerItem.usePreCachedTiles).toBe(false); + expect(imageServerItem.tileHeight).toBe(256); + expect(imageServerItem.tileWidth).toBe(256); + expect(imageServerItem.wkid).toBe(102100); + }); + + it("creates legend", async function () { + expect(imageServerItem.legends.length).toBe(1); + expect(imageServerItem.legends[0].items.length).toBe(3); + expect(imageServerItem.legends[0].items[0].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAeElEQVQ4ja3RsQ3CQBQE0TlYhAiQ8B04owD334q7gAACJ0jno4n5BTxp/paxXAfS7eunhHqwPADCfJTBm+oRHnbyZCc31SPMJxls+sr2KM1OrnLydvaSLzvk3asGPgvkxeSBQL7cNRAgG14yQH42OMQfApTeuwr+AYLYEoMMYRRCAAAAAElFTkSuQmCC" + ); + expect(imageServerItem.legends[0].items[1].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAh0lEQVQ4jbXTsQ3CQBSD4d/KEx26VCg1M7EBW2UDVkpBdZBQIEikSBR3gSWMB/ga27qU/ospJ50VA0+XB0DcaLzgnb0XHDl4wczRCopSbC1XSZEeLg7oINLoBicjCET7MoNpMYPt6nzKRiy5GkER89XoAfHJZnD33oxcQ6TZ6PGHHapWZ8vwA8yIImKpipDXAAAAAElFTkSuQmCC" + ); + expect(imageServerItem.legends[0].items[2].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAIUlEQVQ4jWP8+/cvAzUBE1VNGzVw1MBRA0cNHDVwCBkIAEMfAx/78kANAAAAAElFTkSuQmCC" + ); + + expect(imageServerItem.legends[0].items[0].title).toBe( + "High : Some Value" + ); + expect(imageServerItem.legends[0].items[1].title).toBe("Some time layer"); + expect(imageServerItem.legends[0].items[2].title).toBe( + "Low : Some Value" + ); + }); + + it("creates time intervals", async function () { + expect(imageServerItem.startTime).toBe("1981-12-31T00:00:00.000000000Z"); + expect(imageServerItem.stopTime).toBe("2021-12-30T16:48:00.000000000Z"); + expect(imageServerItem.currentDiscreteTimeTag).toBe( + "2021-12-30T16:48:00Z" + ); + expect(imageServerItem.discreteTimes?.length).toBe(482); + }); + + it("Sets `time` parameter", async function () { + let imageryProvider = imageServerItem.mapItems[0] + .imageryProvider as ArcGisImageServerImageryProvider; + + if (!imageServerItem.currentDiscreteJulianDate) + throw new Error("No currentDiscreteJulianDate"); + + expect(imageryProvider.baseResource.queryParameters).toEqual({ + time: JulianDate.toDate( + imageServerItem.currentDiscreteJulianDate + ).getTime() + }); + + const time = imageServerItem.discreteTimes?.[100]?.time ?? ""; + + runInAction(() => { + imageServerItem.setTrait(CommonStrata.definition, "currentTime", time); + }); + + imageryProvider = imageServerItem.mapItems[0] + .imageryProvider as ArcGisImageServerImageryProvider; + + expect(imageryProvider.baseResource.queryParameters).toEqual({ + time: new Date(time).getTime() + }); + }); + + it("creates next imagery provider", async function () { + const time = imageServerItem.discreteTimes?.[100]?.time ?? ""; + const nextTime = imageServerItem.discreteTimes?.[101]?.time ?? ""; + + runInAction(() => { + imageServerItem.setTrait(CommonStrata.definition, "currentTime", time); + imageServerItem.setTrait(CommonStrata.definition, "isPaused", false); + terria.timelineStack.addToTop(imageServerItem); + }); + + const imageryProvider = imageServerItem.mapItems[0] + ?.imageryProvider as ArcGisImageServerImageryProvider; + const nextImageryProvider = imageServerItem._nextImageryParts + ?.imageryProvider as ArcGisImageServerImageryProvider; + + expect(imageryProvider.baseResource.queryParameters).toEqual({ + time: new Date(time).getTime() + }); + + expect(nextImageryProvider.baseResource.queryParameters).toEqual({ + time: new Date(nextTime).getTime() + }); + }); + }); + + describe("image server with raster fns", function () { + beforeEach(async function () { + imageServerItem.setTrait( + "definition", + "url", + "http://example.com/agsimage/rest/services/rasterfns/ImageServer" + ); + + await imageServerItem.loadMetadata(); + }); + + it("sets basic traits", async function () { + expect(imageServerItem.name).toBe("Some name"); + expect(imageServerItem.description).toBe("Some description"); + expect(imageServerItem.rectangle.east).toBe(-116.43027039325061); + expect(imageServerItem.rectangle.north).toBe(46.31633967431312); + expect(imageServerItem.rectangle.west).toBe(-124.63200690054119); + expect(imageServerItem.rectangle.south).toBe(41.93340221567374); + expect(imageServerItem.attribution).toBe("Some Copyright"); + expect(imageServerItem.maximumScale).toBe(0); + expect(imageServerItem.maximumLevel).toBeUndefined(); + expect(imageServerItem.minimumLevel).toBeUndefined(); + expect(imageServerItem.allowRasterFunction).toBe(true); + expect(imageServerItem.availableRasterFunctions.length).toBe(3); + expect(imageServerItem.disableRasterFunctionSelectors).toBe(false); + expect(imageServerItem.usePreCachedTiles).toBe(false); + expect(imageServerItem.tileHeight).toBe(256); + expect(imageServerItem.tileWidth).toBe(256); + expect(imageServerItem.wkid).toBe(102100); + }); + + it("creates legend", async function () { + await imageServerItem.loadMapItems(); + expect(imageServerItem.legends.length).toBe(1); + expect(imageServerItem.legends[0].items.length).toBe(3); + expect(imageServerItem.legends[0].items[0].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAT0lEQVQ4je3RwQkAMAgDwAjiz/1XVUTtEE1/zQCHibK7C1JmRjQiWB7MDJqZXJB5obtzQQDQquKCzA0BPKhMB+mV6U/5G96Df8PrSHdTwQOUlT8HeNXIpAAAAABJRU5ErkJggg==" + ); + expect(imageServerItem.legends[0].items[1].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAUElEQVQ4je3UwQkAMQhE0S+kbcGyjSK7RWQOOWQKeMgMaO7+IUpE2MpMlQfA2ntrQfmF3a0FX4fn4P0dVpUWvH/l1+FxDJB97JkxmxmVB8APVtY8xR14rRcAAAAASUVORK5CYII=" + ); + expect(imageServerItem.legends[0].items[2].imageUrl).toBe( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAIUlEQVQ4jWP8+/cvAzUBE1VNGzVw1MBRA0cNHDVwCBkIAEMfAx/78kANAAAAAElFTkSuQmCC" + ); + + expect(imageServerItem.legends[0].items[0].title).toBe( + "High : Some value" + ); + expect(imageServerItem.legends[0].items[1].title).toBe("Some label"); + expect(imageServerItem.legends[0].items[2].title).toBe( + "Low : Some value" + ); + }); + + it("adds to rasterFn to legend URL - and reloads correctly", async function () { + await imageServerItem.loadMapItems(); + + expect(spyOnLoad.calls.argsFor(1)[0]).toEqual( + "http://example.com/agsimage/rest/services/rasterfns/ImageServer/legend?f=json" + ); + + // By observing mapItems, we can trigger a reload when the renderingRule changes + const disposer = reaction( + () => imageServerItem.mapItems, + () => { + console.log("woo"); + } + ); + + runInAction(() => { + imageServerItem.setTrait( + CommonStrata.user, + "renderingRule", + createStratumInstance(ArcGisImageServerRenderingRule, { + rasterFunction: "RFTAspectColor" + }) + ); + }); + + expect(spyOnLoad.calls.argsFor(4)[0]).toEqual( + "http://example.com/agsimage/rest/services/rasterfns/ImageServer/legend?renderingRule={%22rasterFunction%22%3A%22RFTAspectColor%22}&f=json" + ); + + disposer(); + }); + + it("has raster functions", async function () { + expect(imageServerItem.availableRasterFunctions.length).toBe(3); + expect(imageServerItem.availableRasterFunctions[0].name).toBe( + "RFTAspectColor" + ); + expect(imageServerItem.availableRasterFunctions[1].name).toBe( + "RFTHillshade" + ); + expect(imageServerItem.availableRasterFunctions[2].name).toBe( + "RFTShadedReliefElevationColorRamp" + ); + expect(imageServerItem.availableRasterFunctions[0].description).toBe( + "This function generates a color representation of aspect." + ); + expect(imageServerItem.availableRasterFunctions[1].description).toBe( + "This function creates a hillshade effect based on the elevation data source." + ); + expect(imageServerItem.availableRasterFunctions[2].description).toBe( + "This function processes the elevation surface as shaded relief. " + ); + }); + + it("creates raster fn selectable dimensions", function () { + expect(imageServerItem.selectableDimensions.length).toBe(1); + let selDim = imageServerItem + .selectableDimensions[0] as SelectableDimensionEnum; + expect(selDim.name).toBe( + "models.arcGisImageServerCatalogItem.rasterFunction" + ); + expect(selDim.allowUndefined).toBeTruthy(); + expect(selDim.selectedId).toBeUndefined(); + expect(selDim.options?.length).toBe(3); + expect(selDim.options?.[0].id).toBe("RFTAspectColor"); + expect(selDim.options?.[1].id).toBe("RFTHillshade"); + expect(selDim.options?.[2].id).toBe("RFTShadedReliefElevationColorRamp"); + + runInAction(() => { + imageServerItem.setTrait( + CommonStrata.user, + "renderingRule", + createStratumInstance(ArcGisImageServerRenderingRule, { + rasterFunction: "RFTHillshade" + }) + ); + }); + + selDim = imageServerItem + .selectableDimensions[0] as SelectableDimensionEnum; + + expect(selDim.selectedId).toBe("RFTHillshade"); + }); + + it("adds rasterfn to parameters", function () { + runInAction(() => { + imageServerItem.setTrait( + CommonStrata.user, + "renderingRule", + createStratumInstance(ArcGisImageServerRenderingRule, { + rasterFunction: "RFTHillshade" + }) + ); + }); + + expect(imageServerItem.flattenedParameters).toEqual({ + renderingRule: '{"rasterFunction":"RFTHillshade"}' + }); + }); + }); + + describe("image server with tiles", function () { + beforeEach(async function () { + imageServerItem.setTrait( + "definition", + "url", + "http://example.com/agsimage/rest/services/tile/ImageServer" + ); + + await imageServerItem.loadMetadata(); + }); + + it("sets basic traits", async function () { + expect(imageServerItem.name).toBe("Some name"); + expect(imageServerItem.description).toBe("Some description"); + expect(imageServerItem.rectangle.east).toBe(-116.43027039325061); + expect(imageServerItem.rectangle.north).toBe(46.31633967431312); + expect(imageServerItem.rectangle.west).toBe(-124.63200690054119); + expect(imageServerItem.rectangle.south).toBe(41.93340221567374); + expect(imageServerItem.attribution).toBe("Some copyright"); + expect(imageServerItem.maximumScale).toBe(1128.497176); + expect(imageServerItem.maximumLevel).toBe(19); + expect(imageServerItem.minimumLevel).toBe(0); + expect(imageServerItem.allowRasterFunction).toBe(true); + expect(imageServerItem.availableRasterFunctions.length).toBe(0); + expect(imageServerItem.disableRasterFunctionSelectors).toBe(false); + expect(imageServerItem.usePreCachedTiles).toBe(true); + expect(imageServerItem.tileHeight).toBe(256); + expect(imageServerItem.tileWidth).toBe(256); + expect(imageServerItem.wkid).toBe(102100); + }); + + it("disables tile if parameters", async function () { + runInAction(() => + imageServerItem.setTrait(CommonStrata.definition, "parameters", { + foo: "bar" + }) + ); + expect(imageServerItem.usePreCachedTiles).toBe(false); + }); + + it("disables tile if renderRule", async function () { + runInAction(() => + imageServerItem.setTrait( + CommonStrata.user, + "renderingRule", + createStratumInstance(ArcGisImageServerRenderingRule, { + rasterFunction: "RFTHillshade" + }) + ) + ); + + expect(imageServerItem.usePreCachedTiles).toBe(false); + }); + + it("creates imagery provider", function () { + const imageryProvider = imageServerItem.mapItems[0] + ?.imageryProvider as ArcGisImageServerImageryProvider; + + expect(imageryProvider.usePreCachedTiles).toBe(true); + }); + }); +}); + +describe("ArcGisImageServerImageryProvider", function () { + let imageryProvider: ArcGisImageServerImageryProvider; + + beforeEach(function () { + spyOnLoad = spyOn(loadWithXhr as any, "load").and.callThrough(); + jasmine.Ajax.install(); + + jasmine.Ajax.stubRequest(/.*/).andCallFunction((r) => { + console.error(r); + throw new Error("Unhandled request: " + r.url); + }); + + jasmine.Ajax.stubRequest( + /http:\/\/example.com\/agsimage\/rest\/services\/.+\/ImageServer.+/ + ).andReturn({ + responseText: "fakeImage" + }); + + jasmine.Ajax.stubRequest( + "http://example.com/agsimage/rest/services/time/ImageServer/identify?f=json&geometryType=esriGeometryPoint&geometry={x%3A%2057.29577951308232%2C%20y%3A%2057.29577951308232%2C%20spatialReference%3A%20{wkid%3A%204326}}&returnCatalogItems=false&token=fakeToken&foo=bar" + ).andReturn({ + responseText: timeIdentify + }); + + jasmine.Ajax.stubRequest( + "http://example.com/agsimage/rest/services/tile/ImageServer/identify?f=json&geometryType=esriGeometryPoint&geometry={x%3A%206378137%2C%20y%3A%207820815.276085484%2C%20spatialReference%3A%20{wkid%3A%203857}}&returnCatalogItems=false&token=fakeToken&foo=bar" + ).andReturn({ + responseText: tileIdentify + }); + }); + + afterEach(function () { + jasmine.Ajax.uninstall(); + }); + + describe("dynamic web mercator", function () { + beforeEach(async function () { + imageryProvider = new ArcGisImageServerImageryProvider({ + url: "http://example.com/agsimage/rest/services/time/ImageServer", + token: "fakeToken", + credit: "Some credit", + parameters: { foo: "bar" }, + minimumLevel: 1, + maximumLevel: 25, + rectangle: Rectangle.fromDegrees(-20, -10, 20, 10), + enablePickFeatures: true, + usePreCachedTiles: false, + tileWidth: 256, + tileHeight: 256, + tilingScheme: new WebMercatorTilingScheme() + }); + }); + + it("should be an ArcGisImageServerImageryProvider", function () { + expect( + imageryProvider instanceof ArcGisImageServerImageryProvider + ).toBeTruthy(); + }); + + it("sets basic properties", function () { + expect(imageryProvider.credit.html).toBe("Some credit"); + expect(imageryProvider.minimumLevel).toBe(1); + expect(imageryProvider.maximumLevel).toBe(25); + expect(imageryProvider.rectangle).toEqual( + Rectangle.fromDegrees(-20, -10, 20, 10) + ); + expect(imageryProvider.enablePickFeatures).toBe(true); + expect(imageryProvider.tileWidth).toBe(256); + expect(imageryProvider.tileHeight).toBe(256); + }); + + it("sets the URL, token and parameters correctly", function () { + expect(imageryProvider.baseResource.toString()).toBe( + "http://example.com/agsimage/rest/services/time/ImageServer/?token=fakeToken&foo=bar" + ); + expect(imageryProvider.baseResource.queryParameters).toEqual({ + foo: "bar", + token: "fakeToken" + }); + }); + + it("tilingScheme should be a WebMercatorTilingScheme", function () { + expect( + imageryProvider.tilingScheme instanceof WebMercatorTilingScheme + ).toBeTruthy(); + }); + + it("creates correct image resource", function () { + expect(imageryProvider.usePreCachedTiles).toBe(false); + + const testResource = imageryProvider.buildImageResource(0, 0, 2); + expect(testResource.url).toBe( + "http://example.com/agsimage/rest/services/time/ImageServer/exportImage?bbox=-20037508.342789244%2C10018754.171394622%2C-10018754.171394622%2C20037508.342789244&size=256%2C256&format=png32&transparent=true&f=image&bboxSR=3857&imageSR=3857&token=fakeToken&foo=bar" + ); + }); + }); + + describe("dynamic wgs84", function () { + beforeEach(async function () { + imageryProvider = new ArcGisImageServerImageryProvider({ + url: "http://example.com/agsimage/rest/services/time/ImageServer", + token: "fakeToken", + credit: "Some credit", + parameters: { foo: "bar" }, + minimumLevel: 3, + maximumLevel: 5, + rectangle: Rectangle.fromDegrees(-20, -10, 20, 10), + enablePickFeatures: true, + usePreCachedTiles: false, + tileWidth: 512, + tileHeight: 512, + tilingScheme: new GeographicTilingScheme() + }); + }); + + it("should be an ArcGisImageServerImageryProvider", function () { + expect( + imageryProvider instanceof ArcGisImageServerImageryProvider + ).toBeTruthy(); + }); + + it("sets basic properties", function () { + expect(imageryProvider.credit.html).toBe("Some credit"); + expect(imageryProvider.minimumLevel).toBe(3); + expect(imageryProvider.maximumLevel).toBe(5); + expect(imageryProvider.rectangle).toEqual( + Rectangle.fromDegrees(-20, -10, 20, 10) + ); + expect(imageryProvider.enablePickFeatures).toBe(true); + expect(imageryProvider.tileWidth).toBe(512); + expect(imageryProvider.tileHeight).toBe(512); + }); + + it("sets the URL, token and parameters correctly", function () { + expect(imageryProvider.baseResource.toString()).toBe( + "http://example.com/agsimage/rest/services/time/ImageServer/?token=fakeToken&foo=bar" + ); + expect(imageryProvider.baseResource.queryParameters).toEqual({ + foo: "bar", + token: "fakeToken" + }); + }); + + it("tilingScheme should be a GeographicTilingScheme", function () { + expect( + imageryProvider.tilingScheme instanceof GeographicTilingScheme + ).toBeTruthy(); + }); + + it("creates correct image resource", function () { + expect(imageryProvider.usePreCachedTiles).toBe(false); + + const testResource = imageryProvider.buildImageResource(1, 1, 4); + expect(testResource.url).toBe( + "http://example.com/agsimage/rest/services/time/ImageServer/exportImage?bbox=-168.75%2C67.5%2C-157.5%2C78.75&size=512%2C512&format=png32&transparent=true&f=image&bboxSR=4326&imageSR=4326&token=fakeToken&foo=bar" + ); + }); + + it("picks features", async function () { + const features = await imageryProvider.pickFeatures(1, 1, 4, 1, 1); + + expect(features.length).toBe(1); + expect(features[0].name).toBe("Pixel"); + expect(features[0].description).toBe("8"); + }); + }); + + describe("tiled web mercator", function () { + beforeEach(async function () { + imageryProvider = new ArcGisImageServerImageryProvider({ + url: "http://example.com/agsimage/rest/services/tile/ImageServer", + token: "fakeToken", + credit: "Some credit", + parameters: { foo: "bar" }, + minimumLevel: 0, + maximumLevel: 24, + rectangle: Rectangle.fromDegrees(-20, -10, 20, 10), + enablePickFeatures: true, + usePreCachedTiles: true, + tileWidth: 256, + tileHeight: 256, + tilingScheme: new WebMercatorTilingScheme() + }); + }); + + it("should be an ArcGisImageServerImageryProvider", function () { + expect( + imageryProvider instanceof ArcGisImageServerImageryProvider + ).toBeTruthy(); + }); + + it("sets basic properties", function () { + expect(imageryProvider.credit.html).toBe("Some credit"); + expect(imageryProvider.minimumLevel).toBe(0); + expect(imageryProvider.maximumLevel).toBe(24); + expect(imageryProvider.rectangle).toEqual( + Rectangle.fromDegrees(-20, -10, 20, 10) + ); + expect(imageryProvider.enablePickFeatures).toBe(true); + expect(imageryProvider.tileWidth).toBe(256); + expect(imageryProvider.tileHeight).toBe(256); + }); + + it("sets the URL, token and parameters correctly", function () { + expect(imageryProvider.baseResource.toString()).toBe( + "http://example.com/agsimage/rest/services/tile/ImageServer/?token=fakeToken&foo=bar" + ); + expect(imageryProvider.baseResource.queryParameters).toEqual({ + foo: "bar", + token: "fakeToken" + }); + }); + + it("tilingScheme should be a WebMercatorTilingScheme", function () { + expect( + imageryProvider.tilingScheme instanceof WebMercatorTilingScheme + ).toBeTruthy(); + }); + + it("creates correct image resource", function () { + expect(imageryProvider.usePreCachedTiles).toBe(true); + + const testResource = imageryProvider.buildImageResource(1, 1, 4); + expect(testResource.url).toBe( + "http://example.com/agsimage/rest/services/tile/ImageServer/tile/4/1/1?token=fakeToken&foo=bar" + ); + }); + + it("picks features", async function () { + const features = await imageryProvider.pickFeatures(1, 1, 4, 1, 1); + + expect(features.length).toBe(1); + expect(features[0].name).toBe("Pixel"); + expect(features[0].description).toBe("178, 135, 99, 255"); + }); + }); +}); diff --git a/test/Models/Catalog/esri/ArcGisPortalReferenceItemSpec.ts b/test/Models/Catalog/esri/ArcGisPortalReferenceItemSpec.ts index 20358256b0a..fe5528b615b 100644 --- a/test/Models/Catalog/esri/ArcGisPortalReferenceItemSpec.ts +++ b/test/Models/Catalog/esri/ArcGisPortalReferenceItemSpec.ts @@ -4,9 +4,7 @@ import Terria from "../../../../lib/Models/Terria"; import registerCatalogMembers from "../../../../lib/Models/Catalog/registerCatalogMembers"; import i18next from "i18next"; -import ArcGisPortalItemReference, { - ArcGisPortalItemStratum -} from "../../../../lib/Models/Catalog/Esri/ArcGisPortalItemReference"; +import ArcGisPortalItemReference from "../../../../lib/Models/Catalog/Esri/ArcGisPortalItemReference"; import ArcGisFeatureServerCatalogItem from "../../../../lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem"; configure({ @@ -24,7 +22,6 @@ const loadWithXhr: ExtendedLoadWithXhr = _loadWithXhr as any; describe("ArcGisPortalItemReference", function () { let terria: Terria; let arcGisPortalItemReference: ArcGisPortalItemReference; - let arcGisPortalItemStratum: ArcGisPortalItemStratum; let portalItemTarget: any; beforeEach(async function () { @@ -75,10 +72,6 @@ describe("ArcGisPortalItemReference", function () { }); await arcGisPortalItemReference.loadReference(); - arcGisPortalItemStratum = arcGisPortalItemReference.strata.get( - ArcGisPortalItemStratum.stratumName - ) as ArcGisPortalItemStratum; - portalItemTarget = arcGisPortalItemReference.target; }); diff --git a/test/Models/CesiumSpec.ts b/test/Models/CesiumSpec.ts index 3d67d0b4df5..21b535143a1 100644 --- a/test/Models/CesiumSpec.ts +++ b/test/Models/CesiumSpec.ts @@ -254,7 +254,6 @@ describeIfSupported("Cesium Model", function () { it("should otherwise use the ION terrain specified by configParameters.cesiumTerrainAssetId", async function () { const fakeIonTerrainProvider = new CesiumTerrainProvider(); - fakeIonTerrainProvider.availability; const createSpy = spyOn( cesium as any, "createTerrainProviderFromIonAssetId" diff --git a/test/Models/ErrorServiceSpec.ts b/test/Models/ErrorServiceSpec.ts index eaa5ca9fa7c..9361386623f 100644 --- a/test/Models/ErrorServiceSpec.ts +++ b/test/Models/ErrorServiceSpec.ts @@ -1,25 +1,59 @@ -import { initializeErrorServiceProvider } from "../../lib/Models/ErrorServiceProviders/ErrorService"; -import RollbarErrorServiceProvider from "../../lib/Models/ErrorServiceProviders/RollbarErrorServiceProvider"; +import TerriaError from "../../lib/Core/TerriaError"; +import { ErrorServiceProvider } from "../../lib/Models/ErrorServiceProviders/ErrorService"; +import StubErrorServiceProvider from "../../lib/Models/ErrorServiceProviders/StubErrorServiceProvider"; +import Terria from "../../lib/Models/Terria"; -describe("initializeErrorServiceProvider", function () { - it("can initialize RollbarErrorServiceProvider", async function () { - const errorService = await initializeErrorServiceProvider({ - provider: "rollbar", - configuration: {} +describe("ErrorService", function () { + let mockErrorServiceProvider: ErrorServiceProvider; + let terria: Terria; + + beforeAll(() => { + jasmine.Ajax.stubRequest(/.*/).andError({}); + jasmine.Ajax.stubRequest(/.*(serverconfig|proxyabledomains).*/).andReturn({ + responseText: JSON.stringify({ foo: "bar" }) + }); + jasmine.Ajax.stubRequest("test-config.json").andReturn({ + responseText: JSON.stringify({ config: true }) }); - expect(errorService instanceof RollbarErrorServiceProvider).toBe(true); + mockErrorServiceProvider = { + init: () => {}, + error: () => {} + }; }); - it("throws an error when an invalid provider type is given", async function () { - let error; - try { - await initializeErrorServiceProvider({ - provider: "foo", - configuration: undefined - }); - } catch (e: any) { - error = e; - } - expect(error.message).toBe(`Unknown error service provider: foo`); + beforeEach(() => { + terria = new Terria({ + appBaseHref: "/", + baseUrl: "./" + }); + }); + + it("Initializes an error service, passing in config", async function () { + const initSpy = spyOn(mockErrorServiceProvider, "init"); + await terria.start({ + configUrl: "test-config.json", + errorService: mockErrorServiceProvider + }); + expect(initSpy).toHaveBeenCalledTimes(1); + }); + + it("Gets called with error", async function () { + const errorSpy = spyOn(mockErrorServiceProvider, "error").and.callThrough(); + await terria.start({ + configUrl: "test-config.json", + errorService: mockErrorServiceProvider + }); + const error = new TerriaError({ + message: "test error" + }); + terria.raiseErrorToUser(error); + expect(errorSpy).toHaveBeenCalledWith(error); + }); + + it("Falls back to stub provider", () => { + terria.start({ + configUrl: "test-config.json" + }); + expect(terria.errorService).toEqual(jasmine.any(StubErrorServiceProvider)); }); }); diff --git a/test/Models/Experiment.ts b/test/Models/Experiment.ts deleted file mode 100644 index 0a52cab527e..00000000000 --- a/test/Models/Experiment.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { autorun, configure, runInAction } from "mobx"; -import CatalogMemberMixin from "../../lib/ModelMixins/CatalogMemberMixin"; -import MappableMixin from "../../lib/ModelMixins/MappableMixin"; -import CommonStrata from "../../lib/Models/Definition/CommonStrata"; -import Terria from "../../lib/Models/Terria"; -import WebMapServiceCatalogGroup from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogGroup"; -import WebMapServiceCatalogItem from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem"; - -configure({ - enforceActions: "always", - computedRequiresReaction: true -}); - -describe("NewStuff", function () { - it("test", function () { - const terria = new Terria(); - const wms = new WebMapServiceCatalogGroup( - "Taxation Statistics 2011-2012", - terria - ); - - const wmsItem = new WebMapServiceCatalogItem( - "Taxation Statistics 2011-2012/ckan_95d9e550_8b36_4273_8df7_2b76c140e73a", - terria - ); - wmsItem.setTrait(CommonStrata.definition, "name", "Foo"); - terria.addModel(wmsItem); - - const wmsItem2 = new WebMapServiceCatalogItem("another", terria); - wmsItem2.setTrait(CommonStrata.definition, "name", "Another"); - wmsItem2.setTrait( - CommonStrata.definition, - "url", - "https://data.gov.au/geoserver/taxation-statistics-2011-12/wms" - ); - terria.addModel(wmsItem2); - - runInAction(() => { - wms.setTrait(CommonStrata.definition, "members", [wmsItem2.uniqueId!]); - wms.setTrait( - CommonStrata.definition, - "name", - "Taxation Statistics 2011-2012" - ); - wms.setTrait( - CommonStrata.definition, - "url", - "https://data.gov.au/geoserver/taxation-statistics-2011-12/wms" - ); - }); - - autorun((dispose) => { - console.log("Run: " + wms.memberModels.length); - wms.memberModels.forEach((model) => { - if (CatalogMemberMixin.isMixedInto(model)) { - console.log(`${model.name}: ${model.uniqueId}`); - } - if (MappableMixin.isMixedInto(model)) { - console.log(model.mapItems); - } - }); - }); - - expect().nothing(); - }); -}); diff --git a/test/Models/FunctionParameters/DateParameterSpec.ts b/test/Models/FunctionParameters/DateParameterSpec.ts new file mode 100644 index 00000000000..5a492cbb368 --- /dev/null +++ b/test/Models/FunctionParameters/DateParameterSpec.ts @@ -0,0 +1,63 @@ +import Clock from "terriajs-cesium/Source/Core/Clock"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import WebProcessingServiceCatalogFunction from "../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction"; +import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; +import DateParameter from "../../../lib/Models/FunctionParameters/DateParameter"; +import Terria from "../../../lib/Models/Terria"; + +describe("DateParameter", function () { + let catalogFunction: WebProcessingServiceCatalogFunction; + let clock: Clock; + + beforeEach(function () { + const terria = new Terria(); + catalogFunction = new WebProcessingServiceCatalogFunction("test", terria); + clock = terria.timelineClock; + clock.currentTime = JulianDate.fromDate(new Date("2024-01-01T00:00")); + }); + + describe("its value", function () { + it("defaults to undefined", function () { + const param = new DateParameter(catalogFunction, { + id: "date", + clock + }); + expect(param.value).toBeUndefined(); + }); + + describe("when the parameter is marked as required", function () { + it("defaults to current clock time", function () { + const param = new DateParameter(catalogFunction, { + id: "date", + clock, + isRequired: true + }); + expect(param.value).toBeDefined(); + expect(param.value).toBe("2024-01-01"); + }); + }); + }); + + describe("set value", function () { + let param: DateParameter; + + beforeEach(function () { + param = new DateParameter(catalogFunction, { + id: "date", + clock + }); + }); + + it("sets the value correctly", function () { + param.setValue(CommonStrata.user, "2024-12-01"); + expect(param.value).toBe("2024-12-01"); + }); + + it("clears the value if the new value is not a valid date time", function () { + param.setValue(CommonStrata.user, "2024-12-01"); + expect(param.value).toBe("2024-12-01"); + param.setValue(CommonStrata.user, "2024-42-42"); + expect(param.value).toBeUndefined(); + }); + }); +}); diff --git a/test/Models/FunctionParameters/DateTimeParameterSpec.ts b/test/Models/FunctionParameters/DateTimeParameterSpec.ts new file mode 100644 index 00000000000..42b0f358272 --- /dev/null +++ b/test/Models/FunctionParameters/DateTimeParameterSpec.ts @@ -0,0 +1,63 @@ +import Clock from "terriajs-cesium/Source/Core/Clock"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import WebProcessingServiceCatalogFunction from "../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction"; +import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; +import DateTimeParameter from "../../../lib/Models/FunctionParameters/DateTimeParameter"; +import Terria from "../../../lib/Models/Terria"; + +describe("DateTimeParameter", function () { + let catalogFunction: WebProcessingServiceCatalogFunction; + let clock: Clock; + + beforeEach(function () { + const terria = new Terria(); + catalogFunction = new WebProcessingServiceCatalogFunction("test", terria); + clock = terria.timelineClock; + clock.currentTime = JulianDate.fromDate(new Date("2024-01-01T00:00")); + }); + + describe("its value", function () { + it("defaults to undefined", function () { + const param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock + }); + expect(param.value).toBeUndefined(); + }); + + describe("when the parameter is marked as required", function () { + it("defaults to current clock time", function () { + const param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock, + isRequired: true + }); + expect(param.value).toBeDefined(); + expect(param.value).toBe("2024-01-01T00:00"); + }); + }); + }); + + describe("set value", function () { + let param: DateTimeParameter; + + beforeEach(function () { + param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock + }); + }); + + it("sets the value correctly", function () { + param.setValue(CommonStrata.user, "2024-12-01T00:00"); + expect(param.value).toBe("2024-12-01T00:00"); + }); + + it("clears the value if the new value is not a valid date time", function () { + param.setValue(CommonStrata.user, "2024-12-01T00:00"); + expect(param.value).toBe("2024-12-01T00:00"); + param.setValue(CommonStrata.user, "2024-42-42T00:00"); + expect(param.value).toBeUndefined(); + }); + }); +}); diff --git a/test/Models/MapNavigation/MapNavigationModelSpec.ts b/test/Models/MapNavigation/MapNavigationModelSpec.ts index d322e497c72..1d231f13466 100644 --- a/test/Models/MapNavigation/MapNavigationModelSpec.ts +++ b/test/Models/MapNavigation/MapNavigationModelSpec.ts @@ -1,12 +1,10 @@ import Terria from "../../../lib/Models/Terria"; -import ViewState from "../../../lib/ReactViewModels/ViewState"; import MapNavigationModel from "../../../lib/ViewModels/MapNavigation/MapNavigationModel"; import { IMapNavigationItem } from "../../../lib/ViewModels/MapNavigation/MapNavigationModel"; import { GenericMapNavigationItemController } from "../../../lib/ViewModels/MapNavigation/MapNavigationItemController"; describe("MapNavigationModel", function () { let terria: Terria; - let viewState: ViewState; let item1: IMapNavigationItem; let item2: IMapNavigationItem; let item3: IMapNavigationItem; @@ -18,10 +16,6 @@ describe("MapNavigationModel", function () { terria = new Terria({ baseUrl: "./" }); - viewState = new ViewState({ - terria: terria, - catalogSearchProvider: undefined - }); item1 = { id: "item1", name: "item1", diff --git a/test/Models/SearchProviders/BingMapsSearchProviderSpec.ts b/test/Models/SearchProviders/BingMapsSearchProviderSpec.ts index 557fe5b848e..a5f9b35d7cb 100644 --- a/test/Models/SearchProviders/BingMapsSearchProviderSpec.ts +++ b/test/Models/SearchProviders/BingMapsSearchProviderSpec.ts @@ -52,7 +52,7 @@ describe("BingMapsSearchProvider", function () { }) ); - const searchResult = bingMapsSearchProvider.search("test"); + bingMapsSearchProvider.search("test"); expect(test).toHaveBeenCalledWith( new Resource({ @@ -86,7 +86,7 @@ describe("BingMapsSearchProvider", function () { false ); }); - const test = spyOn(loadJsonp, "loadJsonp").and.returnValue( + spyOn(loadJsonp, "loadJsonp").and.returnValue( Promise.resolve({ resourceSets: [ { diff --git a/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts b/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts index e0858fc3542..49365eca40e 100644 --- a/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts +++ b/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts @@ -49,7 +49,6 @@ describe("CesiumIonSearchProvider", () => { new Promise((resolve) => resolve([])) ); const result = await searchProvider.search("test"); - console.log(result); expect(result.results.length).toBe(0); expect(result.message?.content).toBe( "translate#viewModels.searchNoLocations" diff --git a/test/Models/TerriaSpec.ts b/test/Models/TerriaSpec.ts index 9a1eba8fc6b..ea41ef46f2a 100644 --- a/test/Models/TerriaSpec.ts +++ b/test/Models/TerriaSpec.ts @@ -1148,7 +1148,7 @@ describe("Terria", function () { ]); done(); }) - .catch((error) => { + .catch((_error) => { done.fail(); }); }); @@ -1526,11 +1526,13 @@ describe("Terria", function () { describe("mapSettings", function () { it("properly interprets map hash parameter", async () => { const getLocalPropertySpy = spyOn(terria, "getLocalProperty"); - //@ts-expect-error - const location: Location = { + const location = { href: "http://test.com/#map=2d" - }; - await terria.start({ configUrl: "", applicationUrl: location }); + } as Location; + await terria.start({ + configUrl: "", + applicationUrl: location + }); await terria.loadPersistedMapSettings(); expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet); expect(getLocalPropertySpy).not.toHaveBeenCalledWith("viewermode"); @@ -1552,11 +1554,13 @@ describe("Terria", function () { terria, "getLocalProperty" ).and.returnValue("3dsmooth"); - //@ts-expect-error - const location: Location = { + const location = { href: "http://test.com/#map=4d" - }; - await terria.start({ configUrl: "", applicationUrl: location }); + } as Location; + await terria.start({ + configUrl: "", + applicationUrl: location + }); await terria.loadPersistedMapSettings(); expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Cesium); expect(terria.mainViewer.viewerOptions.useTerrain).toBe(false); @@ -1584,7 +1588,7 @@ describe("Terria", function () { await terria.loadInitSources(); expect(terria.baseMaximumScreenSpaceError).toBe(1); - expect(terria.useNativeResolution).toBeTruthy; + expect(terria.useNativeResolution).toBeTruthy(); expect(terria.timelineStack.alwaysShowingTimeline).toBeTruthy(); expect(setBaseMapSpy).toHaveBeenCalledWith( terria.baseMapsModel.baseMapItems.find( diff --git a/test/Models/WorkbenchSpec.ts b/test/Models/WorkbenchSpec.ts index 70553b9dd08..0fa10c69b77 100644 --- a/test/Models/WorkbenchSpec.ts +++ b/test/Models/WorkbenchSpec.ts @@ -1,16 +1,20 @@ import CommonStrata from "../../lib/Models/Definition/CommonStrata"; import MagdaReference from "../../lib/Models/Catalog/CatalogReferences/MagdaReference"; -import { BaseModel } from "../../lib/Models/Definition/Model"; import Terria from "../../lib/Models/Terria"; import WebMapServiceCatalogItem from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem"; import Workbench from "../../lib/Models/Workbench"; import Result from "../../lib/Core/Result"; import TerriaError, { TerriaErrorSeverity } from "../../lib/Core/TerriaError"; +import TerriaReference from "../../lib/Models/Catalog/CatalogReferences/TerriaReference"; describe("Workbench", function () { let terria: Terria; let workbench: Workbench; - let item1: BaseModel, item2: BaseModel, item3: BaseModel, item4: BaseModel; + let item1: WebMapServiceCatalogItem, + item2: WebMapServiceCatalogItem, + item3: WebMapServiceCatalogItem, + item4: WebMapServiceCatalogItem, + ref: TerriaReference; beforeEach(function () { terria = new Terria(); @@ -20,6 +24,9 @@ describe("Workbench", function () { item2 = new WebMapServiceCatalogItem("B", terria); item3 = new WebMapServiceCatalogItem("C", terria); item4 = new WebMapServiceCatalogItem("D", terria); + ref = new TerriaReference("test", new Terria()); + ref.setTrait(CommonStrata.user, "url", "test/init/wms-v8.json"); + ref.setTrait(CommonStrata.user, "path", ["MLzS8W", "fCUx4Y"]); item1.setTrait("definition", "url", "test/WMS/single_metadata_url.xml"); item2.setTrait("definition", "url", "test/WMS/single_metadata_url.xml"); @@ -31,6 +38,24 @@ describe("Workbench", function () { terria.addModel(item3); }); + it("can collapse/expand all catalog items", async function () { + // See if it is compatible with references as well as ordinary catalog items. + workbench.items = [item1, ref]; + await ref.loadReference(); + workbench.collapseAll(); + expect(item1.isOpenInWorkbench).toBe(false); + expect((ref.target as WebMapServiceCatalogItem).isOpenInWorkbench).toBe( + false + ); + expect(workbench.shouldExpandAll).toBe(true); + workbench.expandAll(); + expect(item1.isOpenInWorkbench).toBe(true); + expect((ref.target as WebMapServiceCatalogItem).isOpenInWorkbench).toBe( + true + ); + expect(workbench.shouldExpandAll).toBe(false); + }); + it("re-orders items correctly", function () { workbench.items = [item1, item2, item3]; diff --git a/test/Models/parseCustomMarkdownToReactTsSpec.ts b/test/Models/parseCustomMarkdownToReactTsSpec.ts index 10b4c7128b2..55a2693ce76 100644 --- a/test/Models/parseCustomMarkdownToReactTsSpec.ts +++ b/test/Models/parseCustomMarkdownToReactTsSpec.ts @@ -1,6 +1,5 @@ import { parseCustomMarkdownToReactWithOptions } from "../../lib/ReactViews/Custom/parseCustomMarkdownToReact"; import registerCustomComponentTypes from "../../lib/ReactViews/Custom/registerCustomComponentTypes"; -import Terria from "../../lib/Models/Terria"; import { TooltipWithButtonLauncher } from "../../lib/ReactViews/Generic/TooltipWrapper"; const isComponentOfType: any = @@ -8,12 +7,7 @@ const isComponentOfType: any = const findAll: any = require("react-shallow-testutils").findAll; describe("parseCustomMarkdownToReactTs", function () { - let terria: Terria; - beforeEach(function () { - terria = new Terria({ - baseUrl: "./" - }); registerCustomComponentTypes(); }); it("correctly parses tooltip terms", function () { diff --git a/test/ReactViews/Custom/Chart/BottomDockChartSpec.tsx b/test/ReactViews/Custom/Chart/BottomDockChartSpec.tsx index 3851c078098..f2e71fe67fd 100644 --- a/test/ReactViews/Custom/Chart/BottomDockChartSpec.tsx +++ b/test/ReactViews/Custom/Chart/BottomDockChartSpec.tsx @@ -1,6 +1,8 @@ -import { ReactTestRenderer } from "react-test-renderer"; +import TestRenderer, { act, ReactTestRenderer } from "react-test-renderer"; import { ChartItem } from "../../../../lib/ModelMixins/ChartableMixin"; import Terria from "../../../../lib/Models/Terria"; +import BottomDockChart from "../../../../lib/ReactViews/Custom/Chart/BottomDockChart"; +import PointOnMap from "../../../../lib/ReactViews/Custom/Chart/PointOnMap"; describe("BottomDockChart", function () { let terria: Terria; @@ -49,21 +51,19 @@ describe("BottomDockChart", function () { ]; }); - // FIXME: disabling because the new version of `withParentSize` from - // `@vx/responsive` uses ResizeObserver to trigger render which doesn't seem to - // work correctly in tests - // - /* it("renders all points on map for active chart items", function() { - * act(() => { - * testRenderer = TestRenderer.create( - * - * ); - * }); - * const pointsOnMap = testRenderer.root.findAllByType(PointOnMap); - * expect(pointsOnMap.length).toBe(2); - * }); */ + it("renders all points on map for active chart items", function () { + act(() => { + testRenderer = TestRenderer.create( + + ); + }); + const pointsOnMap = testRenderer.root.findAllByType(PointOnMap); + expect(pointsOnMap.length).toBe(2); + }); }); diff --git a/test/ReactViews/Custom/Chart/ChartCustomComponentSpec.tsx b/test/ReactViews/Custom/Chart/ChartCustomComponentSpec.tsx index 1eb3a58f81b..4f42bf8e580 100644 --- a/test/ReactViews/Custom/Chart/ChartCustomComponentSpec.tsx +++ b/test/ReactViews/Custom/Chart/ChartCustomComponentSpec.tsx @@ -81,10 +81,7 @@ describe("ChartCustomComponent", function () { sources: "a, b" } }; - const spy = spyOn( - component, - "constructShareableCatalogItem" - ).and.callThrough(); + spyOn(component, "constructShareableCatalogItem").and.callThrough(); component.processNode(context, node, [], 0); expect(component.constructShareableCatalogItem).toHaveBeenCalledTimes(2); // Make sure the id is dependent on parent, title & source name diff --git a/test/ReactViews/DimensionSelectorSectionSpec.tsx b/test/ReactViews/DimensionSelectorSectionSpec.tsx index 6a73decb1cf..e0469918cf7 100644 --- a/test/ReactViews/DimensionSelectorSectionSpec.tsx +++ b/test/ReactViews/DimensionSelectorSectionSpec.tsx @@ -43,7 +43,7 @@ export default class TestCatalogItem ], selectedId: "option-2", allowUndefined: true, - setDimensionValue: (stratumId: string, newStyle: string) => {} + setDimensionValue: (_stratumId: string, _newStyle: string) => {} }, { id: "some-id-2", @@ -55,7 +55,7 @@ export default class TestCatalogItem ], selectedId: "option-3", allowUndefined: false, - setDimensionValue: (stratumId: string, newStyle: string) => {} + setDimensionValue: (_stratumId: string, _newStyle: string) => {} }, { id: "some-id-3", @@ -67,7 +67,7 @@ export default class TestCatalogItem ], selectedId: "option-8", allowUndefined: false, - setDimensionValue: (stratumId: string, newStyle: string) => {}, + setDimensionValue: (_stratumId: string, _newStyle: string) => {}, disable: true }, { @@ -79,7 +79,7 @@ export default class TestCatalogItem ], selectedId: "false", type: "checkbox", - setDimensionValue: (stratumId, newStyle) => {} + setDimensionValue: (_stratumId, _newStyle) => {} } ]; } diff --git a/test/ReactViews/FeatureInfoSectionSpec.tsx b/test/ReactViews/FeatureInfoSectionSpec.tsx index ebfbc6bc47f..6b023426295 100644 --- a/test/ReactViews/FeatureInfoSectionSpec.tsx +++ b/test/ReactViews/FeatureInfoSectionSpec.tsx @@ -25,10 +25,10 @@ import TerriaFeature from "../../lib/Models/Feature/Feature"; import Terria from "../../lib/Models/Terria"; import ViewState from "../../lib/ReactViewModels/ViewState"; import { FeatureInfoSection } from "../../lib/ReactViews/FeatureInfo/FeatureInfoSection"; -import mixTraits from "../../lib/Traits/mixTraits"; import DiscretelyTimeVaryingTraits from "../../lib/Traits/TraitsClasses/DiscretelyTimeVaryingTraits"; import FeatureInfoUrlTemplateTraits from "../../lib/Traits/TraitsClasses/FeatureInfoTraits"; import MappableTraits from "../../lib/Traits/TraitsClasses/MappableTraits"; +import mixTraits from "../../lib/Traits/mixTraits"; import * as FeatureInfoPanel from "../../lib/ViewModels/FeatureInfoPanel"; import { createWithContexts } from "./withContext"; import CsvCatalogItem from "../../lib/Models/Catalog/CatalogItems/CsvCatalogItem"; @@ -382,6 +382,31 @@ describe("FeatureInfoSection", function () { expect(findWithText(result, "ham").length).toEqual(1); }); + it("gracefully handles bad nested JSON", function () { + feature = new Entity({ + name: "Meals", + properties: { + somethingBad: "{broken object", + somethingGood: JSON.stringify({ good: "this object is good" }) + } + }); + const section = ( + {}} + /> + ); + const result = createWithContexts(viewState, section); + + expect(findWithText(result, "{broken object").length).toEqual(1); + expect( + findWithText(result, `{"good":"this object is good"}`).length + ).toEqual(1); + }); + describe("templating", function () { it("uses and completes a string-form featureInfoTemplate if present", function () { const template = "This is a {{material}} {{foo}}."; diff --git a/test/ReactViews/Map/Panels/LangPanel/LangPanelSpec.tsx b/test/ReactViews/Map/Panels/LangPanel/LangPanelSpec.tsx index 2003f843a51..e1b298084cf 100644 --- a/test/ReactViews/Map/Panels/LangPanel/LangPanelSpec.tsx +++ b/test/ReactViews/Map/Panels/LangPanel/LangPanelSpec.tsx @@ -3,13 +3,11 @@ import React from "react"; import { act } from "react-dom/test-utils"; import Terria from "../../../../../lib/Models/Terria"; -import ViewState from "../../../../../lib/ReactViewModels/ViewState"; import LangPanel from "../../../../../lib/ReactViews/Map/Panels/LangPanel/LangPanel"; describe("LangPanel", function () { let terria: Terria; - let viewState: ViewState; let testRenderer: any; @@ -17,11 +15,6 @@ describe("LangPanel", function () { terria = new Terria({ baseUrl: "./" }); - - viewState = new ViewState({ - terria: terria, - catalogSearchProvider: undefined - }); }); it("should not render if there is no langauge config", function () { diff --git a/test/ReactViews/Mobile/MobileHeaderSpec.tsx b/test/ReactViews/Mobile/MobileHeaderSpec.tsx index 12f94b252ba..c6a22699681 100644 --- a/test/ReactViews/Mobile/MobileHeaderSpec.tsx +++ b/test/ReactViews/Mobile/MobileHeaderSpec.tsx @@ -17,7 +17,6 @@ describe("MobileHeader", function () { terria = new Terria({ baseUrl: "./" }); - terria; viewState = new ViewState({ terria: terria, catalogSearchProvider: undefined diff --git a/test/ReactViews/MoreShallowTools.js b/test/ReactViews/MoreShallowTools.js index c52e091d7da..a8b249bf490 100644 --- a/test/ReactViews/MoreShallowTools.js +++ b/test/ReactViews/MoreShallowTools.js @@ -16,7 +16,7 @@ export function getShallowRenderedOutput(jsx) { export function getMountedInstance(jsx) { const renderer = getRenderedRenderer(jsx); - return renderer.getMountedInstance(renderer); + return renderer.getMountedInstance(); } export function findAllEqualTo(reactElement, text) { diff --git a/test/ReactViews/Tools/ItemSearchTool/SearchFormSpec.tsx b/test/ReactViews/Tools/ItemSearchTool/SearchFormSpec.tsx index ff4fdaa7ed1..d2c928bb864 100644 --- a/test/ReactViews/Tools/ItemSearchTool/SearchFormSpec.tsx +++ b/test/ReactViews/Tools/ItemSearchTool/SearchFormSpec.tsx @@ -63,6 +63,6 @@ function render( act(() => { rendered = create(); }); - // @ts-expect-error + // @ts-expect-error assigned in callback return rendered; } diff --git a/test/ReactViews/Tools/ItemSearchTool/SearchResultsSpec.tsx b/test/ReactViews/Tools/ItemSearchTool/SearchResultsSpec.tsx index 734da60a44a..6e98364b89e 100644 --- a/test/ReactViews/Tools/ItemSearchTool/SearchResultsSpec.tsx +++ b/test/ReactViews/Tools/ItemSearchTool/SearchResultsSpec.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { act, create, ReactTestRenderer } from "react-test-renderer"; import { ItemSearchResult } from "../../../../lib/Models/ItemSearchProviders/ItemSearchProvider"; import Terria from "../../../../lib/Models/Terria"; @@ -30,7 +29,7 @@ async function render( act(() => { rendered = create(); }); - // @ts-expect-error + // @ts-expect-error assigned in callback return rendered; } diff --git a/test/ReactViews/Workbench/Controls/FilterSectionSpec.tsx b/test/ReactViews/Workbench/Controls/FilterSectionSpec.tsx new file mode 100644 index 00000000000..666ceb0cf45 --- /dev/null +++ b/test/ReactViews/Workbench/Controls/FilterSectionSpec.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { act } from "react-dom/test-utils"; +import TestRenderer, { ReactTestRenderer } from "react-test-renderer"; +import FilterSection from "../../../../lib/ReactViews/Workbench/Controls/FilterSection"; +import { Range } from "rc-slider"; +import Terria from "../../../../lib/Models/Terria"; +import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; +import CreateModel from "../../../../lib/Models/Definition/CreateModel"; +import { FilterTraits } from "../../../../lib/Traits/TraitsClasses/Cesium3dTilesTraits"; +import objectArrayTrait from "../../../../lib/Traits/Decorators/objectArrayTrait"; +import ModelTraits from "../../../../lib/Traits/ModelTraits"; +import { runInAction } from "mobx"; + +class TestTraits extends ModelTraits { + @objectArrayTrait({ + type: FilterTraits, + idProperty: "name", + name: "filters", + description: "The filters to apply to this catalog item." + }) + filters?: FilterTraits[]; +} + +class TestModel extends CreateModel(TestTraits) {} + +describe("FilterSectionSpec", function () { + let testRenderer: ReactTestRenderer; + let terria: Terria; + let item: TestModel; + + beforeAll(() => { + terria = new Terria({ + baseUrl: "./" + }); + item = new TestModel("test", terria); + }); + + it("Renders nothing if no filters", function () { + testRenderer = TestRenderer.create(); + expect(testRenderer.root.children.length).toBe(0); + }); + + it("Renders a range input for each filter", function () { + runInAction(() => { + const filter = item.addObject( + CommonStrata.user, + "filters", + "level-filter" + ); + filter?.setTrait(CommonStrata.user, "property", "level"); + filter?.setTrait(CommonStrata.user, "minimumValue", 0); + filter?.setTrait(CommonStrata.user, "maximumValue", 42); + filter?.setTrait(CommonStrata.user, "minimumShown", 10); + filter?.setTrait(CommonStrata.user, "maximumShown", 20); + }); + + act(() => { + testRenderer = TestRenderer.create(); + const rangeInputs = testRenderer.root.findAllByType(Range); + expect(rangeInputs.length).toBe(1); + }); + }); +}); diff --git a/test/ReactViews/Workbench/Controls/LegendSpec.tsx b/test/ReactViews/Workbench/Controls/LegendSpec.tsx index b3f5b6e7ce0..06bdc4ad71e 100644 --- a/test/ReactViews/Workbench/Controls/LegendSpec.tsx +++ b/test/ReactViews/Workbench/Controls/LegendSpec.tsx @@ -1,6 +1,5 @@ const findAllWithClass = require("react-shallow-testutils").findAllWithClass; -import React from "react"; import TestRenderer from "react-test-renderer"; import CsvCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/CsvCatalogItem"; import WebMapServiceCatalogItem from "../../../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem"; @@ -49,8 +48,7 @@ describe("Legend", function () { .then(() => { const legendSection = ; const result = getShallowRenderedOutput(legendSection); - // @ts-expect-error - expect(result).toEqual(null); + expect(result).toBeNull(); }) .then(done); }); diff --git a/test/ViewModels/UploadDataTypesSpec.ts b/test/ViewModels/UploadDataTypesSpec.ts index f419c58e2b1..b0c2aeb7460 100644 --- a/test/ViewModels/UploadDataTypesSpec.ts +++ b/test/ViewModels/UploadDataTypesSpec.ts @@ -16,7 +16,7 @@ describe("UploadDataTypes", function () { }); it("returns all the builtin remote upload types", function () { - expect(UploadDataTypes.getDataTypes().remoteDataType.length).toEqual(23); + expect(UploadDataTypes.getDataTypes().remoteDataType.length).toEqual(26); }); }); @@ -56,7 +56,8 @@ describe("UploadDataTypes", function () { { value: "foo42", name: "Another Foo type", - description: "Some other foo files" + description: "Some other foo files", + customComponent: undefined } ]); }); @@ -82,12 +83,14 @@ describe("UploadDataTypes", function () { { value: "foo42", name: "Foo type", - description: "Foo files" + description: "Foo files", + customComponent: undefined }, { value: "foo42", name: "Another Foo type", - description: "Some other foo files" + description: "Some other foo files", + customComponent: undefined } ]); }); @@ -129,7 +132,8 @@ describe("UploadDataTypes", function () { { value: "foo42", name: "Another Foo type", - description: "Some other foo files" + description: "Some other foo files", + customComponent: undefined } ]); }); @@ -155,12 +159,14 @@ describe("UploadDataTypes", function () { { value: "foo42", name: "Foo type", - description: "Foo files" + description: "Foo files", + customComponent: undefined }, { value: "foo42", name: "Another Foo type", - description: "Some other foo files" + description: "Some other foo files", + customComponent: undefined } ]); }); diff --git a/wwwroot/cesium-ion-oauth2.html b/wwwroot/cesium-ion-oauth2.html new file mode 100644 index 00000000000..ea8ae6ac4e2 --- /dev/null +++ b/wwwroot/cesium-ion-oauth2.html @@ -0,0 +1,36 @@ + + + + Cesium ion OAuth2 Token Exchange + + + + diff --git a/wwwroot/languages/ca/translation.json b/wwwroot/languages/ca/translation.json index d1b175bde64..72c2d6b6f77 100644 --- a/wwwroot/languages/ca/translation.json +++ b/wwwroot/languages/ca/translation.json @@ -709,7 +709,10 @@ "assimp-local-description": "**Avís:** el convertidor de fitxers 3D és experimental.\nConsulteu la llista de [formats admesos](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md).\nEls fitxers s'han de comprimir.", "assimp-remote": "Convertidor de fitxers 3D (experimental)", "assimp-remote-description": "**Avís:** el convertidor de fitxers 3D és experimental.\nConsulteu la llista de [formats admesos](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md).\nTambé s'admeten fitxers zip", - "carto-v3": "Carto V3" + "carto-v3": "Carto V3", + "i3s": "I3S", + "esri-imageServer": "Esri ArcGIS ImageServer (capa única)", + "cesium-ion": "Cesium ion" }, "printWindow": { "errorTitle": "Error d'impressió", @@ -2079,6 +2082,14 @@ "name": "Mín" }, "name": "Estil" + }, + "arcGisImageServerCatalogItem": { + "invalidUrlTitle": "No es pot carregar ImageServer", + "invalidServiceTitle": "ArcGIS Image Service invàlid", + "rasterFunction": "Funció de ràster", + "name": "Esri ArcGIS ImageServer", + "invalidUrlMessage": "No s'ha pogut carregar el punt final de l'ArcGis ImageServer perquè l'element del catàleg no té una \"url\".", + "invalidServiceMessage": "S'ha produït un error en invocar ArcGIS Image Service. La resposta del servidor no sembla ser un document Image Service vàlid." } }, "deltaTool": { diff --git a/wwwroot/languages/cs/translation.json b/wwwroot/languages/cs/translation.json index 563bd1949d5..360b220364b 100644 --- a/wwwroot/languages/cs/translation.json +++ b/wwwroot/languages/cs/translation.json @@ -197,7 +197,7 @@ }, "share": { "browserPrint": "Chcete-li dosáhnout lepších výsledků při tisku, použijte místo funkce tisku webového prohlížeče tlačítko Tisk {{appName}}.", - "localDataNote": "<0><0>Poznámka:<1>Následující zdroje dat NEBUDOU sdíleny, protože obsahují data z tohoto místního systému. Chcete-li tyto zdroje dat sdílet, zveřejněte jejich data na webovém serveru a <2>přidejte je pomocí url adresy.", + "localDataNote": "<0><0>Poznámka:<1>Následující zdroje dat NEBUDOU sdíleny, protože obsahují data z tohoto místního systému nebo z ověřené online služby. Chcete-li tyto zdroje dat sdílet, zveřejněte jejich data na webovém serveru a <2>přidejte je pomocí url adresy.", "generatingUrl": "Generování sdílené adresy URL...", "printTitle": "Tisk mapy", "printExplanation": "Otevřít tisknutelnou verzi této mapy.", @@ -463,7 +463,7 @@ "resultsLabel": "Výsledky hledání", "done": "Hotovo", "data": "Data", - "searchInDataCatalog": "Hledání <1>'{{locationSearchText}}' v katalogu dat", + "searchInDataCatalog": "Hledání '{{locationSearchText}}' v katalogu dat", "search": "Hledat '{{searchText}}' v Katalogu dat", "viewMore": "Zobrazit více výsledků {{name}}", "placeholder": "Hledání lokalit", @@ -504,7 +504,8 @@ "previewItemErrorTitle": "Položku katalogu nelze zobrazit.", "dataDescription": "Popis dat", "resourceDescription": "Popis Zdroje", - "mayBeExperiencingIssues": "U této datové sady může v současné době docházet k problémům" + "mayBeExperiencingIssues": "U této datové sady může v současné době docházet k problémům", + "selectMultipleDatasets": "<0>Stiskněte Shift a klikněte<1 /><2>pro přidání více datových sad" }, "description": { "name": "Popis", @@ -649,7 +650,7 @@ "expand": "Rozbalit", "download": "Stáhnout", "loading": "Načítání dat grafu...", - "noData": "Nenalezeny žádné údaje, které by bylo možné zobrazit v tabulce.", + "noData": "Nebyla nalezena žádná data grafu.", "showItemInChart": "Zobrazit {{value}} v tabulce" }, "viewModels": { @@ -662,7 +663,10 @@ "searchLocations": "Poloha", "searchNoPlaceNames": "Litujeme, ale vašemu vyhledávacímu dotazu neodpovídají žádné oficiální názvy míst.", "searchErrorOccurred": "Při hledání došlo k chybě. Zkontrolujte prosím připojení k internetu nebo to zkuste znovu později.", - "searchPlaceNames": "Oficiální názvy míst" + "searchPlaceNames": "Oficiální názvy míst", + "searchMinCharacters_0": "Je třeba zadat minimálně {{count}} znak", + "searchMinCharacters_1": "Je třeba zadat minimálně {{count}} znaky", + "searchMinCharacters_2": "Je třeba zadat minimálně {{count}} znaků" }, "terriaViewer": { "slowWebGLAvailableTitle": "Slabý výkon WebGL", @@ -730,7 +734,10 @@ "assimp-local-description": "**Upozornění:** Konvertor 3D souborů je experimentální. \nViz seznam [podporovaných formátů](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nSoubory musí být zazipovány.", "assimp-remote": "Převodník 3D souborů (experimentální)", "assimp-remote-description": "**Upozornění:** Konvertor 3D souborů je experimentální. \nViz seznam [podporovaných formátů](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nPodporovány jsou také soubory Zip", - "carto-v3": "Carto V3" + "carto-v3": "Carto V3", + "i3s": "I3S", + "esri-imageServer": "Esri ArcGIS ImageServer (jedna vrstva)", + "cesium-ion": "Ionty cesia" }, "printWindow": { "errorTitle": "Chyba tisku", @@ -1304,7 +1311,13 @@ "timeDimensionDisabled": "Čas zakázán (kliknutím povolíte)", "bulkGeocoderInfoMessage": "CSV obsahuje adresy, ale {{number}} nelze na mapě najít: ", "bulkGeocoderInfo2Message": "V CSV chybí adresy {{nullAddresses}}.", - "timeDimensionEnabled": "Čas povolen (kliknutím jej zakážete)" + "timeDimensionEnabled": "Čas povolen (kliknutím jej zakážete)", + "custom": "Vlastní", + "regionMapping": "Mapování regionu", + "editStyle": "Upravit Styl", + "activeStyle": "Zobrazit proměnnou", + "regionColumn": "Sloupec Region", + "manualRegionMapping": "Ruční mapování regionu" }, "terria": { "loadConfigErrorTitle": "Nepodařilo se načíst konfiguraci mapy Terria", @@ -1391,7 +1404,7 @@ "missingUrlTitle": "Nelze načíst GetCapabilities", "missingUrlMessage": "Nepodařilo se načíst dokument GetCapabilities služby WFS (Web Feature Service), protože položka katalogu nemá `url`.", "noLayerFoundTitle": "Nenalezena žádná vrstva", - "noLayerFoundMessage": "Datová sada WFS '{{name}}' nemá žádné vrstvy odpovídající '{{typeNames}}'. {{suggested}} {{line}}{{line}}Buď byl katalogový soubor nastaven nesprávně, nebo se změnil server WFS.", + "noLayerFoundMessage": "WFS datová sada '{{name}}' nemá žádné vrstvy odpovídající '{{typeNames}}'. Buď byl soubor katalogu nastaven nesprávně, nebo se změnil server WFS.", "reachedMaxFeatureLimit": "Varování: Tato vrstva dosáhla limitu funkcí WFS ({{maxFeatures}})" }, "webMapServiceCatalogGroup": { @@ -1567,6 +1580,515 @@ }, "showClippingBox": "Zobrazit ořezový rámeček", "keepBoxAboveGround": "Pouze nad zemí" + }, + "tableStyling": { + "fill": { + "selectableDimensions": { + "type": { + "name": "Typ", + "undefinedLabel": "Prosím upřesněte", + "options": { + "noStyle": { + "name": "Bez stylu" + }, + "sequentialContinuous": { + "name": "Sekvenční (průběžný)" + }, + "sequentialDiscrete": { + "name": "Sekvenční (diskrétní)" + }, + "divergingContinuous": { + "name": "Divergentní (průběžný)" + }, + "divergingDiscrete": { + "name": "Divergentní (diskrétní)" + }, + "qualitative": { + "name": "Kvalitativní" + }, + "customQualitative": { + "name": "Vlastní (kvalitativní)" + }, + "customDiscrete": { + "name": "Vlastní (diskrétní)" + } + } + }, + "tableColorColumn": { + "name": "Proměnná" + }, + "dataType": { + "name": "Typ sloupce (rozšířený)" + }, + "scheme": { + "name": "Schéma" + }, + "numberOfBins": { + "name": "Počet zásobníků" + } + }, + "name": "Barva výplně" + }, + "hideAdvancedOptions": "Skrýt pokročilé možnosti", + "reset": "Obnovit výchozí styl", + "copyUserStratum": "Zkopírujte uživatelskou vrstvu do schránky", + "data": { + "name": "Data", + "selectableDimensions": { + "tableStyleType": { + "name": "Symbologie", + "options": { + "fill": { + "name": "Barva výplně" + }, + "pointSize": { + "name": "Velikost bodu" + }, + "point": { + "name": "Styl bodu / značky" + }, + "outline": { + "name": "Barva obrysu" + }, + "label": { + "name": "Styl štítku" + }, + "trail": { + "name": "Styl trasy" + } + } + }, + "tableStyle": { + "name": "Styl" + }, + "dataset": { + "name": "Datová sada" + } + } + }, + "name": "Styl", + "showAdvancedOptions": "Zobrazit pokročilé možnosti", + "bins": { + "selectableDimensions": { + "start": { + "selectableDimensions": { + "start": { + "name": "Start" + }, + "stop": { + "name": "Stop" + }, + "color": { + "name": "Barva" + } + }, + "name": "{{value1}} až {{value2}}" + } + }, + "name": "Zásobníky" + }, + "nullColor": { + "name": "Výchozí barva" + }, + "outlierColor": { + "name": "Odlehlá barva" + }, + "additionalColors": { + "name": "Další barvy", + "selectableDimensions": { + "nullColor": { + "name": "Výchozí barva" + }, + "outlierColor": { + "name": "Odlehlá barva" + }, + "regionColor": { + "name": "Barva regionu" + } + } + }, + "pointSize": { + "selectableDimensions": { + "pointSizeNull": { + "name": "Výchozí velikost" + }, + "pointSizeColumn": { + "name": "Proměnná" + }, + "pointSizesFactor": { + "name": "Faktor velikosti" + }, + "pointSizeOffset": { + "name": "Odsazení velikosti" + } + }, + "name": "Velikost bodu" + }, + "timeOptions": { + "selectableDimensions": { + "tableTimeIsSampled": { + "options": { + "true": { + "name": "Ano" + }, + "false": { + "name": "Ne" + } + }, + "name": "Je vybrán vzorek" + }, + "tableEndTimeColumn": { + "name": "Sloupec času ukončení" + }, + "tableTimeIdColumns": { + "name": "ID sloupců" + }, + "tableTimeDisplayDuration": { + "name": "Doba zobrazení" + }, + "tableTimeSpreadStartTime": { + "options": { + "true": { + "name": "Ano" + }, + "false": { + "name": "Ne" + } + }, + "name": "Rozptyl času spuštění" + }, + "tableTimeSpreadFinishTime": { + "name": "Rozptyl času dokončení", + "options": { + "true": { + "name": "Ano" + }, + "false": { + "name": "Ne" + } + } + }, + "tableTimeColumn": { + "name": "Sloupec času" + } + }, + "name": "Možnosti času" + }, + "workbenchOptions": { + "name": "Možnosti pracovní plochy", + "selectableDimensions": { + "tableStyleEnalbed": { + "name": "Zobrazit styl v pracovní ploše", + "options": { + "false": { + "name": "Styl skrytý" + }, + "true": { + "name": "Styl zobrazený" + } + } + }, + "showDisableStyleOption": { + "name": "Zobrazit možnost zakázat styl" + }, + "showDisableTimeOption": { + "name": "Zobrazit možnost vypnutí času" + }, + "enableManualRegionMapping": { + "name": "Povolit ruční mapování oblastí" + } + } + }, + "style": { + "selectableDimensions": { + "bin": { + "name": "Koš stylů", + "selectableDimensions": { + "add": { + "value": "Přidat koš stylů" + }, + "bin": { + "range": "{{value1}} až {{value2}}", + "selectableDimensions": { + "remove": { + "value": "Odebrat" + }, + "start": { + "name": "Start" + }, + "stop": { + "name": "Stop" + } + }, + "noValue": "Bez hodnoty" + } + } + }, + "column": { + "name": "Proměnná" + }, + "styleType": { + "name": "Typ", + "undefinedLabel": "Upřesněte prosím", + "constant": { + "name": "Bez stylu" + }, + "bin": { + "name": "Diskrétní" + }, + "enum": { + "name": "Kvalitativní" + } + }, + "enum": { + "selectableDimensions": { + "enum": { + "noValue": "Žádná hodnota", + "selectableDimensions": { + "value": { + "name": "Hodnota" + }, + "remove": { + "value": "Odebrat" + } + }, + "add": { + "value": "Přidat styl pro hodnotu" + } + } + }, + "name": "Výčet stylů" + } + }, + "null": { + "name": "Výchozí" + } + }, + "point": { + "selectableDimensions": { + "height": { + "name": "Výška" + }, + "marker": { + "name": "Značka", + "tooltip": "Marker podporuje URL a base64 libovolného podporovaného formátu obrázku (např. PNG, SVG)" + }, + "rotation": { + "name": "Rotace" + }, + "width": { + "name": "Šířka" + } + }, + "name": "Styl značky" + }, + "label": { + "name": "Styl štítku", + "selectableDimensions": { + "column": { + "name": "Sloupec štítku" + }, + "scale": { + "name": "Měřítko" + }, + "outlineColor": { + "name": "Barva obrysu" + }, + "outlineWidth": { + "name": "Šířka obrysu" + }, + "fillColor": { + "name": "Barva výplně" + }, + "font": { + "name": "Font" + }, + "style": { + "name": "Styl štítku", + "options": { + "fill": { + "name": "Pouze výplň" + }, + "outline": { + "name": "Pouze obrys" + }, + "fillAndOutline": { + "name": "Výplň a obrys" + } + } + }, + "horizontalOrigin": { + "options": { + "left": { + "name": "Vlevo" + }, + "center": { + "name": "Center" + }, + "right": { + "name": "Vpravo" + } + }, + "name": "Horizontální počátek" + }, + "verticalOrigin": { + "name": "Vertikální počátek", + "options": { + "top": { + "name": "Nahoře" + }, + "center": { + "name": "Center" + }, + "baseline": { + "name": "Baseline" + }, + "bottom": { + "name": "Dole" + } + } + }, + "offsetX": { + "name": "Pixel offset X" + }, + "offsetY": { + "name": "Pixel offset Y" + } + } + }, + "trail": { + "selectableDimensions": { + "trailTime": { + "name": "Čas trasy (sekundy)" + }, + "resolution": { + "name": "Rozlišení" + }, + "trailStyleOptions": { + "selectableDimensions": { + "material": { + "options": { + "solidColor": { + "name": "Plná barva" + }, + "polylineGlow": { + "name": "Záře polylinií" + } + }, + "name": "Typ materiálu" + } + }, + "name": "Možnosti stylu trasy" + }, + "width": { + "name": "Šířka" + }, + "solidColor": { + "name": "Plná barva" + }, + "taperPower": { + "name": "Síla kužele" + }, + "growColor": { + "name": "Barva záře" + }, + "leadTime": { + "name": "Průběžná doba (s)" + }, + "growPower": { + "name": "Síla záře" + } + } + }, + "displayRange": { + "name": "Rozsah zobrazení", + "selectableDimensions": { + "max": { + "name": "Max" + } + } + }, + "colors": { + "selectableDimensions": { + "value": { + "name": "Hodnota" + }, + "remove": { + "value": "Odebrat" + }, + "add": { + "value": "Přidat barvu" + }, + "color": { + "name": "Barva" + } + }, + "name": "Barvy" + }, + "legend": { + "name": "Legenda", + "selectableDimensions": { + "legendTitle": { + "name": "Název" + }, + "title": { + "name": "Název položky {{index}}" + }, + "legendTicks": { + "name": "Ticks" + } + } + }, + "styleOptions": { + "name": "Možnosti stylu", + "selectableDimensions": { + "latitudeColumn": { + "name": "Sloupec zeměpisná šířka" + }, + "styleTitle": { + "name": "Název" + }, + "longitudeColumn": { + "name": "Sloupec zeměpisná délka" + } + } + }, + "variableAndColumn": { + "selectableDimensions": { + "columnTitle": { + "name": "Název" + }, + "columnUnits": { + "name": "Jednotky" + } + }, + "name": "Proměnná/sloupec" + }, + "outline": { + "selectableDimensions": { + "color": { + "name": "Barva" + }, + "width": { + "name": "Šířka" + } + }, + "name": "Styl obrysu" + }, + "min": { + "name": "Min" + }, + "regionMapping": { + "name": "Mapování regionu" + } + }, + "arcGisImageServerCatalogItem": { + "invalidServiceMessage": "Při vyvolání služby ArcGIS Image Service došlo k chybě. Odpověď serveru se nezdá být platným dokumentem služby Image Service.", + "name": "Esri ArcGIS ImageServer", + "invalidUrlTitle": "Nelze načíst ImageServer", + "invalidUrlMessage": "Nelze načíst koncový bod ArcGis ImageServer, protože položka katalogu nemá `url`.", + "invalidServiceTitle": "Neplatná služba ArcGIS Image Service", + "rasterFunction": "Rastrová funkce" } }, "deltaTool": { @@ -1687,5 +2209,28 @@ }, "includeStory": { "message": "Zahrnout příběh do sdílení" + }, + "relatedMaps": { + "buttonText": "Související mapy", + "buttonTitle": "Zobrazit související mapy", + "panelHeading": "Související mapy", + "panelText": "Kliknutím na níže uvedenou mapu ji otevřete v samostatném okně nebo kartě." + }, + "searchProvider": { + "models": { + "idForMatchingErrorTitle": "Chybějící vlastnost", + "idForMatchingErrorMessage": "Objekty modelu musí mít vlastnost `id`, `localId` nebo `name`.", + "unsupportedTypeTitle": "Neznámý typ", + "unsupportedTypeMessage": "Nepodařilo se vytvořit neznámý typ modelu {{type}}." + }, + "noSearchProviders": "Neexistují žádní nakonfigurovaní poskytovatelé vyhledávání" + }, + "selectableDimensions": { + "undefinedLabel": "Neuvedeno", + "enabled": "Povoleno", + "disabled": "Vypnuto", + "colorAdd": "Přidat", + "colorRemove": "Odebrat", + "invalid": "Neplatné" } } diff --git a/wwwroot/languages/en/translation.json b/wwwroot/languages/en/translation.json index d5276345530..fe135e3fa8d 100644 --- a/wwwroot/languages/en/translation.json +++ b/wwwroot/languages/en/translation.json @@ -202,7 +202,7 @@ "browserPrint": "For better printed results, please use {{appName}}'s Print button instead of your web browser's print feature.", "shortLinkShortening": "Shortening...", "shortLinkError": "An error occurred while attempting to shorten the URL. Please check your internet connection and try again.", - "localDataNote": "<0><0>Note:<1>The following data sources will NOT be shared because they include data from this local system. To share these data sources, publish their data on a web server and <2>add them using a url.", + "localDataNote": "<0><0>Note:<1>The following data sources will NOT be shared because they include data from this local system or from an authenticated online service. To share these data sources, publish their data on a web server and <2>add them using a url.", "generatingUrl": "Generating share URL...", "printTitle": "Print Map", "printExplanation": "Open a printable version of this map.", @@ -695,6 +695,7 @@ "wps-getCapabilities": "Web Processing Service (WPS) Server", "esri-group": "Esri ArcGIS Server", "esri-mapServer": "Esri ArcGIS MapServer (single layer)", + "esri-imageServer": "Esri ArcGIS ImageServer (single layer)", "esri-featureServer": "Esri ArcGIS FeatureServer (single layer)", "3d-tiles": "3D Tiles", "open-street-map": "Open Street Map Server", @@ -716,7 +717,10 @@ "assimp-local-description": "**Warning:** 3D file converter is experimental. \nSee list of [supported formats](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nFiles must be zipped.", "assimp-remote": "3D file converter (experimental)", "assimp-remote-description": "**Warning:** 3D file converter is experimental. \nSee list of [supported formats](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nZip files are also supported", - "ifc": "IFC" + "ifc": "IFC", + "cog": "Cloud Optimised Geotiff", + "i3s": "I3S", + "cesium-ion": "Cesium ion" }, "printWindow": { "errorTitle": "Error printing", @@ -895,6 +899,14 @@ "noLayersFoundTitle": "Could not load ArcGIS MapServer.", "noLayersFoundMessage": "The Esri ArcGIS MapServer '{{name}}' has no layers to show." }, + "arcGisImageServerCatalogItem": { + "name": "Esri ArcGIS ImageServer", + "invalidUrlTitle": "Unable to load ImageServer", + "invalidUrlMessage": "Could not load the ArcGis ImageServer endpoint because the catalog item does not have a `url`.", + "invalidServiceTitle": "Invalid ArcGIS Image Service", + "invalidServiceMessage": "An error occurred while invoking the ArcGIS Image Service. The server's response does not appear to be a valid Image Service document.", + "rasterFunction": "Raster Function" + }, "bing": { "name": "Bing Maps" }, @@ -2028,6 +2040,9 @@ } } } + }, + "cogCatalogItem": { + "experimentalReprojectionWarning": "This COG is not in a native projection (EPSG:4326 or EPSG:3857). Reprojecting is experimental and may not work as expected. If you have control of the source data we recommend reprojecting to EPSG:4326 or EPSG:3857 using GDAL or similar." } }, "searchProvider": { diff --git a/wwwroot/languages/es/translation.json b/wwwroot/languages/es/translation.json index d1dffc7ff32..628eb50233e 100644 --- a/wwwroot/languages/es/translation.json +++ b/wwwroot/languages/es/translation.json @@ -35,7 +35,10 @@ "searchErrorOccurred": "Se ha producido un error en la búsqueda. Por favor, compruebe su conexión a internet o inténtelo más tarde.", "inDataCatalogue": "En el catálogo de datos", "searchNoCatalogueItem": "Lo sentimos, ningún artículo del catálogo coincide con su búsqueda.", - "searchNoPlaceNames": "Lo sentimos, ningún topónimo oficial coincide con su búsqueda." + "searchNoPlaceNames": "Lo sentimos, ningún topónimo oficial coincide con su búsqueda.", + "searchMinCharacters_0": "Necesita introducir al menos {{count}} carácter", + "searchMinCharacters_1": "Necesita introducir al menos {{count}} caracteres", + "searchMinCharacters_2": "Necesita introducir al menos {{count}} caracteres" }, "catalogItem": { "trash": "Quitar del catálogo", @@ -111,6 +114,303 @@ "contact_point": "Contacto", "author": "Autor", "licence": "Licencia" + }, + "webMapServiceCatalogItem": { + "getCapabilitiesUrl": "URL GetCapabilities", + "serviceDescription": "Descripción del servicio", + "serviceContact": "Contacto del servicio", + "accessConstraints": "Restricciones de acceso", + "noLayerFoundTitle": "Capa no encontrada", + "defaultStyleLabel": "Estilo por defecto", + "dataDescription": "Descripción de los datos" + }, + "userDrawing": { + "messageHeader": "Dibujar en el mapa", + "clickToAddFirstPoint": "Haga click para añadir un punto", + "btnCancel": "Cancelar", + "btnDone": "Hecho", + "clickToAddAnotherPoint": "Haga click para añadir otro punto" + }, + "raiseError": { + "notificationFeedback": "Para preguntas y comentarios, haga click aquí.", + "developerDetails": "Detalles para desarrolladores" + }, + "mapboxVectorTile": { + "name": "Teselas vectoriales de Mapbox (MVT)" + }, + "webFeatureServiceCatalogItem": { + "noLayerFoundMessage": "El conjunto de dato WFS '{{name}}' no tiene capas que coincidan con '{{typeNames}}'. O el catálogo ha sido configurado erróneamente, o el servidor WFS ha cambiado." + }, + "tableData": { + "activeStyle": "Mostrar Variable", + "editStyle": "Editar estilo", + "noData": "No hay datos que mostrar.", + "custom": "Personalizado", + "styleDisabledLabel": "Deshabilitar estilo", + "bulkGeocoderInfo2Message": "{{nullAddresses}} direcciones faltan en el CSV.", + "timeDimensionEnabled": "Opciones temporales activadas (click para deshabilitar)", + "legendNullLabel": "(sin valor)", + "unsupportedCharactersTitle": "Carácteres erróneos en las columnas de latitud y longitud", + "timeDimensionDisabled": "Opciones temporales desactivadas (click para habilitar)", + "unsupportedCharactersMessage": "La pareja {{longitude}} (longitud), {{latitude}} (latitud) no pudo ser interpretada", + "regionMapping": "Mapeo de regiones", + "regionColumn": "Columna de región", + "manualRegionMapping": "Mapeo de regiones manual" + }, + "tableStyling": { + "reset": "Restaurar estilo por defecto", + "name": "Estilo", + "showAdvancedOptions": "Mostrar opciones avanzadas", + "data": { + "name": "Datos", + "selectableDimensions": { + "tableStyle": { + "name": "Estilo" + }, + "tableStyleType": { + "name": "Símbolos", + "options": { + "fill": { + "name": "Color de relleno" + }, + "outline": { + "name": "Color del borde" + } + } + }, + "dataset": { + "name": "Conjunto de datos" + } + } + }, + "fill": { + "selectableDimensions": { + "type": { + "options": { + "divergingContinuous": { + "name": "Divergente (continuo)" + }, + "noStyle": { + "name": "Sin estilo" + }, + "sequentialContinuous": { + "name": "Secuencial (continuo)" + }, + "sequentialDiscrete": { + "name": "Secuencial (discreto)" + }, + "divergingDiscrete": { + "name": "Divergente (concreto)" + }, + "qualitative": { + "name": "Cualitativo" + }, + "customQualitative": { + "name": "Personalizado (cualitativo)" + } + }, + "name": "Tipo", + "undefinedLabel": "Por favor especifique" + }, + "tableColorColumn": { + "name": "Variable" + }, + "dataType": { + "name": "Tipo de columna (avanzada)" + }, + "scheme": { + "name": "Esquema" + }, + "numberOfBins": { + "name": "Número de contenedores" + } + }, + "name": "Color de relleno" + }, + "colors": { + "selectableDimensions": { + "color": { + "name": "Color" + }, + "value": { + "name": "Valor" + }, + "remove": { + "value": "Quitar" + } + }, + "name": "Colores" + }, + "additionalColors": { + "selectableDimensions": { + "nullColor": { + "name": "Color por defecto" + }, + "regionColor": { + "name": "Color región" + }, + "outlierColor": { + "name": "Color valores atípicos" + } + }, + "name": "Colores adicionales" + }, + "style": { + "selectableDimensions": { + "column": { + "name": "Variable" + } + }, + "null": { + "name": "Por defecto" + } + }, + "timeOptions": { + "selectableDimensions": { + "tableTimeColumn": { + "name": "Columna temporal" + }, + "tableEndTimeColumn": { + "name": "Columna de tiempo final" + }, + "tableTimeIdColumns": { + "name": "Columnas de identificador" + }, + "tableTimeIsSampled": { + "name": "Es muestreado", + "options": { + "false": { + "name": "No" + }, + "true": { + "name": "Sí" + } + } + }, + "tableTimeDisplayDuration": { + "name": "Mostrar duración" + }, + "tableTimeSpreadStartTime": { + "options": { + "false": { + "name": "No" + }, + "true": { + "name": "Sí" + } + }, + "name": "Unificar fecha inicial datos" + }, + "tableTimeSpreadFinishTime": { + "options": { + "true": { + "name": "Sí" + }, + "false": { + "name": "No" + } + }, + "name": "Unificar fecha final datos" + } + }, + "name": "Opciones temporales" + }, + "hideAdvancedOptions": "Ocultar opciones avanzadas", + "copyUserStratum": "Copiar configuración en el portapapeles", + "bins": { + "name": "Contenedores", + "selectableDimensions": { + "start": { + "name": "Desde {{value1}} hasta {{value2}}", + "selectableDimensions": { + "color": { + "name": "Color" + }, + "start": { + "name": "Inicio" + }, + "stop": { + "name": "Final" + } + } + } + } + }, + "min": { + "name": "Mín" + }, + "displayRange": { + "name": "Rango de visualización", + "selectableDimensions": { + "max": { + "name": "Máx" + } + } + }, + "legend": { + "name": "Leyenda" + }, + "styleOptions": { + "name": "Opciones de estilo", + "selectableDimensions": { + "styleTitle": { + "name": "Título" + }, + "longitudeColumn": { + "name": "Columna de longitud" + }, + "latitudeColumn": { + "name": "Columna de latitud" + } + } + }, + "outline": { + "name": "Estilo borde", + "selectableDimensions": { + "color": { + "name": "Color" + }, + "width": { + "name": "Ancho" + } + } + }, + "workbenchOptions": { + "name": "Opciones del banco de trabajo", + "selectableDimensions": { + "tableStyleEnalbed": { + "name": "Mostrar estilo en el banco de trabajo", + "options": { + "true": { + "name": "Estilo mostrado" + }, + "false": { + "name": "Estilo oculto" + } + } + }, + "showDisableStyleOption": { + "name": "Mostrar opción para deshabilitar estilo" + }, + "showDisableTimeOption": { + "name": "Mostrar opción para deshabilitar opciones temporales" + }, + "enableManualRegionMapping": { + "name": "Permitir mapeo de regiones manual" + } + } + }, + "variableAndColumn": { + "name": "Variable/columna", + "selectableDimensions": { + "columnTitle": { + "name": "Título" + }, + "columnUnits": { + "name": "Unidades" + } + } + } } }, "analytics": { @@ -193,9 +493,9 @@ "openFeature": "Zoom a", "openFeatureTitle": "Zoom al dato", "colorScaleUpdateRange": "Actualizar Rango", - "zoomToTitle": "Zoom a la Extensión", - "opacity": "Opacidad:{{opacity}}%", - "label": "Conjunto de Datos", + "zoomToTitle": "Zoom a la extensión", + "opacity": "Opacidad: {{opacity}}%", + "label": "Datasets", "rangeMin": "Mínima:", "rangeMax": "Máxima:", "colorScaleRangeMin": "El valor mínimo debe ser un número.", @@ -203,8 +503,8 @@ "diffImageTitle": "Ver las diferencias de imagen entre dos fechas", "diffImage": "Diferencia", "exportDataTitle": "Exportar datos", - "collapseAll": "Colapsar todo", - "expandAll": "Ampliar todo", + "collapseAll": "Colapsar Todo", + "expandAll": "Ampliar Todo", "toggleVisibility": "Mostrar/ocultar datos", "zoomTo": "Zoom ideal", "showMoreActionsTitle": "Mostrar mas acciones", @@ -235,7 +535,7 @@ "viewLess": "Ver menos resultados de {{name}}", "placeholder": "Buscar lugares", "searchCatalogue": "Buscar en el catálogo", - "searchInDataCatalog": "Buscar <1>'{{locationSearchText}}' en el Catálogo de Datos" + "searchInDataCatalog": "Buscar '{{locationSearchText}}'en el Catálogo de Datos" }, "addData": { "back": "Atrás", @@ -276,7 +576,7 @@ "titleIII": "Carga de imágenes históricas", "bodyII": "Es posible que tenga que ajustar el nivel de zoom para que las imágenes por satélite sean visibles en el mapa.", "bodyIV": "Es posible que las imágenes por satélite no estén siempre disponibles en el momento y lugar que usted prefiera. Si sólo quieres ver las imágenes de una ubicación concreta, haz clic en \"filtrar por ubicación\" en tu banco de datos.", - "bodyV": "Puedes aplicar una serie de estilos, como el falso color, a las imágenes de satélite haciendo clic en el menú desplegable de estilos de la mesa de trabajo de datos." + "bodyV": "Puedes aplicar una serie de estilos, como el falso color, a las imágenes de satélite haciendo clic en el menú desplegable de estilos de el banco de trabajo de datos." }, "story": { "story": "Historia", @@ -287,7 +587,7 @@ "cancelEditing": "Cancelar", "placeholder": "Introduzca un título aquí" }, - "removeAllStories": "Eliminar Todos", + "removeAllStories": "Eliminar Todas", "delete": "Eliminar", "deleteStory": "borrar", "edit": "Editar", @@ -299,12 +599,13 @@ "gettingStarted": "Empezar", "gettingStartedTitle": "Empezar", "share": "Compartir", - "collapse": "Colapso", + "collapse": "Colapsar", "badgeBarLabel": "Escenas", "untitledScene": "escena sin título", "doesNotExist": "La historia no existe", - "removeAllStoriesDialog": "¿Está seguro de que desea eliminar la escena {{ count }}?", - "removeAllStoriesDialog_plural": "¿Está seguro de que desea eliminar las escenas {{ count }}?", + "removeAllStoriesDialog_0": "¿Está seguro de que desea eliminar {{ count }} escena?", + "removeAllStoriesDialog_1": "¿Está seguro de que desea eliminar las {{ count }} escenas?", + "removeAllStoriesDialog_2": "", "panelBody": "Crea y comparte historias interactivas directamente desde tu mapa", "recapture": "Recaptura", "recaptureStory": "volver a capturar", @@ -342,7 +643,7 @@ "btnCloseFeature": "Cerrar el panel de dato", "panelHeading": "Información del Elemento", "userSelection": "Selección de usuario", - "clickToAddData": "Pinche'$t(addData.addDataBtnText)' para añadir dato al mapa.", + "clickToAddData": "Pinche '$t(addData.addDataBtnText)' para añadir datos al mapa.", "noDataAvailable": "No hay datos disponibles aquí - pruebe otra localización.", "clickMap": "Pinchar en el mapa para conocer más sobre esta localización", "pickLocation": "Elegir localización", @@ -376,7 +677,7 @@ "location": "Localización", "myLocation": "Mi localización", "browserCannotProvide": "Su navegador no puede proporcionar su localización.", - "originError": "Su navegador solo puede proveer su localización cuando usa HTTPS. Usted posible que usar {{secureUrl}} en su lugar." + "originError": "Su navegador solo puede proveer su localización cuando usa HTTPS. Puede utilizar {{secureUrl}} en su lugar." }, "countDatasets": { "loadError": "No se pudo cargar.", @@ -441,13 +742,13 @@ "balancedPerformance": "Balance entre rendimiento y calidad", "maximumPerformance": "Máximo rendimiento, baja calidad" }, - "mapQuality": "Calidad del Mapa Ráster:", + "mapQuality": "Calidad del Mapa:", "nativeResolutionHeader": "Usar resolución nativa del dispositivo", "btnTitle": "Cambiar vista", "baseMap": "Mapa Base", "timeline": { - "alwaysShowLabel": "Pulse empezar a mostrar la línea de tiempo sólo cuando haya conjuntos de datos que varíen en el tiempo en la mesa de trabajo", - "hideLabel": "Pulse para que se muestre siempre la línea de tiempo, aunque no haya conjuntos de datos que varíen en el tiempo en la mesa de trabajo", + "alwaysShowLabel": "Pulse empezar a mostrar la línea de tiempo sólo cuando haya conjuntos de datos que varíen en el tiempo en el banco de trabajo", + "hideLabel": "Pulse para que se muestre siempre la línea de tiempo, aunque no haya conjuntos de datos que varíen en el tiempo el banco de trabajo", "title": "Línea de tiempo", "alwaysShow": "Siempre mostrar" }, @@ -545,7 +846,8 @@ "selectToPreviewDataset": "Seleccione un conjunto de datos para ver una vista previa", "selectToPreviewSeparator": "O", "goToTheMap": "Vaya al mapa", - "noPreviewAvailable": "NO HAY VISTA PREVIA DISPONIBLE" + "noPreviewAvailable": "NO HAY VISTA PREVIA DISPONIBLE", + "selectMultipleDatasets": "<0>Pulse Mayus y haga click<1 /><2>para añadir múltiples conjuntos de datos" }, "description": { "typeName": "Nombre del tipo", @@ -581,7 +883,7 @@ "preface": { "title": "Hacer el recorrido", "close": "Tal vez más tarde", - "content": "¿No está seguro por dónde comenzar? Haga un recorrido rápido por las funciones principales de nuestro software y aprenda algunas de las funciones clave para ayudarlo en su camino.", + "content": "¿No está seguro por dónde comenzar? Haga un recorrido rápido por las funciones principales de nuestro software y conozca algunas de las funciones clave para ayudarlo en su camino.", "start": "Empezar Tour" }, "locationSearchInput": { @@ -592,7 +894,7 @@ "content": "## Historias\nLas historias permiten añadir información contextual a un conjunto de datos para dar vida a una narración. Cree su propia historia de datos con el Editor de Historias y compártala a través del panel \"Compartir\" una vez que haya terminado." }, "exploreMapDataButton": { - "content": "## Explorar datos del mapa\nBusca en el catálogo datos disponibles y agrega al mapa aquí. Tu puedes agregar múltiples set de datos en cualquier momento, y verás a ellos en el listado abajo" + "content": "## Explore datos del mapa\n\nRecorra el catálogo de datos disponibles y agréguelos al mapa desde aquí. Puede agregar múltiples conjuntos de datos en cualquier momento, y los verá listados debajo en el Tablero." }, "menuBarMapSettingsButton": { "content": "## Configuraciones de mapa\nPersonalice los ajustes de los mapas, como los mapas base, el terreno 2D/3D y la calidad de la imagen del mapa para mejorar el rendimiento." @@ -713,9 +1015,9 @@ "emptyWorkbench": { "helpfulHints": "Consejos útiles", "emptyArea": "Su banco de trabajo está vacío", - "helpfulHintsOne": "Busque los datos disponibles seleccionando \"Explorar datos del mapa\" o haga clic en \"Cargar\" para añadir sus propios datos al mapa.", + "helpfulHintsOne": "Busque los datos disponibles seleccionando \"Explorar los datos del mapa\" o haga clic en \"Subir\" para añadir sus propios datos al mapa.", "helpfulHintsTwo": "Una vez que hayas añadido datos al mapa, tus conjuntos de datos activos aparecerán aquí en tu banco de trabajo. El banco de trabajo le ayudará a interactuar con los datos.", - "helpfulHintsThree": "En la mesa de trabajo puede activar y desactivar conjuntos de datos, cambiar su opacidad, activar la comparación en pantalla dividida, cambiar estilos y navegar por fechas y horas, si los datos admiten esta funcionalidad." + "helpfulHintsThree": "En el banco de trabajo puede activar y desactivar conjuntos de datos, cambiar su opacidad, activar la comparación en pantalla dividida, cambiar estilos y navegar por fechas y horas, si los datos admiten esta funcionalidad." }, "timer": { "nextScheduledUpdateTime": "Próxima actualización de datos a {{scheduledUpdateTime}}", @@ -771,7 +1073,9 @@ "assimp-local": "Convertidor de archivos 3D (zip) (experimental)", "assimp-local-description": "**Atención:** El convertidor de archivos 3D es experimental.\nVer lista de [formatos soportados](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nLos archivos deben estar comprimidos.", "assimp-remote": "Convertidor de archivos 3D (experimental)", - "assimp-remote-description": "**Atención:** El convertidor de archivos 3D es experimental.\nConsulte la lista de [formatos compatibles](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nTambién se admiten archivos Zip" + "assimp-remote-description": "**Atención:** El convertidor de archivos 3D es experimental.\nConsulte la lista de [formatos compatibles](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nTambién se admiten archivos Zip", + "esri-imageServer": "Esri ArcGIS ImageServer (capa única)", + "i3s": "I3S" }, "printWindow": { "errorTitle": "Error de impresión", @@ -822,7 +1126,8 @@ "devError": "cesium es requerido", "failedToObtain": "No se pudo obtener la imagen X: {{x}} Y: {{y}} Nivel: {{level}}.", "notWebMercatorTilingScheme": "Este conjunto de datos no puede mostrarse en el mapa 2D porque no es compatible con la proyección Web Mercator (EPSG:3857).", - "unusalTilingScheme": "Este conjunto de datos no puede mostrarse en el mapa 2D porque no es compatible con la proyección Web Mercator (EPSG:3857)." + "unusalTilingScheme": "Este conjunto de datos no puede mostrarse en el mapa 2D porque no es compatible con la proyección Web Mercator (EPSG:3857).", + "terrainServerErrorMessage": " El servidor de terreno no responde por el momento. Puede seguir utilizando todas las functiones de {{appName}} pero no habrá detalles de terreno en el modo 3D. Lamentamos los inconvenientes ocasionados. Por favor intente nuevamente más tarde, el servidor debería responder correctamente. Si el problema continúa contáctese por correo electrónico a {{supportEmail}}." }, "displayVariablesConcept": { "defaultName": "Visualización de la variable" @@ -866,7 +1171,60 @@ "devError": "No hay contexto para la recoloracion de imágenes" }, "mapboxVectorTileImageryProvider": { - "requireLayerName": "CajademapaVectorTileProveedorimagenes requiere un nombre de capa pasado como options.normbrecapa" + "requireLayerName": "CajademapaVectorTileProveedorimagenes requiere un nombre de capa pasado como options.normbrecapa", + "requireStyles": "MapboxVectorTileImageryProvider requiere una función de estilo como parámetro en options.styleFunc" + }, + "extraCreditLinks": { + "dataAttribution": "Atribución de los datos", + "disclaimer": "Descargo de responsabilidad" + } + }, + "relatedMaps": { + "buttonText": "Mapas relacionados", + "buttonTitle": "Ver mapas relacionados", + "panelHeading": "Mapas relacionados", + "panelText": "Hacer click en el mapa inferior lo abrirá en una ventana o pestaña separadas." + }, + "compare": { + "done": "Hecho" + }, + "selectableDimensions": { + "undefinedLabel": "No especificado", + "colorAdd": "Añadir", + "enabled": "Activado", + "colorRemove": "Quitar", + "disabled": "Deshabilitado" + }, + "deltaTool": { + "titlePrefix": "Detección de cambio", + "generateDeltaBtn": "Generar mapa de diferencia", + "loadingError": { + "title": "Error al cargar la herramienta Delta", + "message": "Se produjo un error al intentar cargar la herramienta Delta desde el servidor. Por favor, inténtelo de nuevo." + }, + "cancelBtn": "Cancelar", + "secondaryImage": "Imagen secundaria", + "selectedLocation": "Lugar Seleccionado", + "description": "Esta herramienta visualiza la diferencia entre las imágenes capturadas en dos puntos discretos en el tiempo.", + "pickLocation": "Para ver las imágenes disponibles, seleccione su ubicación de interés en el mapa opuesto.", + "pickerMessage": { + "pickFirst": "Seleccione un punto haciendo clic en el mapa.", + "pickAnother": "Haga clic en otro punto para cambiar la selección", + "pickError": "¡Error al intentar recuperar las imágenes de la ubicación! Por favor, seleccione un punto de nuevo haciendo clic en el mapa." + }, + "catalogItem": { + "description": "Esta capa visualiza la diferencia entre las imágenes capturadas en dos puntos discretos en el tiempo" + }, + "primaryImage": "Imagen primaria" + }, + "mapNavigation": { + "additionalTools": "Herramientas adicionales" + }, + "pedestrianMode": { + "toolButtonTitle": "Modo Peatón", + "dropPedestrianTooltipMessage": "Click izquierdo para seleccionar
    Click derecho / Esc para cancelar", + "controls": { + "title": "Controles" } } } diff --git a/wwwroot/languages/eu/translation.json b/wwwroot/languages/eu/translation.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/wwwroot/languages/eu/translation.json @@ -0,0 +1 @@ +{} diff --git a/wwwroot/languages/id/translation.json b/wwwroot/languages/id/translation.json index 350a2486504..6312e5bd6f7 100644 --- a/wwwroot/languages/id/translation.json +++ b/wwwroot/languages/id/translation.json @@ -297,7 +297,18 @@ }, "compass": { "guidanceBtnText": "Lihat petunjuk lengkap tentang bagaimana cara menggunakan kompas", - "title": "Klik dan seret untuk memutar kamera" + "title": "Klik dan seret untuk memutar kamera", + "guidance": { + "title": "Kontrol Giroskop", + "innerCircleDescription1": "Klik di bagian tengah dan seret perlahan ke atas, bawah, kiri atau kanan untuk memiringkan dan memutar peta secara bersamaan.", + "innerCircleDescription2": "Klik dua kali di sini untuk mengatur ulang tampilan ke kondisi default.", + "ctrlDragDescription": "Anda juga dapat memiringkan dan memutar peta dengan menahan tombol CTRL dan menyeret peta.", + "outerRingTitle": "Cincin Luar", + "outerRingDescription": "Seret cincin luar dengan gerakan melingkar untuk memutar tampilan peta 360˚.", + "innerCircleTitle": "Lingkaran Dalam" + }, + "description": "Seret cincin luar: memutar tampilan. \nSeret giroskop bagian dalam: membebaskan orbit. \nKlik dua kali: mengatur ulang tampilan. \nTIPS: Anda juga dapat membebaskan orbit dengan menahan tombol CTRL dan menyeret peta.", + "guidanceBtnTitle": "Panduan Giroskop" }, "zoomCotrol": { "zoomOut": "Perkecil", @@ -319,7 +330,8 @@ "duplicateModelErrorMessage": "Terjadi error saat memisahkan item katalog`\"{{name}}\"`. Item katalog ini mungkin tidak mendukung fungsionalitas \"bandingkan\"", "modelNotFoundErrorMessage": "ID Model `\"{{id}}\"` tidak ditemukan", "toggleSplitterTool": "Aktifkan perbandingan berdampingan antara dua set data yang berbeda", - "toggleSplitterToolDisabled": "Harap nonaktifkan fitur perbandingan berdampingan lainnya untuk mengaktifkan perbandingan standar" + "toggleSplitterToolDisabled": "Harap nonaktifkan fitur perbandingan berdampingan lainnya untuk mengaktifkan perbandingan standar", + "title": "Seret ke kiri atau ke kanan untuk menyesuaikan tampilan" }, "location": { "originError": "Browser Anda hanya dapat memberikan lokasi Anda saat menggunakan HTTPS. Anda mungkin dapat menggunakan {{secureUrl}} sebagai gantinya.", diff --git a/wwwroot/test/ArcGisImageServer/rasterFns/imageserver.json b/wwwroot/test/ArcGisImageServer/rasterFns/imageserver.json new file mode 100644 index 00000000000..7cd10b579d2 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/rasterFns/imageserver.json @@ -0,0 +1,300 @@ +{ + "currentVersion": 10.91, + "serviceDescription": "Some service description", + "name": "Some name", + "description": "Some description", + "extent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "initialExtent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "fullExtent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "hasMultidimensions": false, + "pixelSizeX": 10, + "pixelSizeY": 10, + "datasetFormat": "AMD", + "uncompressedSize": 9000000, + "blockWidth": 2048, + "blockHeight": 256, + "compressionType": "None", + "bandNames": ["Some Band Name"], + "allowCopy": true, + "allowAnalysis": true, + "bandCount": 1, + "pixelType": "F32", + "minPixelSize": 0, + "maxPixelSize": 0, + "copyrightText": "Some Copyright", + "serviceDataType": "esriImageServiceDataTypeElevation", + "serviceSourceType": "esriImageServiceSourceTypeMosaicDataset", + "minValues": [515.6699829101562], + "maxValues": [1611.125], + "meanValues": [712.371337411377], + "stdvValues": [45.56883666949917], + "objectIdField": "OBJECTID", + "fields": [ + { + "name": "OBJECTID", + "type": "esriFieldTypeOID", + "alias": "OBJECTID", + "domain": null + }, + { + "name": "Shape", + "type": "esriFieldTypeGeometry", + "alias": "Shape", + "domain": null + }, + { + "name": "Name", + "type": "esriFieldTypeString", + "alias": "Name", + "domain": null, + "length": 50 + }, + { + "name": "MinPS", + "type": "esriFieldTypeDouble", + "alias": "MinPS", + "domain": null + }, + { + "name": "MaxPS", + "type": "esriFieldTypeDouble", + "alias": "MaxPS", + "domain": null + }, + { + "name": "LowPS", + "type": "esriFieldTypeDouble", + "alias": "LowPS", + "domain": null + }, + { + "name": "HighPS", + "type": "esriFieldTypeDouble", + "alias": "HighPS", + "domain": null + }, + { + "name": "Category", + "type": "esriFieldTypeInteger", + "alias": "Category", + "domain": { + "type": "codedValue", + "name": "MosaicCatalogItemCategoryDomain", + "description": "Catalog item categories.", + "codedValues": [ + { + "name": "Unknown", + "code": 0 + }, + { + "name": "Primary", + "code": 1 + }, + { + "name": "Overview", + "code": 2 + }, + { + "name": "Unprocessed Overview", + "code": 3 + }, + { + "name": "Partial Overview", + "code": 4 + }, + { + "name": "Uploaded", + "code": 253 + }, + { + "name": "Incomplete", + "code": 254 + }, + { + "name": "Custom", + "code": 255 + } + ], + "mergePolicy": "esriMPTDefaultValue", + "splitPolicy": "esriSPTDefaultValue" + } + }, + { + "name": "Tag", + "type": "esriFieldTypeString", + "alias": "Tag", + "domain": null, + "length": 20 + }, + { + "name": "GroupName", + "type": "esriFieldTypeString", + "alias": "GroupName", + "domain": null, + "length": 50 + }, + { + "name": "ProductName", + "type": "esriFieldTypeString", + "alias": "ProductName", + "domain": null, + "length": 50 + }, + { + "name": "CenterX", + "type": "esriFieldTypeDouble", + "alias": "CenterX", + "domain": null + }, + { + "name": "CenterY", + "type": "esriFieldTypeDouble", + "alias": "CenterY", + "domain": null + }, + { + "name": "ZOrder", + "type": "esriFieldTypeInteger", + "alias": "ZOrder", + "domain": null + }, + { + "name": "Shape_Length", + "type": "esriFieldTypeDouble", + "alias": "Shape_Length", + "domain": null + }, + { + "name": "Shape_Area", + "type": "esriFieldTypeDouble", + "alias": "Shape_Area", + "domain": null + }, + { + "name": "Version", + "type": "esriFieldTypeString", + "alias": "Version", + "domain": null, + "length": 20 + }, + { + "name": "PointCount", + "type": "esriFieldTypeDouble", + "alias": "Point Count", + "domain": null + }, + { + "name": "PointSpacing", + "type": "esriFieldTypeDouble", + "alias": "Point Spacing", + "domain": null + }, + { + "name": "ZMin", + "type": "esriFieldTypeDouble", + "alias": "ZMin", + "domain": null + }, + { + "name": "ZMax", + "type": "esriFieldTypeDouble", + "alias": "ZMax", + "domain": null + } + ], + "capabilities": "Image,Metadata,Catalog,Mensuration", + "defaultMosaicMethod": "Northwest", + "allowedMosaicMethods": "NorthWest,Center,LockRaster,ByAttribute,Nadir,Viewpoint,Seamline,None", + "sortField": "", + "sortValue": null, + "sortAscending": true, + "mosaicOperator": "First", + "maxDownloadSizeLimit": 0, + "defaultCompressionQuality": 10000, + "defaultResamplingMethod": "Bilinear", + "maxImageHeight": 4100, + "maxImageWidth": 15000, + "maxRecordCount": 1000, + "maxDownloadImageCount": 0, + "maxMosaicImageCount": 20, + "allowRasterFunction": true, + "rasterFunctionInfos": [ + { + "name": "None", + "description": "Make a Raster or Raster Dataset into a Function Raster Dataset.", + "help": "" + }, + { + "name": "RFTAspectColor", + "description": "This function generates a color representation of aspect." + }, + { + "name": "RFTHillshade", + "description": "This function creates a hillshade effect based on the elevation data source.", + "help": "" + }, + { + "name": "RFTShadedReliefElevationColorRamp", + "description": "This function processes the elevation surface as shaded relief. ", + "help": "This raster function template uses a shaded relief function.." + } + ], + "rasterTypeInfos": [ + { + "name": "Raster Dataset", + "description": "Supports all ArcGIS Raster Datasets", + "help": "" + } + ], + "mensurationCapabilities": "Basic", + "hasHistograms": true, + "hasColormap": false, + "hasRasterAttributeTable": false, + "minScale": 0, + "maxScale": 0, + "exportTilesAllowed": false, + "supportsStatistics": true, + "supportsAdvancedQueries": true, + "editFieldsInfo": null, + "ownershipBasedAccessControlForRasters": null, + "allowComputeTiePoints": false, + "useStandardizedQueries": true, + "advancedQueryCapabilities": { + "useStandardizedQueries": true, + "supportsStatistics": true, + "supportsOrderBy": true, + "supportsDistinct": true, + "supportsPagination": true + }, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } +} diff --git a/wwwroot/test/ArcGisImageServer/rasterFns/legend.json b/wwwroot/test/ArcGisImageServer/rasterFns/legend.json new file mode 100644 index 00000000000..e293b958014 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/rasterFns/legend.json @@ -0,0 +1,38 @@ +{ + "layers": [ + { + "layerId": 0, + "layerName": "Layer Name", + "layerType": "Raster Layer", + "minScale": 0, + "maxScale": 0, + "legendType": "Stretched", + "legend": [ + { + "label": "High : Some value", + "url": "a758ed6a2eb93b3f835ab3a17f21dfda", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAT0lEQVQ4je3RwQkAMAgDwAjiz/1XVUTtEE1/zQCHibK7C1JmRjQiWB7MDJqZXJB5obtzQQDQquKCzA0BPKhMB+mV6U/5G96Df8PrSHdTwQOUlT8HeNXIpAAAAABJRU5ErkJggg==", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "Some label", + "url": "b0b4583afdd50894cc0738805356a037", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAUElEQVQ4je3UwQkAMQhE0S+kbcGyjSK7RWQOOWQKeMgMaO7+IUpE2MpMlQfA2ntrQfmF3a0FX4fn4P0dVpUWvH/l1+FxDJB97JkxmxmVB8APVtY8xR14rRcAAAAASUVORK5CYII=", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "Low : Some value", + "url": "92a46f91e372e90744ce1a7d03335211", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAIUlEQVQ4jWP8+/cvAzUBE1VNGzVw1MBRA0cNHDVwCBkIAEMfAx/78kANAAAAAElFTkSuQmCC", + "contentType": "image/png", + "height": 20, + "width": 20 + } + ] + } + ] +} diff --git a/wwwroot/test/ArcGisImageServer/tile/identify.json b/wwwroot/test/ArcGisImageServer/tile/identify.json new file mode 100644 index 00000000000..611d2522202 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/tile/identify.json @@ -0,0 +1,13 @@ +{ + "objectId": 0, + "name": "Pixel", + "value": "178, 135, 99, 255", + "location": { + "x": 15865828.500025423, + "y": -2553895.2233379674, + "spatialReference": { "wkid": 102100, "latestWkid": 3857 } + }, + "properties": null, + "catalogItems": null, + "catalogItemVisibilities": [] +} diff --git a/wwwroot/test/ArcGisImageServer/tile/imageserver.json b/wwwroot/test/ArcGisImageServer/tile/imageserver.json new file mode 100644 index 00000000000..6ebccccfb43 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/tile/imageserver.json @@ -0,0 +1,220 @@ +{ + "currentVersion": 10.91, + "serviceDescription": "Some service description.", + "name": "Some name", + "description": "Some description", + "extent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "initialExtent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "fullExtent": { + "xmin": -1.3873971544712003e7, + "ymin": 5151008.633437126, + "xmax": -1.2960958413099812e7, + "ymax": 5831188.605321072, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } + }, + "pixelSizeX": 0.298582141647617, + "pixelSizeY": 0.2985821416476171, + "bandCount": 3, + "pixelType": "U8", + "minPixelSize": 0.29858214164761643, + "maxPixelSize": 1222.9924525624938, + "copyrightText": "Some copyright", + "serviceDataType": "esriImageServiceDataTypeRGB", + "minValues": [], + "maxValues": [], + "meanValues": [], + "stdvValues": [], + "objectIdField": "", + "fields": [], + "capabilities": "Mensuration,Image,Metadata", + "defaultMosaicMethod": "Center", + "allowedMosaicMethods": "", + "sortField": "", + "sortValue": null, + "sortAscending": true, + "mosaicOperator": "First", + "defaultCompressionQuality": 75, + "defaultResamplingMethod": "Bilinear", + "maxImageHeight": 4100, + "maxImageWidth": 15000, + "singleFusedMapCache": true, + "tileInfo": { + "rows": 256, + "cols": 256, + "dpi": 96, + "format": "MIXED", + "compressionQuality": 75, + "origin": { + "x": -2.0037508342787e7, + "y": 2.0037508342787e7 + }, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + }, + "lods": [ + { + "level": 0, + "resolution": 156543.033928, + "scale": 5.91657527591555e8 + }, + { + "level": 1, + "resolution": 78271.5169639999, + "scale": 2.95828763795777e8 + }, + { + "level": 2, + "resolution": 39135.7584820001, + "scale": 1.47914381897889e8 + }, + { + "level": 3, + "resolution": 19567.8792409999, + "scale": 7.3957190948944e7 + }, + { + "level": 4, + "resolution": 9783.93962049996, + "scale": 3.6978595474472e7 + }, + { + "level": 5, + "resolution": 4891.96981024998, + "scale": 1.8489297737236e7 + }, + { + "level": 6, + "resolution": 2445.98490512499, + "scale": 9244648.868618 + }, + { + "level": 7, + "resolution": 1222.99245256249, + "scale": 4622324.434309 + }, + { + "level": 8, + "resolution": 611.49622628138, + "scale": 2311162.217155 + }, + { + "level": 9, + "resolution": 305.748113140558, + "scale": 1155581.108577 + }, + { + "level": 10, + "resolution": 152.874056570411, + "scale": 577790.554289 + }, + { + "level": 11, + "resolution": 76.4370282850732, + "scale": 288895.277144 + }, + { + "level": 12, + "resolution": 38.2185141425366, + "scale": 144447.638572 + }, + { + "level": 13, + "resolution": 19.1092570712683, + "scale": 72223.819286 + }, + { + "level": 14, + "resolution": 9.55462853563415, + "scale": 36111.909643 + }, + { + "level": 15, + "resolution": 4.77731426794937, + "scale": 18055.954822 + }, + { + "level": 16, + "resolution": 2.38865713397468, + "scale": 9027.977411 + }, + { + "level": 17, + "resolution": 1.19432856685505, + "scale": 4513.988705 + }, + { + "level": 18, + "resolution": 0.597164283559817, + "scale": 2256.994353 + }, + { + "level": 19, + "resolution": 0.298582141647617, + "scale": 1128.497176 + } + ] + }, + "cacheType": "Map", + "allowRasterFunction": true, + "rasterFunctionInfos": [ + { + "name": "None", + "description": "", + "help": "" + } + ], + "rasterTypeInfos": [ + { + "name": "Raster Dataset", + "description": "Supports all ArcGIS Raster Datasets", + "help": "" + } + ], + "mensurationCapabilities": "Basic", + "hasHistograms": false, + "hasColormap": false, + "hasRasterAttributeTable": false, + "minScale": 4622324.434309, + "maxScale": 1128.497176, + "exportTilesAllowed": false, + "hasMultidimensions": false, + "supportsStatistics": false, + "supportsAdvancedQueries": false, + "editFieldsInfo": null, + "ownershipBasedAccessControlForRasters": null, + "allowComputeTiePoints": false, + "useStandardizedQueries": true, + "advancedQueryCapabilities": { + "useStandardizedQueries": true, + "supportsStatistics": false, + "supportsOrderBy": false, + "supportsDistinct": false, + "supportsPagination": false + }, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } +} diff --git a/wwwroot/test/ArcGisImageServer/tile/legend.json b/wwwroot/test/ArcGisImageServer/tile/legend.json new file mode 100644 index 00000000000..d3f4a619a2f --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/tile/legend.json @@ -0,0 +1,38 @@ +{ + "layers": [ + { + "layerId": 0, + "layerName": "Some tiled layer", + "layerType": "Raster Layer", + "minScale": 0, + "maxScale": 0, + "legendType": "RGB Composite", + "legend": [ + { + "label": "Red: Band_1", + "url": "2929cfae2efac980ef6c9ea09223463e", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAANUlEQVQ4jWPMy8v7z0BFwMLAwMAwcdIkqhiWn5fHwEQVk5DAqIGjBo4aOGrgqIEQwEjtKgAATl0Hu6JrzFUAAAAASUVORK5CYII=", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "Green: Band_2", + "url": "49f1c6a1be24c5484bbf3e629796273e", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAANUlEQVQ4jWPMy8v7z0BFwMLAwMAwaeIkqhiWl5/HwEQVk5DAqIGjBo4aOGrgqIEQwEjtKgAATl0Hu6sKxboAAAAASUVORK5CYII=", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "Blue: Band_3", + "url": "ea7072b100ba7bef3760e98b91c4313f", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAANUlEQVQ4jWPMy8v7z0BFwMLAwMAwadJEqhiWl5fPwEQVk5DAqIGjBo4aOGrgqIEQwEjtKgAATl0Hu75+IUcAAAAASUVORK5CYII=", + "contentType": "image/png", + "height": 20, + "width": 20 + } + ] + } + ] +} diff --git a/wwwroot/test/ArcGisImageServer/time/identify.json b/wwwroot/test/ArcGisImageServer/time/identify.json new file mode 100644 index 00000000000..560bd9f07ac --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/time/identify.json @@ -0,0 +1,13 @@ +{ + "objectId": 0, + "name": "Pixel", + "value": "8", + "location": { + "x": 128.2904493061971, + "y": -18.296866690954595, + "spatialReference": { "wkid": 4326, "latestWkid": 4326 } + }, + "properties": null, + "catalogItems": null, + "catalogItemVisibilities": [] +} diff --git a/wwwroot/test/ArcGisImageServer/time/imageserver.json b/wwwroot/test/ArcGisImageServer/time/imageserver.json new file mode 100644 index 00000000000..10f7f0c86b0 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/time/imageserver.json @@ -0,0 +1,121 @@ +{ + "currentVersion": 10.91, + "serviceDescription": "Some service description.", + "name": "Some name", + "description": "Some description", + "extent": { + "xmin": -180.3125, + "ymin": -90.25, + "xmax": 179.6875, + "ymax": 90.25, + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + } + }, + "initialExtent": { + "xmin": -180.3125, + "ymin": -90.25, + "xmax": 179.6875, + "ymax": 90.25, + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + } + }, + "fullExtent": { + "xmin": -180.3125, + "ymin": -90.25, + "xmax": 179.6875, + "ymax": 90.25, + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + } + }, + "hasMultidimensions": true, + "timeInfo": { + "startTimeField": "StdTime", + "endTimeField": "StdTime", + "timeExtent": [378604800000, 1640882880000], + "timeReference": null, + "defaultTimeInterval": 365.2425, + "defaultTimeIntervalUnits": "esriTimeUnitsDays" + }, + "pixelSizeX": 0.5, + "pixelSizeY": 0.5, + "datasetFormat": "Cache/LERC2D", + "uncompressedSize": 2079360, + "blockWidth": 256, + "blockHeight": 256, + "compressionType": "LERC2D", + "bandNames": ["Band_1"], + "allowCopy": true, + "allowAnalysis": true, + "defaultVariable": "", + "bandCount": 1, + "noDataValue": -999, + "noDataValues": [-999], + "pixelType": "F64", + "minPixelSize": 0, + "maxPixelSize": 0, + "copyrightText": "Some copyright", + "serviceDataType": "esriImageServiceDataTypeScientific", + "serviceSourceType": "esriImageServiceSourceTypeDataset", + "minValues": [0], + "maxValues": [8972.0625], + "meanValues": [2224.8882386419177], + "stdvValues": [2508.1968623077223], + "objectIdField": "", + "fields": [], + "capabilities": "Mensuration,Image,Metadata", + "defaultMosaicMethod": "Center", + "allowedMosaicMethods": "", + "sortField": "", + "sortValue": null, + "sortAscending": true, + "mosaicOperator": "First", + "defaultCompressionQuality": 75, + "defaultResamplingMethod": "Bilinear", + "maxImageHeight": 4100, + "maxImageWidth": 15000, + "allowRasterFunction": true, + "rasterFunctionInfos": [ + { + "name": "None", + "description": "", + "help": "" + } + ], + "rasterTypeInfos": [ + { + "name": "Raster Dataset", + "description": "Supports all ArcGIS Raster Datasets", + "help": "" + } + ], + "mensurationCapabilities": "Basic", + "hasHistograms": true, + "hasColormap": false, + "hasRasterAttributeTable": false, + "minScale": 0, + "maxScale": 0, + "exportTilesAllowed": false, + "supportsStatistics": false, + "supportsAdvancedQueries": false, + "editFieldsInfo": null, + "ownershipBasedAccessControlForRasters": null, + "allowComputeTiePoints": false, + "useStandardizedQueries": true, + "advancedQueryCapabilities": { + "useStandardizedQueries": true, + "supportsStatistics": false, + "supportsOrderBy": false, + "supportsDistinct": false, + "supportsPagination": false + }, + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + } +} diff --git a/wwwroot/test/ArcGisImageServer/time/legend.json b/wwwroot/test/ArcGisImageServer/time/legend.json new file mode 100644 index 00000000000..ed6654e0c05 --- /dev/null +++ b/wwwroot/test/ArcGisImageServer/time/legend.json @@ -0,0 +1,38 @@ +{ + "layers": [ + { + "layerId": 0, + "layerName": "Some time layer", + "layerType": "Raster Layer", + "minScale": 0, + "maxScale": 0, + "legendType": "Stretched", + "legend": [ + { + "label": "High : Some Value", + "url": "9632f29a708d14f7ffa6950657169300", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAeElEQVQ4ja3RsQ3CQBQE0TlYhAiQ8B04owD334q7gAACJ0jno4n5BTxp/paxXAfS7eunhHqwPADCfJTBm+oRHnbyZCc31SPMJxls+sr2KM1OrnLydvaSLzvk3asGPgvkxeSBQL7cNRAgG14yQH42OMQfApTeuwr+AYLYEoMMYRRCAAAAAElFTkSuQmCC", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "", + "url": "993dab15d066b40b3591f48bd77d5666", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAh0lEQVQ4jbXTsQ3CQBSD4d/KEx26VCg1M7EBW2UDVkpBdZBQIEikSBR3gSWMB/ga27qU/ospJ50VA0+XB0DcaLzgnb0XHDl4wczRCopSbC1XSZEeLg7oINLoBicjCET7MoNpMYPt6nzKRiy5GkER89XoAfHJZnD33oxcQ6TZ6PGHHapWZ8vwA8yIImKpipDXAAAAAElFTkSuQmCC", + "contentType": "image/png", + "height": 20, + "width": 20 + }, + { + "label": "Low : Some Value", + "url": "92a46f91e372e90744ce1a7d03335211", + "imageData": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAAAAAAAAAHqZRakAAAAIUlEQVQ4jWP8+/cvAzUBE1VNGzVw1MBRA0cNHDVwCBkIAEMfAx/78kANAAAAAElFTkSuQmCC", + "contentType": "image/png", + "height": 20, + "width": 20 + } + ] + } + ] +} diff --git a/wwwroot/test/cogs/32756.tif b/wwwroot/test/cogs/32756.tif new file mode 100644 index 00000000000..776e0d7429f Binary files /dev/null and b/wwwroot/test/cogs/32756.tif differ diff --git a/wwwroot/test/cogs/4326.tif b/wwwroot/test/cogs/4326.tif new file mode 100644 index 00000000000..c3f9bc9c3af Binary files /dev/null and b/wwwroot/test/cogs/4326.tif differ diff --git a/yarn.lock b/yarn.lock index 1a1c86baef3..a5f755636ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1640,17 +1640,10 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac" integrity sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ== -"@types/d3-array@^2.0.0": - version "2.12.3" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.3.tgz#8d16d51fb04ad5a5a8ebe14eb8263a579f1efdd1" - integrity sha512-hN879HLPTVqZV3FQEXy7ptt083UXwguNbnxdTGzVW4y4KjX5uyNKljrQixZcSJfLyFirbpUokxpXtvR+N5+KIg== - -"@types/d3-axis@^1.0.12": - version "1.0.16" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.16.tgz#93d7a28795c2f8b0e2fd550fcc4d29b7f174e693" - integrity sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ== - dependencies: - "@types/d3-selection" "^1" +"@types/d3-array@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== "@types/d3-color@*": version "3.1.3" @@ -1696,10 +1689,10 @@ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== -"@types/d3-scale-chromatic@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-2.0.1.tgz#495cbbae7273e0d0ff564cdc19aa6d2b9928da83" - integrity sha512-3EuZlbPu+pvclZcb1DhlymTWT2W+lYsRKBjvkH2ojDbCWDYavifqu1vYX9WGzlPgCgcS4Alhk1+zapXbGEGylQ== +"@types/d3-scale-chromatic@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" + integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== "@types/d3-scale@4.0.2": version "4.0.2" @@ -1708,10 +1701,10 @@ dependencies: "@types/d3-time" "*" -"@types/d3-selection@^1", "@types/d3-selection@^1.4.1": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.3.tgz#36928bbe64eb8e0bbcbaa01fb05c21ff6c71fa93" - integrity sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA== +"@types/d3-selection@*", "@types/d3-selection@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe" + integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg== "@types/d3-shape@^1.3.1": version "1.3.8" @@ -1735,12 +1728,12 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== -"@types/d3-transition@^1.1.4": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.3.2.tgz#ed59beca7b4d679cfa52f88a6a50e5bbeb7e0a3c" - integrity sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg== +"@types/d3-transition@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f" + integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ== dependencies: - "@types/d3-selection" "^1" + "@types/d3-selection" "*" "@types/dateformat@^3.0.1": version "3.0.1" @@ -1807,11 +1800,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/json5@^0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" - integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== - "@types/leaflet@^1.7.10": version "1.7.10" resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.10.tgz#092f97af29bb870b7d1ed72d516d4b3dde66a6c8" @@ -1819,6 +1807,16 @@ dependencies: "@types/geojson" "*" +"@types/linkify-it@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8" + integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw== + +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + "@types/lodash-es@^4.17.3": version "4.17.5" resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.5.tgz#1c3fdd16849d84aea43890b1c60da379fb501353" @@ -1831,11 +1829,24 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== +"@types/markdown-it@^14.0.1": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + "@types/math-expression-evaluator@^1.2.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/math-expression-evaluator/-/math-expression-evaluator-1.2.2.tgz#05970d669d8c27c43f97d1557fa2a67e79698100" integrity sha512-eHy3f0C2mfFdHbK2zoKRXaM830dm2pSTUBWZ3aJCWxFsOxErWsXNPziFLliqBddGYreCM4Nyl1IZwFCt0jwRNg== +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1881,6 +1892,11 @@ resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== +"@types/proj4@^2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@types/proj4/-/proj4-2.5.5.tgz#044d53782dc75f20335577ca3af2643962a56980" + integrity sha512-y4tHUVVoMEOm2nxRLQ2/ET8upj/pBmoutGxFw2LZJTQWPgWXI+cbxVEUFFmIzr/bpFR83hGDOTSXX6HBeObvZA== + "@types/prop-types@*": version "15.7.4" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" @@ -1891,21 +1907,6 @@ resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg== -"@types/rc-slider@^8.6.6": - version "8.6.6" - resolved "https://registry.yarnpkg.com/@types/rc-slider/-/rc-slider-8.6.6.tgz#961ccfd0b8c632a9d8cdcc28b54d6be4781c419f" - integrity sha512-2Q3vwKrSm3PbgiMNwzxMkOaMtcAGi0xQ8WPeVKoabk1vNYHiVR44DMC3mr9jC2lhbxCBgGBJWF9sBhmnSDQ8Bg== - dependencies: - "@types/rc-tooltip" "*" - "@types/react" "*" - -"@types/rc-tooltip@*": - version "3.7.6" - resolved "https://registry.yarnpkg.com/@types/rc-tooltip/-/rc-tooltip-3.7.6.tgz#d3833b3f3e494ab14e4a95158f9f6b321042c97a" - integrity sha512-Otouf6HW49RSwtTa3EBdp0+BZoOQy3KDTEcVgLdZEgcYp4HB5nP6N3S6bTUkPtq3H5KmYBzEOaiq0SNMB/MY2g== - dependencies: - "@types/react" "*" - "@types/react-color@^3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" @@ -2451,6 +2452,11 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@xmldom/xmldom@^0.8.10": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2715,6 +2721,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -2924,11 +2935,6 @@ async@~0.2.10: resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= -async@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-1.2.1.tgz#a4816a17cd5ff516dfa2c7698a453369b9790de0" - integrity sha1-pIFqF81f9RbfosdpikUzabl5DeA= - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -3637,21 +3643,6 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1, chokidar@^3.4.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chokidar@^2.0.0, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3671,6 +3662,21 @@ chokidar@^2.0.0, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.1, chokidar@^3.4.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^3.5.1: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -3686,6 +3692,13 @@ chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -3962,11 +3975,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-polyfill@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/console-polyfill/-/console-polyfill-0.3.0.tgz#84900902a18c47a5eba932be75fa44d23e8af861" - integrity sha512-w+JSDZS7XML43Xnwo2x5O5vxB0ID7T5BdqDtyqT6uiCAX2kZAgcWxNaGqT97tZfSHzfOcvrfsDAodKcJ3UvnXQ== - constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -4128,7 +4136,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.5.2: +create-react-class@^15.5.2, create-react-class@^15.7.0: version "15.7.0" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== @@ -4291,7 +4299,7 @@ custom-event@~1.0.0: resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= -"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3": +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== @@ -4305,32 +4313,7 @@ d3-array@3.2.1: dependencies: internmap "1 - 2" -d3-array@^1.0.0, d3-array@^1.2.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" - integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== - -d3-axis@^1.0.0: - version "1.0.12" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" - integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== - -d3-collection@1, d3-collection@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" - integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== - -d3-color@1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" - integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== - -"d3-color@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" - integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== - -"d3-color@1 - 3", d3-color@3.1.0, d3-color@^3.0.1: +"d3-color@1 - 3", d3-color@3.1.0, d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== @@ -4342,28 +4325,23 @@ d3-delaunay@6.0.2: dependencies: delaunator "5" -d3-dispatch@1, d3-dispatch@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58" - integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA== +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== -d3-drag@1: - version "1.2.5" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70" - integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w== +"d3-drag@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== dependencies: - d3-dispatch "1" - d3-selection "1" + d3-dispatch "1 - 3" + d3-selection "3" -d3-ease@1, d3-ease@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" - integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== - -d3-format@1: - version "1.4.5" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" - integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== +"d3-ease@1 - 3", d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== "d3-format@1 - 3", d3-format@3.1.0: version "3.1.0" @@ -4377,21 +4355,7 @@ d3-geo@3.1.0: dependencies: d3-array "2.5.0 - 3" -d3-interpolate@1, d3-interpolate@^1.3.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" - integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== - dependencies: - d3-color "1" - -"d3-interpolate@1 - 2": - version "2.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" - integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== - dependencies: - d3-color "1 - 2" - -"d3-interpolate@1.2.0 - 3", d3-interpolate@3.0.1: +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3.0.1, d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -4403,13 +4367,18 @@ d3-path@1, d3-path@^1.0.5: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== -d3-scale-chromatic@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab" - integrity sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA== +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale-chromatic@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314" + integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== dependencies: - d3-color "1 - 2" - d3-interpolate "1 - 2" + d3-color "1 - 3" + d3-interpolate "1 - 3" d3-scale@4.0.2: version "4.0.2" @@ -4422,36 +4391,24 @@ d3-scale@4.0.2: d3-time "2.1.1 - 3" d3-time-format "2 - 4" -d3-scale@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" - integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== - dependencies: - d3-array "^1.2.0" - d3-collection "1" - d3-format "1" - d3-interpolate "1" - d3-time "1" - d3-time-format "2" - -d3-selection@1, d3-selection@^1.0.0, d3-selection@^1.1.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c" - integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg== +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== -d3-shape@^1.0.0, d3-shape@^1.0.6, d3-shape@^1.2.0: +d3-shape@^1.0.6, d3-shape@^1.2.0: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== dependencies: d3-path "1" -d3-time-format@2: - version "2.3.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850" - integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ== +d3-shape@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== dependencies: - d3-time "1" + d3-path "^3.1.0" "d3-time-format@2 - 4", d3-time-format@4.1.0: version "4.1.0" @@ -4460,11 +4417,6 @@ d3-time-format@2: dependencies: d3-time "1 - 3" -d3-time@1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" - integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== - "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" @@ -4472,33 +4424,32 @@ d3-time@1: dependencies: d3-array "2 - 3" -d3-timer@1: - version "1.0.10" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" - integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== -d3-transition@1, d3-transition@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" - integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== - dependencies: - d3-color "1" - d3-dispatch "1" - d3-ease "1" - d3-interpolate "1" - d3-selection "^1.1.0" - d3-timer "1" - -d3-zoom@^1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a" - integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ== - dependencies: - d3-dispatch "1" - d3-drag "1" - d3-interpolate "1" - d3-selection "1" - d3-transition "1" +"d3-transition@2 - 3", d3-transition@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" d@1, d@^1.0.1: version "1.0.1" @@ -4560,13 +4511,6 @@ debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: dependencies: ms "^2.1.1" -decache@^3.0.5: - version "3.1.0" - resolved "https://registry.yarnpkg.com/decache/-/decache-3.1.0.tgz#4f5036fbd6581fcc97237ac3954a244b9536c2da" - integrity sha1-T1A2+9ZYH8yXI3rDlUokS5U2wto= - dependencies: - find "^0.2.4" - decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -5073,10 +5017,10 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== -entities@~2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== errno@^0.1.3: version "0.1.8" @@ -5092,13 +5036,6 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-stack-parser@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8" - integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ== - dependencies: - stackframe "^1.1.1" - es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -5606,12 +5543,12 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-xml-parser@^3.14.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736" - integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg== +fast-xml-parser@^4.3.6: + version "4.4.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" + integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== dependencies: - strnum "^1.0.4" + strnum "^1.0.5" fastq@^1.6.0: version "1.13.0" @@ -5757,13 +5694,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find@^0.2.4: - version "0.2.9" - resolved "https://registry.yarnpkg.com/find/-/find-0.2.9.tgz#4b73f1ff9e56ad91b76e716407fe5ffe6554bb8c" - integrity sha1-S3Px/55WrZG3bnFkB/5f/mVUu4w= - dependencies: - traverse-chain "~0.1.0" - findup-sync@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" @@ -7293,11 +7223,6 @@ is-wsl@^2.1.0, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -is_js@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" - integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0= - isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -7579,7 +7504,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: +json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= @@ -7902,19 +7827,12 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" - integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== - dependencies: - uc.micro "^1.0.1" - -linkify-it@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" - integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== dependencies: - uc.micro "^1.0.1" + uc.micro "^2.0.0" load-json-file@^1.0.0: version "1.1.0" @@ -8083,11 +8001,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@~2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" - integrity sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0= - make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -8132,16 +8045,17 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it@^11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6" - integrity sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ== +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" matchdep@^2.0.0: version "2.0.0" @@ -8179,10 +8093,10 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== media-typer@0.3.0: version "0.3.0" @@ -9632,6 +9546,11 @@ pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -10073,6 +9992,11 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.1.tgz#b2fe35f8dca63183cd3b86883ecc8f720ea96ae6" + integrity sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -10230,13 +10154,6 @@ replace-homedir@^1.0.0: is-absolute "^1.0.0" remove-trailing-separator "^1.1.0" -request-ip@~2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.0.2.tgz#deeae6d4af21768497db8cd05fa37143f8f1257e" - integrity sha1-3urm1K8hdoSX24zQX6NxQ/jxJX4= - dependencies: - is_js "^0.9.0" - request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -10456,22 +10373,6 @@ robust-predicates@^3.0.2: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollbar@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/rollbar/-/rollbar-2.24.0.tgz#3e0986ee6600d06d3abd4d85f471bcbc11fafc99" - integrity sha512-cjAGDeTOUH5Bc4qzXlrp2D3eTO51adMWMxzOrr079t/nZidrO8ISBFM8SnazJVv5fOjIX5VbB/4a+gwbn9l4rw== - dependencies: - async "~1.2.1" - console-polyfill "0.3.0" - error-stack-parser "^2.0.4" - json-stringify-safe "~5.0.0" - lru-cache "~2.2.1" - request-ip "~2.0.1" - source-map "^0.5.7" - uuid "3.0.x" - optionalDependencies: - decache "^3.0.5" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -10512,12 +10413,12 @@ sass-loader@^10: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.66.1: - version "1.69.7" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.7.tgz#6e7e1c8f51e8162faec3e9619babc7da780af3b7" - integrity sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ== +sass@^1.79.1: + version "1.79.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.4.tgz#f9c45af35fbeb53d2c386850ec842098d9935267" + integrity sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg== dependencies: - chokidar ">=3.0.0 <4.0.0" + chokidar "^4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" @@ -11044,11 +10945,6 @@ stack-trace@0.0.10: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= -stackframe@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" - integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== - static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -11267,7 +11163,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^1.0.4: +strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== @@ -11460,10 +11356,10 @@ terriajs-cesium-widgets@5.0.0: nosleep.js "^0.12.0" terriajs-cesium "^8.0.0" -terriajs-cesium@8.0.0, terriajs-cesium@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/terriajs-cesium/-/terriajs-cesium-8.0.0.tgz#7f83b5fa12014c2803f8e2513be3f1779913dd99" - integrity sha512-YpNZh1fmBaRuyVQP1R6/0C5CfwwuVAGvJGgXSD1rSskjpynQ3MmRNi+YfqzsHxkkyUBEOtLFnzPxRYDAy/Id0w== +terriajs-cesium@8.0.1, terriajs-cesium@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/terriajs-cesium/-/terriajs-cesium-8.0.1.tgz#2f7fb10d8f2d54258022a650d19fb1afe1a3a7d7" + integrity sha512-99sEp1DKC2g67QwRueE+EZxbOrI9nAw+LP5AsGBtJqr6cD+20Whkx3kMDweE4tjeBMWPjDMZbsbFbILbbSqqVw== dependencies: "@tweenjs/tween.js" "^23.1.1" "@zip.js/zip.js" "^2.7.34" @@ -11549,13 +11445,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -thredds-catalog-crawler@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/thredds-catalog-crawler/-/thredds-catalog-crawler-0.0.6.tgz#5c7bf00dfa1846f35e3b5835f81b70639966487f" - integrity sha512-xyxSTSTOnx3Fem+k8mgIOJYI6LXJvBK4IvrNDuNEluzvgubdEJ5x/Bk3GGrcXXy8HlD09An4Ha/nCDT2wdXSiA== +thredds-catalog-crawler@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/thredds-catalog-crawler/-/thredds-catalog-crawler-0.0.7.tgz#67b06d5b279f8ef798cd20cb9443b05cda2388a9" + integrity sha512-B9TmJ3M4nGKQ8vqoFnWaLE7le8KP8Fygne4jwybF2h8fCPftbe8BK49X3Nru380voMfl4JdBSjI3zNRdkckPyQ== dependencies: - fast-xml-parser "^3.14.0" - xmldom "^0.5.0" + "@xmldom/xmldom" "^0.8.10" + fast-xml-parser "^4.3.6" through2-filter@^3.0.0: version "3.0.0" @@ -11737,11 +11633,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -traverse-chain@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" - integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= - traverse@^0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" @@ -11871,10 +11762,10 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== unbox-primitive@^1.0.1: version "1.0.1" @@ -12117,11 +12008,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.0.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" - integrity sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE= - uuid@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" @@ -12624,11 +12510,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" - integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA== - xmldom@~0.1.19: version "0.1.31" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
    ${col.title}{{${col.name}}}