UNPKG

terriajs

Version:

Geospatial data visualization platform.

297 lines (258 loc) 8.69 kB
import { featureCollection } from "@turf/helpers"; import { Geometry, GeometryCollection } from "geojson"; import i18next from "i18next"; import { computed, makeObservable, observable, runInAction } from "mobx"; import RequestErrorEvent from "terriajs-cesium/Source/Core/RequestErrorEvent"; import URI from "urijs"; import { toFeatureCollection } from "../../../Core/GeoJson"; import JsonValue, { isJsonNumber, isJsonObject, isJsonString, isJsonStringArray, JsonObject } from "../../../Core/Json"; import loadJson from "../../../Core/loadJson"; import Result from "../../../Core/Result"; import TerriaError, { networkRequestError } from "../../../Core/TerriaError"; import GeoJsonMixin from "../../../ModelMixins/GeojsonMixin"; import CartoMapV3CatalogItemTraits from "../../../Traits/TraitsClasses/CartoMapV3CatalogItemTraits"; import { GeoJsonTraits } from "../../../Traits/TraitsClasses/GeoJsonTraits"; import TableStyleTraits from "../../../Traits/TraitsClasses/Table/StyleTraits"; import CreateModel from "../../Definition/CreateModel"; import createStratumInstance from "../../Definition/createStratumInstance"; import LoadableStratum from "../../Definition/LoadableStratum"; import { BaseModel } from "../../Definition/Model"; import StratumOrder from "../../Definition/StratumOrder"; import Terria from "../../Terria"; class CartoMapV3Stratum extends LoadableStratum(GeoJsonTraits) { static stratumName = "cartoMapV3Stratum"; constructor(readonly catalogItem: CartoMapV3CatalogItem) { super(); makeObservable(this); } static load(item: CartoMapV3CatalogItem) { return new CartoMapV3Stratum(item); } duplicateLoadableStratum(newModel: BaseModel): this { return new CartoMapV3Stratum(newModel as CartoMapV3CatalogItem) as this; } // Hide "cartodb_id" style @computed get styles() { return [ createStratumInstance(TableStyleTraits, { id: "cartodb_id", hidden: true }) ]; } } StratumOrder.addLoadStratum(CartoMapV3Stratum.stratumName); export default class CartoMapV3CatalogItem extends GeoJsonMixin( CreateModel(CartoMapV3CatalogItemTraits) ) { static readonly type = "carto-v3"; @observable geoJsonUrls: string[] = []; // Commented out as we don't support tileJSON yet // @observable // mvtTileJsonUrls: string[] = []; @observable geoJsonSize: number | undefined; constructor( id: string | undefined, terria: Terria, sourceReference?: BaseModel | undefined ) { super(id, terria, sourceReference); makeObservable(this); if (this.strata.get(CartoMapV3Stratum.stratumName) === undefined) { runInAction(() => { this.strata.set( CartoMapV3Stratum.stratumName, new CartoMapV3Stratum(this) ); }); } } get type() { return CartoMapV3CatalogItem.type; } get typeName() { return i18next.t("models.carto-v3.name"); } protected async forceLoadMetadata() { let response: JsonObject | undefined; // If cartoQuery is defined - use Query API (https://api-docs.carto.com/#8f2020d9-edf3-4b50-ae58-9edeaa34613c) if (this.cartoQuery) { const url = new URI(this.baseUrl) .path("") .path(`v3/maps/${this.connectionName}/query`); response = ( await callCartoApi(url.toString(), this.accessToken, { q: this.cartoQuery, geo_column: this.cartoGeoColumn }) )?.throwIfError(); } // If cartoTableName is defined - use Table API (https://api-docs.carto.com/#6a05d4d7-c6a1-4635-a8de-c91fa5e77fda) else if (this.cartoTableName) { const url = new URI(this.baseUrl) .path("") .path(`v3/maps/${this.connectionName}/table`) .query({ name: this.cartoTableName, columns: this.cartoColumns?.join(","), geo_column: this.cartoGeoColumn }); response = ( await callCartoApi(url.toString(), this.accessToken) )?.throwIfError(); } else { throw new TerriaError({ title: "Invalid Carto V3 config", message: "`cartoQuery` or `cartoTableName` must be defined" }); } let geoJsonUrls: string[] = []; // Commented out as we don't support tileJSON yet // let mvtTileJsonUrls: string[] = []; if ( response && isJsonObject(response.geojson) && isJsonStringArray(response.geojson.url) ) { geoJsonUrls = response.geojson.url; } // Commented out as we don't support tileJSON yet // if ( // response && // isJsonObject(response.tilejson) && // isJsonStringArray(response.tilejson.url) // ) { // mvtTileJsonUrls = response.tilejson.url; // } if (geoJsonUrls.length === 0 /*&& mvtTileJsonUrls.length === 0*/) { throw TerriaError.from("No GeoJSON found."); } runInAction(() => { if (response && isJsonNumber(response?.size)) { this.geoJsonSize = response.size; } this.geoJsonUrls = geoJsonUrls; // Commented out as we don't support tileJSON yet // this.mvtTileJsonUrls = mvtTileJsonUrls; }); } protected async forceLoadGeojsonData() { if (this.geoJsonUrls.length === 0) throw TerriaError.from("No GeoJSON URL found for Carto table"); let jsonData: JsonValue | undefined = undefined; // Download all geoJson files const geojsonResponses = await Promise.all( this.geoJsonUrls.map(async (url) => { jsonData = (await callCartoApi(url, this.accessToken))?.throwIfError(); if (jsonData === undefined) { throw new TerriaError({ title: "Failed to load GeoJSON", message: `Failed to load GeoJSON URL ${url}` }); } if ( isJsonObject(jsonData, false) && typeof jsonData.type === "string" ) { // Actual geojson const fc = toFeatureCollection(jsonData); if (fc) return fc; } throw new TerriaError({ title: "Failed to load GeoJSON", message: `Invalid response from GeoJSON URL ${url}:\n\n \`\`\` ${JSON.stringify(jsonData)} \`\`\`` }); }) ); // NOTE: Commented out until we add tileJson/mvt support // Download all tileJson files // const tilejsonResponses = await Promise.all( // this.mvtTileJsonUrls.map(async (url) => { // jsonData = await loadJson(url, { // Authorization: `Bearer ${this.accessToken}` // }); // if (jsonData === undefined) { // throw new TerriaError({ // title: "Failed to load GeoJSON", // message: `Failed to load GeoJSON URL ${url}` // }); // } // if (isJsonObject(jsonData, false)) { // return jsonData; // } // throw new TerriaError({ // title: "Failed to load GeoJSON", // message: `Invalid response from GeoJSON URL ${url}:\n\n // \`\`\` // ${JSON.stringify(jsonData)} // \`\`\`` // }); // }) // ); // Merge all geojson responses into a combined feature collection const combinedFeatureCollection = featureCollection< Geometry | GeometryCollection >([]); geojsonResponses.forEach((fc) => { for (let i = 0; i < fc.features.length; i++) { combinedFeatureCollection.features.push(fc.features[i]); } }); return combinedFeatureCollection; } } interface CartoApiErrorResponse { error?: string; message?: string; status?: number; source?: string; connection_id?: string; connection_name?: string; } /** Wrap loadJson calls to handle Carto API error messages */ async function callCartoApi(url: string, auth?: string, body?: JsonObject) { try { return new Result( await loadJson( url, auth ? { Authorization: `Bearer ${auth}` } : {}, body ) ); } catch (e) { if (e instanceof RequestErrorEvent) { try { const jsonResponse = isJsonString(e.response) ? JSON.parse(e.response) : e.response; if (isJsonObject(jsonResponse) && isJsonString(jsonResponse.error)) { const cartoError = jsonResponse as CartoApiErrorResponse; return Result.error( networkRequestError( TerriaError.from(e, { title: "Error from Carto API", message: cartoError.message ?? cartoError.error, importance: -1 }) ) ); } } catch {} } return Result.error(e); } }