diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d2b241cba5..9846c6d638f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,9 @@ "program": "${workspaceRoot}/node_modules/.bin/jasmine", "args": [ "--config=jasmine.json" - ] + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" }, { "type": "node", diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index c891c2d6720..f0119727ceb 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -2,11 +2,8 @@ import { IRequestOptions } from "@esri/arcgis-rest-request"; import { UserSession } from "@esri/arcgis-rest-auth"; import { getItemThumbnailUrl } from "../../resources"; import { IModel } from "../../types"; -import { bBoxToExtent, isBBox } from "../../extent"; -import { IExtent } from "@esri/arcgis-rest-types"; import { getItemHomeUrl } from "../../urls/get-item-home-url"; import { getContentEditUrl, getHubRelativeUrl } from "./internalContentUtils"; -import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; import { isDiscussable } from "../../discussions"; @@ -15,24 +12,7 @@ import { ServiceCapabilities, } from "../hostedServiceUtils"; import { IItemAndIServerEnrichments } from "../../items/_enrichments"; - -// if called and valid, set 3 things -- else just return type custom -export const getExtentObject = (itemExtent: number[][]): IExtent => { - return isBBox(itemExtent) - ? ({ ...bBoxToExtent(itemExtent), type: "extent" } as unknown as IExtent) - : undefined; -}; - -export function deriveLocationFromItemExtent(itemExtent?: number[][]) { - const location: IHubLocation = { type: "custom" }; - const geometry: any = getExtentObject(itemExtent); - if (geometry) { - location.geometries = [geometry]; - location.spatialReference = geometry.spatialReference; - location.extent = itemExtent; - } - return location; -} +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; export function computeProps( model: IModel, @@ -46,6 +26,9 @@ export function computeProps( token = session.token; } + // compute base properties on content + content = computeBaseProps(model.item, content); + // thumbnail url const thumbnailUrl = getItemThumbnailUrl(model.item, requestOptions, token); // TODO: Remove this once opendata-ui starts using `links.thumbnail` instead @@ -65,14 +48,6 @@ export function computeProps( // error that doesn't let us save the form content.licenseInfo = model.item.licenseInfo || ""; - if (!content.location) { - // build location if one does not exist based off of the boundary and the item's extent - content.location = - model.item.properties?.boundary === "none" - ? { type: "none" } - : deriveLocationFromItemExtent(model.item.extent); - } - content.isDiscussable = isDiscussable(content); if (enrichments.server) { diff --git a/packages/common/src/content/_internal/internalContentUtils.ts b/packages/common/src/content/_internal/internalContentUtils.ts index c6c1a73d3cb..95c17aa6ef7 100644 --- a/packages/common/src/content/_internal/internalContentUtils.ts +++ b/packages/common/src/content/_internal/internalContentUtils.ts @@ -11,17 +11,20 @@ import { parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; import { IItem, IPortal } from "@esri/arcgis-rest-portal"; import { + IExtent, ILayerDefinition, ISpatialReference, IUser, } from "@esri/arcgis-rest-types"; -import { IHubContent, PublisherSource } from "../../core"; +import { IHubContent, IHubLocation, PublisherSource } from "../../core"; import { IHubGeography, GeographyProvenance, IHubRequestOptions, } from "../../types"; import { + GeoJSONPolygonToBBox, + allCoordinatesPossiblyWGS84, bBoxToExtent, extentToBBox, extentToPolygon, @@ -36,6 +39,8 @@ import { getEnvironmentFromPortalUrl } from "../../utils"; import { HubEnvironment } from "../../permissions"; import { _getHubUrlFromPortalHostname } from "../../urls/_get-hub-url-from-portal-hostname"; import { IRequestOptions } from "@esri/arcgis-rest-request"; +import { geojsonToArcGIS } from "@terraformer/arcgis"; +import { Polygon } from "geojson"; /** * Hashmap of Hub environment and application url surfix @@ -95,6 +100,80 @@ export const getContentBoundary = (item: IItem): IHubGeography => { return boundary; }; +/** + * Constructs IExtent from numeric item extent array + * @param extent Raw item extent array + * @returns IExtent + */ +export const getExtentObject = (extent: number[][]): IExtent => { + return isBBox(extent) + ? ({ ...bBoxToExtent(extent), type: "extent" } as unknown as IExtent) + : undefined; +}; + +/** + * Derives proper IHubLocation given an ArcGIS Item. If no + * location (item.properties.location) is present, one will be + * constructed from the item's extent. + * @param item ArcGIS Item + * @returns IHubLocation + */ +export const deriveLocationFromItem = (item: IItem): IHubLocation => { + const { properties, extent } = item; + const location: IHubLocation = properties?.location; + + if (location) { + // IHubLocation already exists, so return it + return location; + } + + if (properties?.boundary === "none") { + // Per https://confluencewikidev.esri.com/display/Hub/Hub+Location+Management + // bounds = 'none' -> specifies not to show on map. If this is true and + // no location is already present, opt to not generate location from item extent + return { type: "none" }; + } + + // IHubLocation does not exist on item properties, so construct it + // from item extent + const geometry: any = getExtentObject(extent); + if (geometry) { + // geometry constructed from bbox + return { + type: "custom", + extent, + geometries: [geometry], + spatialReference: geometry.spatialReference, + }; + } else { + // Could not construct extent object, attempt to construct from geojson + try { + // Item extent is supposed to be in WGS84 per item documentation: + // https://developers.arcgis.com/rest/users-groups-and-items/item.htm + // But in many situations, this is not the case. So we do out best to + // determine the spatial reference of the extent. + const bbox = GeoJSONPolygonToBBox(extent as any as Polygon); + const defaultSpatialReference = { wkid: 4326 }; + const _geometry: Partial<__esri.Geometry> = { + type: "polygon", + ...geojsonToArcGIS(extent as any as Polygon), + spatialReference: allCoordinatesPossiblyWGS84(bbox) + ? defaultSpatialReference + : (getItemSpatialReference(item) as any) || defaultSpatialReference, + }; + return { + type: "custom", + extent: bbox, + geometries: [_geometry], + spatialReference: _geometry.spatialReference, + }; + } catch { + // Finally, exhausted all options and return a location of type none + return { type: "none" }; + } + } +}; + /** * Determine if we are in an enterprise environment * NOTE: when no request options are provided, the underlying diff --git a/packages/common/src/content/search.ts b/packages/common/src/content/search.ts index 887040d1f68..8fb271ff053 100644 --- a/packages/common/src/content/search.ts +++ b/packages/common/src/content/search.ts @@ -9,8 +9,10 @@ import { getItemHomeUrl } from "../urls"; import { unique } from "../util"; import { mapBy } from "../utils"; import { getFamily } from "./get-family"; -import { getHubRelativeUrl } from "./_internal/internalContentUtils"; -import { bBoxToExtent, extentToPolygon, isBBox } from "../extent"; +import { + deriveLocationFromItem, + getHubRelativeUrl, +} from "./_internal/internalContentUtils"; /** * Enrich a generic search result @@ -45,22 +47,10 @@ export async function enrichContentSearchResult( siteRelative: "not-implemented", thumbnail: "not-implemented", }, - location: item.properties?.location, + location: deriveLocationFromItem(item), rawResult: item, }; - // Include geometry in IHubSearchResult - if (isBBox(item.extent)) { - // PR Reference: https://github.com/Esri/hub.js/pull/987 - const extent = bBoxToExtent(item.extent); - const geometryPolygon = extentToPolygon(extent); - result.geometry = { - geometry: { type: "polygon", ...geometryPolygon }, - provenance: "item", - spatialReference: extent.spatialReference, - }; - } - // default includes const DEFAULTS: string[] = []; diff --git a/packages/common/src/core/_internal/computeBaseProps.ts b/packages/common/src/core/_internal/computeBaseProps.ts new file mode 100644 index 00000000000..a7dafacc24d --- /dev/null +++ b/packages/common/src/core/_internal/computeBaseProps.ts @@ -0,0 +1,20 @@ +import { IItem } from "@esri/arcgis-rest-types"; +import { IHubItemEntity } from "../types"; +import { deriveLocationFromItem } from "../../content/_internal/internalContentUtils"; + +/** + * Base property mapping for item backed entity types + * @param item IItem + * @param entity IHubItemEntity + * @returns + */ +export function computeBaseProps>( + item: IItem, + entity: T +): T { + // TODO: Currently only location is determined for base + // properties, but all properties that are commonly shared + // across items should be moved here. + entity.location = entity.location || deriveLocationFromItem(item); + return entity; +} diff --git a/packages/common/src/discussions/_internal/computeProps.ts b/packages/common/src/discussions/_internal/computeProps.ts index 444f390b437..742d47da45e 100644 --- a/packages/common/src/discussions/_internal/computeProps.ts +++ b/packages/common/src/discussions/_internal/computeProps.ts @@ -6,6 +6,7 @@ import { IModel } from "../../types"; import { IHubDiscussion } from "../../core"; import { isDiscussable } from "../utils"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and a Discussion, set various computed properties that can't be directly mapped @@ -25,6 +26,8 @@ export function computeProps( const session: UserSession = requestOptions.authentication as UserSession; token = session.token; } + // compute base properties on discussion + discussion = computeBaseProps(model.item, discussion); // thumbnail url discussion.thumbnailUrl = getItemThumbnailUrl( diff --git a/packages/common/src/discussions/edit.ts b/packages/common/src/discussions/edit.ts index 1b7648ae126..6b64c91b08a 100644 --- a/packages/common/src/discussions/edit.ts +++ b/packages/common/src/discussions/edit.ts @@ -132,7 +132,7 @@ export async function updateDiscussion( let allowedLocations: Polygon[]; try { allowedLocations = - updatedDiscussion.location?.geometries?.map( + updatedDiscussion.location.geometries?.map( (geometry) => arcgisToGeoJSON(geometry) as any as Polygon ) || null; } catch (e) { diff --git a/packages/common/src/extent.ts b/packages/common/src/extent.ts index 55a8a9e9204..7c08aeb686a 100644 --- a/packages/common/src/extent.ts +++ b/packages/common/src/extent.ts @@ -2,6 +2,7 @@ import { IExtent, IPoint, IPolygon, Position } from "@esri/arcgis-rest-types"; import { IHubRequestOptions, BBox } from "./types"; import { getProp } from "./objects"; import { IRequestOptions, request } from "@esri/arcgis-rest-request"; +import { Polygon } from "geojson"; /** * Turns an bounding box coordinate array into an extent object @@ -190,3 +191,48 @@ export const getExtentCenter = (extent: IExtent): IPoint => { const y = (ymax - ymin) / 2 + ymin; return { x, y, spatialReference }; }; + +/** + * Checks coordinate or coordinate array to determine if all coordinates are + * possibly WGS84. This is a best effert attempt, not a guarantee. + * @param bboxOrCoordinates + * @returns + */ +export const allCoordinatesPossiblyWGS84 = ( + bboxOrCoordinates: number[][] | number[] +): boolean => { + const flattenCoordinates = [].concat(...bboxOrCoordinates); + for (let i = 0; i < flattenCoordinates.length; i += 2) { + const [lon, lat] = [flattenCoordinates[i], flattenCoordinates[i + 1]]; + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + return false; + } + } + return true; +}; + +/** + * Turns a geojson polygon in to a bounding box coordinate array + * @param polygon + * @returns BBox + */ +export const GeoJSONPolygonToBBox = (polygon: Partial): BBox => { + let xmin = Infinity; + let ymin = Infinity; + let xmax = -Infinity; + let ymax = -Infinity; + + for (const coordinate of polygon.coordinates) { + for (const [x, y] of coordinate) { + xmin = Math.min(xmin, x); + ymin = Math.min(ymin, y); + xmax = Math.max(xmax, x); + ymax = Math.max(ymax, y); + } + } + + return [ + [xmin, ymin], + [xmax, ymax], + ]; +}; diff --git a/packages/common/src/initiative-templates/_internal/computeProps.ts b/packages/common/src/initiative-templates/_internal/computeProps.ts index 978cc1620f4..f85bfc2dacd 100644 --- a/packages/common/src/initiative-templates/_internal/computeProps.ts +++ b/packages/common/src/initiative-templates/_internal/computeProps.ts @@ -7,6 +7,7 @@ import { getItemThumbnailUrl } from "../../resources"; import { IModel } from "../../types"; import { InitiativeTemplateDefaultFeatures } from "./InitiativeTemplateBusinessRules"; import { computeLinks } from "./computeLinks"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and an initiative template, set various computed properties that can't be directly mapped @@ -27,6 +28,9 @@ export function computeProps( token = session.token; } + // compute base properties on initiativeTemplate + initiativeTemplate = computeBaseProps(model.item, initiativeTemplate); + // thumbnail url initiativeTemplate.thumbnailUrl = getItemThumbnailUrl( model.item, diff --git a/packages/common/src/initiative-templates/fetch.ts b/packages/common/src/initiative-templates/fetch.ts index 1ec59b93bb3..5504cd43c69 100644 --- a/packages/common/src/initiative-templates/fetch.ts +++ b/packages/common/src/initiative-templates/fetch.ts @@ -2,7 +2,10 @@ import { IRequestOptions } from "@esri/arcgis-rest-request"; import { getItem, IItem } from "@esri/arcgis-rest-portal"; import { getFamily } from "../content/get-family"; -import { getHubRelativeUrl } from "../content/_internal/internalContentUtils"; +import { + deriveLocationFromItem, + getHubRelativeUrl, +} from "../content/_internal/internalContentUtils"; import { IHubInitiativeTemplate } from "../core/types"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getItemBySlug } from "../items/slugs"; @@ -82,6 +85,7 @@ export async function enrichInitiativeTemplateSearchResult( thumbnail: "not-implemented", workspaceRelative: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/src/initiatives/HubInitiatives.ts b/packages/common/src/initiatives/HubInitiatives.ts index 16a9f5c9e47..bc80de9d10b 100644 --- a/packages/common/src/initiatives/HubInitiatives.ts +++ b/packages/common/src/initiatives/HubInitiatives.ts @@ -50,7 +50,10 @@ import { portalSearchItemsAsItems } from "../search/_internal/portalSearchItems" import { getTypeWithKeywordQuery } from "../associations/internal/getTypeWithKeywordQuery"; import { negateGroupPredicates } from "../search/_internal/negateGroupPredicates"; import { computeLinks } from "./_internal/computeLinks"; -import { getHubRelativeUrl } from "../content/_internal/internalContentUtils"; +import { + deriveLocationFromItem, + getHubRelativeUrl, +} from "../content/_internal/internalContentUtils"; import { setEntityStatusKeyword } from "../utils/internal/setEntityStatusKeyword"; /** @@ -269,6 +272,7 @@ export async function enrichInitiativeSearchResult( thumbnail: "not-implemented", workspaceRelative: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/src/initiatives/_internal/computeProps.ts b/packages/common/src/initiatives/_internal/computeProps.ts index 8a73aa6c0a4..b56524838d0 100644 --- a/packages/common/src/initiatives/_internal/computeProps.ts +++ b/packages/common/src/initiatives/_internal/computeProps.ts @@ -10,6 +10,7 @@ import { processEntityFeatures } from "../../permissions/_internal/processEntity import { InitiativeDefaultFeatures } from "./InitiativeBusinessRules"; import { computeLinks } from "./computeLinks"; import { getAuthedImageUrl } from "../../core/_internal/getAuthedImageUrl"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and an Initiative, set various computed properties that can't be directly mapped @@ -30,6 +31,9 @@ export function computeProps( token = session.token; } + // compute base properties on initiative + initiative = computeBaseProps(model.item, initiative); + // thumbnail url initiative.thumbnailUrl = getItemThumbnailUrl( model.item, diff --git a/packages/common/src/pages/HubPages.ts b/packages/common/src/pages/HubPages.ts index f12a8474163..d80cd5bf494 100644 --- a/packages/common/src/pages/HubPages.ts +++ b/packages/common/src/pages/HubPages.ts @@ -1,5 +1,8 @@ import { getFamily } from "../content/get-family"; -import { getHubRelativeUrl } from "../content/_internal/internalContentUtils"; +import { + deriveLocationFromItem, + getHubRelativeUrl, +} from "../content/_internal/internalContentUtils"; import { fetchItemEnrichments } from "../items/_enrichments"; import { getProp } from "../objects"; import { getItemThumbnailUrl } from "../resources"; @@ -214,6 +217,7 @@ export async function enrichPageSearchResult( siteRelative: "not-implemented", thumbnail: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/src/pages/_internal/computeProps.ts b/packages/common/src/pages/_internal/computeProps.ts index 1bed944ad53..0f52346b106 100644 --- a/packages/common/src/pages/_internal/computeProps.ts +++ b/packages/common/src/pages/_internal/computeProps.ts @@ -8,6 +8,7 @@ import { PageDefaultFeatures } from "./PageBusinessRules"; import { getItemHomeUrl } from "../../urls/get-item-home-url"; import { IHubPage } from "../../core/types/IHubPage"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and a page, set various computed properties that can't be directly mapped @@ -27,6 +28,8 @@ export function computeProps( const session: UserSession = requestOptions.authentication as UserSession; token = session.token; } + // compute base properties on page + page = computeBaseProps(model.item, page); // thumbnail url const thumbnailUrl = getItemThumbnailUrl(model.item, requestOptions, token); // TODO: Remove this once opendata-ui starts using `links.thumbnail` instead diff --git a/packages/common/src/projects/_internal/computeProps.ts b/packages/common/src/projects/_internal/computeProps.ts index 2f49330db79..912f76be9d8 100644 --- a/packages/common/src/projects/_internal/computeProps.ts +++ b/packages/common/src/projects/_internal/computeProps.ts @@ -8,6 +8,7 @@ import { processEntityFeatures } from "../../permissions/_internal/processEntity import { ProjectDefaultFeatures } from "./ProjectBusinessRules"; import { computeLinks } from "./computeLinks"; import { getAuthedImageUrl } from "../../core/_internal/getAuthedImageUrl"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and a project, set various computed properties that can't be directly mapped @@ -27,6 +28,8 @@ export function computeProps( const session: UserSession = requestOptions.authentication as UserSession; token = session.token; } + // compute base properties on project + project = computeBaseProps(model.item, project); // thumbnail url project.thumbnailUrl = getItemThumbnailUrl(model.item, requestOptions, token); diff --git a/packages/common/src/projects/fetch.ts b/packages/common/src/projects/fetch.ts index c2a7767c2f2..d949c4792c8 100644 --- a/packages/common/src/projects/fetch.ts +++ b/packages/common/src/projects/fetch.ts @@ -20,6 +20,7 @@ import { getProp } from "../objects/get-prop"; import { listAssociations } from "../associations"; import { getTypeByIdsQuery } from "../associations/internal/getTypeByIdsQuery"; import { computeLinks } from "./_internal/computeLinks"; +import { deriveLocationFromItem } from "../content/_internal/internalContentUtils"; /** * @private @@ -100,6 +101,7 @@ export async function enrichProjectSearchResult( thumbnail: "not-implemented", workspaceRelative: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts index 4c6b0ea40e2..cbd9b47da46 100644 --- a/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts +++ b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts @@ -7,7 +7,6 @@ import { import { IHubSearchResult } from "../../types/IHubSearchResult"; import { itemToSearchResult } from "../portalSearchItems"; import { IOgcItem } from "./interfaces"; -import { geojsonToArcGIS } from "@terraformer/arcgis"; export async function ogcItemToSearchResult( ogcItem: IOgcItem, @@ -19,21 +18,8 @@ export async function ogcItemToSearchResult( // as `license` and `source` if the OgcItem came from the index. const pseudoItem = ogcItem.properties as IItem; const result = await itemToSearchResult(pseudoItem, includes, requestOptions); - // Expose extraneous members like `license`, `source`, `properties.location` and `geometry` + // Expose extraneous members like `license` and `source` result.source = ogcItem.properties.source; result.license = ogcItem.properties.license; - result.location = ogcItem.properties.properties?.location; - // Add IHubGeography to result - if (ogcItem.geometry) { - try { - result.geometry = { - geometry: geojsonToArcGIS(ogcItem.geometry) as IPolygonProperties, - }; - } catch { - // If geojsonToArcGIS throws an error from an invalid input geometry, - // just ignore for now - } - } - return result; } diff --git a/packages/common/src/search/types/IHubSearchResult.ts b/packages/common/src/search/types/IHubSearchResult.ts index 4a6c0633a88..5db13e78921 100644 --- a/packages/common/src/search/types/IHubSearchResult.ts +++ b/packages/common/src/search/types/IHubSearchResult.ts @@ -40,6 +40,10 @@ export interface IHubSearchResult extends IHubEntityBase { typeKeywords?: string[]; /** + * @deprecated geometry is being dropped and replaced + * with 'location' for all location specific information + * on a search result + * * Geometry connected to this entity * For items, it will default to the extent, * but may be derived from a boundary resource diff --git a/packages/common/src/sites/HubSites.ts b/packages/common/src/sites/HubSites.ts index cd082796c4e..de751eef918 100644 --- a/packages/common/src/sites/HubSites.ts +++ b/packages/common/src/sites/HubSites.ts @@ -7,7 +7,10 @@ import { handleDomainChanges } from "./_internal"; import { IRequestOptions } from "@esri/arcgis-rest-request"; import { fetchItemEnrichments } from "../items/_enrichments"; import { parseInclude } from "../search/_internal/parseInclude"; -import { getHubRelativeUrl } from "../content/_internal/internalContentUtils"; +import { + deriveLocationFromItem, + getHubRelativeUrl, +} from "../content/_internal/internalContentUtils"; import { applyPermissionMigration } from "./_internal/applyPermissionMigration"; import { computeProps } from "./_internal/computeProps"; import { getPropertyMap } from "./_internal/getPropertyMap"; @@ -509,6 +512,7 @@ export async function enrichSiteSearchResult( siteRelative: "not-implemented", thumbnail: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/src/sites/_internal/computeProps.ts b/packages/common/src/sites/_internal/computeProps.ts index 569216a5e30..b10f5c46804 100644 --- a/packages/common/src/sites/_internal/computeProps.ts +++ b/packages/common/src/sites/_internal/computeProps.ts @@ -9,6 +9,7 @@ import { SiteDefaultFeatures } from "./SiteBusinessRules"; import { getItemHomeUrl } from "../../urls/get-item-home-url"; import { IHubSite } from "../../core/types/IHubSite"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * Given a model and a site, set various computed properties that can't be directly mapped @@ -28,7 +29,8 @@ export function computeProps( const session: UserSession = requestOptions.authentication as UserSession; token = session.token; } - + // compute base properties on site + site = computeBaseProps(model.item, site); // thumbnail url const thumbnailUrl = getItemThumbnailUrl(model.item, requestOptions, token); // TODO: Remove this once opendata-ui starts using `links.thumbnail` instead diff --git a/packages/common/src/templates/_internal/computeProps.ts b/packages/common/src/templates/_internal/computeProps.ts index f2fe1f74b0f..01af1d1598a 100644 --- a/packages/common/src/templates/_internal/computeProps.ts +++ b/packages/common/src/templates/_internal/computeProps.ts @@ -7,6 +7,7 @@ import { IHubTemplate } from "../../core/types/IHubTemplate"; import { computeLinks } from "./computeLinks"; import { getProp } from "../../objects"; import { getDeployedTemplateType } from "../utils"; +import { computeBaseProps } from "../../core/_internal/computeBaseProps"; /** * @private @@ -22,33 +23,36 @@ export function computeProps( template: Partial, requestOptions: IRequestOptions ): IHubTemplate { - // 1. compute relevant template links + // 1. compute base properties on template + template = computeBaseProps(model.item, template); + + // 2. compute relevant template links template.links = computeLinks(model.item, requestOptions); - // 2. append the template's thumbnail url at the top-level + // 3. append the template's thumbnail url at the top-level template.thumbnailUrl = template.links.thumbnail; - // 3. compute relevant template dates + // 4. compute relevant template dates template.createdDate = new Date(model.item.created); template.createdDateSource = "item.created"; template.updatedDate = new Date(model.item.modified); template.updatedDateSource = "item.modified"; - // 4. determine whether the template is discussable + // 5. determine whether the template is discussable template.isDiscussable = isDiscussable(template); - // 5. process features that can be disabled by the entity owner + // 6. process features that can be disabled by the entity owner template.features = processEntityFeatures( getProp(model, "data.settings.features") || {}, TemplateDefaultFeatures ); - // 6. compute additional template-specific properties + // 7. compute additional template-specific properties template.isDeployed = (getProp(model, "item.typeKeywords") || []).includes( "Deployed" ); template.deployedType = getDeployedTemplateType(model.item); - // 7. cast b/c this takes a partial but returns a full template + // 8. cast b/c this takes a partial but returns a full template return template as IHubTemplate; } diff --git a/packages/common/src/templates/fetch.ts b/packages/common/src/templates/fetch.ts index 661042dfb41..0a7ba185dd3 100644 --- a/packages/common/src/templates/fetch.ts +++ b/packages/common/src/templates/fetch.ts @@ -16,6 +16,7 @@ import { fetchItemEnrichments } from "../items/_enrichments"; import { getProp } from "../objects"; import { computeLinks } from "./_internal/computeLinks"; import { getDeployedTemplateType } from "./utils"; +import { deriveLocationFromItem } from "../content/_internal/internalContentUtils"; /** * @private @@ -98,6 +99,7 @@ export async function enrichTemplateSearchResult( thumbnail: "not-implemented", workspaceRelative: "not-implemented", }, + location: deriveLocationFromItem(item), rawResult: item, }; diff --git a/packages/common/test/content/_internal/internalContentUtils.test.ts b/packages/common/test/content/_internal/internalContentUtils.test.ts index 2d4d9a6b527..245bde19ce1 100644 --- a/packages/common/test/content/_internal/internalContentUtils.test.ts +++ b/packages/common/test/content/_internal/internalContentUtils.test.ts @@ -1,8 +1,14 @@ import { IItem } from "@esri/arcgis-rest-types"; -import { getContentEditUrl } from "../../../src/content/_internal/internalContentUtils"; +import { + getContentEditUrl, + getExtentObject, + deriveLocationFromItem, +} from "../../../src/content/_internal/internalContentUtils"; +import * as internalContentUtils from "../../../src/content/_internal/internalContentUtils"; import { IHubRequestOptions } from "../../../src/types"; import { cloneObject } from "../../../src/util"; import { MOCK_HUB_REQOPTS } from "../../mocks/mock-auth"; +import { IHubLocation } from "../../../src"; describe("getContentEditUrl", () => { let requestOptions: IHubRequestOptions; @@ -289,3 +295,177 @@ describe("getContentEditUrl", () => { expect(chk).toBe("https://myserver.gis/home/item.html?id=9001"); }); }); + +describe("getExtentObject", () => { + it("getExtentObject isBBox is true", () => { + const chk = getExtentObject([ + [100, 100], + [120, 120], + ]); + expect(chk.xmin).toBe(100); + expect(chk.ymin).toBe(100); + expect(chk.xmax).toBe(120); + expect(chk.ymax).toBe(120); + }); + it("getExtentObject isBBox is true", () => { + const chk = getExtentObject([]); + expect(chk).toBeUndefined(); + }); +}); + +describe("deriveLocationFromItem", () => { + const item: IItem = { + type: "Dashboard", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + properties: {}, + typeKeywords: ["ArcGIS Dashboards"], + } as IItem; + it("should return existing IHubLocation", () => { + const _item = cloneObject(item); + const location = { + type: "custom", + extent: [[0], [0]], + } as IHubLocation; + _item.properties = { + location, + }; + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual(location); + }); + it("should return boundary if type none", () => { + const _item = cloneObject(item); + _item.properties = { + boundary: { type: "none" }, + }; + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual({ type: "none" }); + }); + it("should return boundary if no extent on item", () => { + const _item = cloneObject(item); + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual({ type: "none" }); + }); + it("should create custom location from item extent", () => { + const getExtentObjectSpy = spyOn( + internalContentUtils, + "getExtentObject" + ).and.callThrough(); + const _item = cloneObject(item); + _item.extent = [ + [0, 0], + [0, 0], + ]; + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual({ + type: "custom", + extent: [ + [0, 0], + [0, 0], + ], + geometries: [ + { + xmin: 0, + ymin: 0, + xmax: 0, + ymax: 0, + spatialReference: { + wkid: 4326, + }, + type: "extent", + } as any, + ], + spatialReference: { + wkid: 4326, + }, + }); + }); + it("should construct location from geojson if getExtentObject undefined", () => { + const _item = cloneObject(item); + _item.spatialReference = "102100" as any; + _item.extent = { + type: "Polygon", + coordinates: [ + [ + [-14999967.4259, 6499962.1704], + [-6200050.3592, 6499962.1704], + [-6200050.3592, 2352563.6343], + [-14999967.4259, 2352563.6343], + [-14999967.4259, 6499962.1704], + ], + ], + } as any; + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual({ + type: "custom", + extent: [ + [-14999967.4259, 2352563.6343], + [-6200050.3592, 6499962.1704], + ], + geometries: [ + { + type: "polygon", + rings: [ + [ + [-14999967.4259, 6499962.1704], + [-6200050.3592, 6499962.1704], + [-6200050.3592, 2352563.6343], + [-14999967.4259, 2352563.6343], + [-14999967.4259, 6499962.1704], + ], + ], + spatialReference: { + wkid: 102100, + } as any, + } as any, + ], + spatialReference: { + wkid: 102100, + }, + }); + }); + it("should construct location from geojson and return default spatial reference if none defined", () => { + const _item = cloneObject(item); + _item.extent = { + type: "Polygon", + coordinates: [ + [ + [-14999967.4259, 6499962.1704], + [-6200050.3592, 6499962.1704], + [-6200050.3592, 2352563.6343], + [-14999967.4259, 2352563.6343], + [-14999967.4259, 6499962.1704], + ], + ], + } as any; + const chk = deriveLocationFromItem(_item); + expect(chk).toEqual({ + type: "custom", + extent: [ + [-14999967.4259, 2352563.6343], + [-6200050.3592, 6499962.1704], + ], + geometries: [ + { + type: "polygon", + rings: [ + [ + [-14999967.4259, 6499962.1704], + [-6200050.3592, 6499962.1704], + [-6200050.3592, 2352563.6343], + [-14999967.4259, 2352563.6343], + [-14999967.4259, 6499962.1704], + ], + ], + spatialReference: { + wkid: 4326, + } as any, + } as any, + ], + spatialReference: { + wkid: 4326, + }, + }); + }); +}); diff --git a/packages/common/test/content/computeProps.test.ts b/packages/common/test/content/computeProps.test.ts index 10a4c5ff77c..30d27fa09cc 100644 --- a/packages/common/test/content/computeProps.test.ts +++ b/packages/common/test/content/computeProps.test.ts @@ -1,8 +1,4 @@ -import { - computeProps, - deriveLocationFromItemExtent, - getExtentObject, -} from "../../src/content/_internal/computeProps"; +import { computeProps } from "../../src/content/_internal/computeProps"; import { IHubEditableContent } from "../../src/core/types/IHubEditableContent"; import { IItemAndIServerEnrichments } from "../../src/items/_enrichments"; import { IHubRequestOptions, IModel } from "../../src/types"; @@ -23,6 +19,10 @@ describe("content computeProps", () => { it("handles when properties are undefined", () => { const model: IModel = { item: { + extent: [ + [0, 0], + [0, 0], + ], type: "Feature Service", id: "9001", created: new Date().getTime(), @@ -44,6 +44,10 @@ describe("content computeProps", () => { it("handles when boundary is undefined", () => { const model: IModel = { item: { + extent: [ + [0, 0], + [0, 0], + ], type: "Feature Service", id: "9001", created: new Date().getTime(), @@ -181,27 +185,3 @@ describe("content computeProps", () => { expect(chk.thumbnail).toBeUndefined(); }); }); - -describe("getItemExtent", () => { - it("getItemExtent isBBox is true", () => { - const chk = getExtentObject([ - [100, 100], - [120, 120], - ]); - expect(chk.xmin).toBe(100); - expect(chk.ymin).toBe(100); - expect(chk.xmax).toBe(120); - expect(chk.ymax).toBe(120); - }); -}); - -describe("deriveLocationFromItemExtent", () => { - it("deriveLocationFromItemExtent valid extent", () => { - const chk = deriveLocationFromItemExtent([ - [100, 100], - [120, 120], - ]); - expect(chk.geometries?.length).toBe(1); - expect(chk.type).toBe("custom"); - }); -}); diff --git a/packages/common/test/content/index.test.ts b/packages/common/test/content/index.test.ts index 7f23278789c..cd38a69bceb 100644 --- a/packages/common/test/content/index.test.ts +++ b/packages/common/test/content/index.test.ts @@ -6,8 +6,8 @@ import { IHubLocation, IHubRequestOptions, } from "../../src"; +import * as internalContentUtils from "../../src/content/_internal/internalContentUtils"; import * as FetchEnrichments from "../../src/items/_enrichments"; -import * as Extent from "../../src/extent"; const LOCATION: IHubLocation = { type: "custom", @@ -93,6 +93,7 @@ describe("content module:", () => { }); describe("enrichments:", () => { let enrichmentSpy: jasmine.Spy; + let deriveLocationFromItemSpy: jasmine.Spy; let hubRo: IHubRequestOptions; beforeEach(() => { enrichmentSpy = spyOn( @@ -105,6 +106,10 @@ describe("content module:", () => { }, }); }); + deriveLocationFromItemSpy = spyOn( + internalContentUtils, + "deriveLocationFromItem" + ).and.callThrough(); hubRo = { portal: "https://some-server.com/gis/sharing/rest", }; @@ -121,6 +126,10 @@ describe("content module:", () => { 0, "should not fetch enrichments" ); + expect(deriveLocationFromItemSpy.calls.count()).toBe( + 1, + "should call location enrichment" + ); // verify expected output const ITM = cloneObject(FEATURE_SERVICE_ITEM); @@ -171,46 +180,5 @@ describe("content module:", () => { expect(enrichments).toEqual(["server"]); expect(ro).toBe(hubRo); }); - - it("adds geometry to search result", async () => { - const isBBoxSpy = spyOn(Extent, "isBBox").and.callThrough(); - const bBoxToExtentSpy = spyOn(Extent, "bBoxToExtent").and.callThrough(); - const extentToPolygonSpy = spyOn( - Extent, - "extentToPolygon" - ).and.callThrough(); - const chk = await enrichContentSearchResult( - cloneObject(FEATURE_SERVICE_ITEM), - ["server.layers.length AS layerCount"], - hubRo - ); - expect(isBBoxSpy.calls.count()).toBe( - 1, - "should call isBBoxSpy on item.extent" - ); - expect(isBBoxSpy.calls.allArgs()[0]).toEqual([ - FEATURE_SERVICE_ITEM.extent, - ]); - expect(bBoxToExtentSpy.calls.count()).toBe( - 1, - "should convert bbox to extent" - ); - expect(bBoxToExtentSpy.calls.allArgs()[0]).toEqual([ - FEATURE_SERVICE_ITEM.extent, - ]); - expect(extentToPolygonSpy.calls.count()).toBe( - 1, - "should call extentToPolygon" - ); - expect(extentToPolygonSpy.calls.allArgs()[0]).toEqual([ - { - xmin: 20.9847, - ymin: 37.0075, - xmax: 26.6331, - ymax: 41.7264, - spatialReference: { wkid: 4326 }, - }, - ]); - }); }); }); diff --git a/packages/common/test/core/_internal/computeBaseProps.tests.ts b/packages/common/test/core/_internal/computeBaseProps.tests.ts new file mode 100644 index 00000000000..5b24bdef73f --- /dev/null +++ b/packages/common/test/core/_internal/computeBaseProps.tests.ts @@ -0,0 +1,18 @@ +import { computeBaseProps } from "../../../src/core/_internal/computeBaseProps"; +import { IItem } from "@esri/arcgis-rest-types"; +import { IHubItemEntity } from "../../../src"; +import * as internalContentUtils from "../../../src/content/_internal/internalContentUtils"; + +describe("computeBaseProps", () => { + const item: IItem = {} as IItem; + const entity: Partial = {}; + it("sets base properties on entity", () => { + const deriveLocationFromItemSpy = spyOn( + internalContentUtils, + "deriveLocationFromItem" + ).and.callThrough(); + const chk = computeBaseProps(item, entity); + expect(chk.location).toEqual({ type: "none" }); + expect(deriveLocationFromItemSpy.calls.count()).toEqual(1); + }); +}); diff --git a/packages/common/test/discussions/edit.test.ts b/packages/common/test/discussions/edit.test.ts index f48f8cd7c20..6b9af643aa3 100644 --- a/packages/common/test/discussions/edit.test.ts +++ b/packages/common/test/discussions/edit.test.ts @@ -196,7 +196,7 @@ describe("discussions edit:", () => { ...DEFAULT_SETTINGS, }) ); - const prj: IHubDiscussion = { + const disc: IHubDiscussion = { itemControl: "edit", id: GUID, name: "Hello World", @@ -217,7 +217,7 @@ describe("discussions edit:", () => { canDelete: false, typeKeywords: [], }; - const chk = await updateDiscussion(prj, { authentication: MOCK_AUTH }); + const chk = await updateDiscussion(disc, { authentication: MOCK_AUTH }); expect(chk.id).toBe(GUID); expect(chk.name).toBe("Hello World"); expect(chk.description).toBe("Some longer description"); @@ -235,7 +235,7 @@ describe("discussions edit:", () => { expect(updateModelSpy.calls.count()).toBe(1); const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; expect(createSettingsSpy.calls.count()).toBe(1); - expect(modelToUpdate.item.description).toBe(prj.description); + expect(modelToUpdate.item.description).toBe(disc.description); expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); }); it("updates backing model and updates settings when settings id exists", async () => { @@ -259,7 +259,7 @@ describe("discussions edit:", () => { ...DEFAULT_SETTINGS, }) ); - const prj: IHubDiscussion = { + const disc: IHubDiscussion = { itemControl: "edit", id: GUID, name: "Hello World", @@ -281,7 +281,7 @@ describe("discussions edit:", () => { typeKeywords: [], entitySettingsId: "an id", }; - const chk = await updateDiscussion(prj, { authentication: MOCK_AUTH }); + const chk = await updateDiscussion(disc, { authentication: MOCK_AUTH }); expect(chk.id).toBe(GUID); expect(chk.name).toBe("Hello World"); expect(chk.description).toBe("Some longer description"); @@ -299,7 +299,7 @@ describe("discussions edit:", () => { expect(updateModelSpy.calls.count()).toBe(1); expect(updateSettingsSpy.calls.count()).toBe(1); const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; - expect(modelToUpdate.item.description).toBe(prj.description); + expect(modelToUpdate.item.description).toBe(disc.description); expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); }); describe("allowedLocations", () => { @@ -328,7 +328,7 @@ describe("discussions edit:", () => { ...DEFAULT_SETTINGS, }) ); - const prj: IHubDiscussion = { + const disc: IHubDiscussion = { itemControl: "edit", id: GUID, name: "Hello World", @@ -354,7 +354,7 @@ describe("discussions edit:", () => { geometries: [{ x: 0, y: 0 } as any], }, }; - const chk = await updateDiscussion(prj, { authentication: MOCK_AUTH }); + const chk = await updateDiscussion(disc, { authentication: MOCK_AUTH }); expect(chk.id).toBe(GUID); expect(chk.name).toBe("Hello World"); expect(chk.description).toBe("Some longer description"); @@ -372,7 +372,7 @@ describe("discussions edit:", () => { expect(updateModelSpy.calls.count()).toBe(1); expect(updateSettingsSpy.calls.count()).toBe(1); const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; - expect(modelToUpdate.item.description).toBe(prj.description); + expect(modelToUpdate.item.description).toBe(disc.description); expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); // should transform location and persist to settings expect(arcgisToGeoJSONSpy.calls.count()).toBe(1); @@ -381,6 +381,83 @@ describe("discussions edit:", () => { settingsToUpdate.data.settings.discussions.allowedLocations ).toEqual([{ type: "Point", coordinates: [0, 0] }]); }); + it("processes locations of type = 'none' and persists them to settings", async () => { + const slugSpy = spyOn(slugUtils, "getUniqueSlug").and.returnValue( + Promise.resolve("dcdev-wat-blarg-1") + ); + const getModelSpy = spyOn(modelUtils, "getModel").and.returnValue( + Promise.resolve(DISCUSSION_MODEL) + ); + const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( + (m: IModel) => { + return Promise.resolve(m); + } + ); + const arcgisToGeoJSONSpy = spyOn( + terraformer, + "arcgisToGeoJSON" + ).and.returnValue({ type: "Point", coordinates: [0, 0] }); + const updateSettingsSpy = spyOn( + settingUtils, + "updateSetting" + ).and.returnValue( + Promise.resolve({ + id: GUID, + ...DEFAULT_SETTINGS, + }) + ); + const disc: IHubDiscussion = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Discussion", + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + typeKeywords: [], + entitySettingsId: "an id", + location: { + type: "none", + }, + }; + const chk = await updateDiscussion(disc, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + expect(chk.entitySettingsId).toBe(GUID); + expect(chk.discussionSettings).toEqual( + DEFAULT_SETTINGS.settings?.discussions + ); + // should ensure unique slug + expect(slugSpy.calls.count()).toBe(1); + expect(slugSpy.calls.argsFor(0)[0]).toEqual( + { slug: "dcdev-wat-blarg", existingId: GUID }, + "should receive slug" + ); + expect(getModelSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + expect(updateSettingsSpy.calls.count()).toBe(1); + const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; + expect(modelToUpdate.item.description).toBe(disc.description); + expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); + // should transform location and persist to settings + expect(arcgisToGeoJSONSpy.calls.count()).toBe(0); + const settingsToUpdate = updateSettingsSpy.calls.argsFor(0)[0]; + expect( + settingsToUpdate.data.settings.discussions.allowedLocations + ).toBeNull(); + }); it("processes locations and handles error if thrown", async () => { const slugSpy = spyOn(slugUtils, "getUniqueSlug").and.returnValue( Promise.resolve("dcdev-wat-blarg-1") @@ -409,7 +486,7 @@ describe("discussions edit:", () => { ...DEFAULT_SETTINGS, }) ); - const prj: IHubDiscussion = { + const disc: IHubDiscussion = { itemControl: "edit", id: GUID, name: "Hello World", @@ -435,7 +512,7 @@ describe("discussions edit:", () => { geometries: [{ x: 0, y: 0 } as any], }, }; - const chk = await updateDiscussion(prj, { authentication: MOCK_AUTH }); + const chk = await updateDiscussion(disc, { authentication: MOCK_AUTH }); expect(chk.id).toBe(GUID); expect(chk.name).toBe("Hello World"); expect(chk.description).toBe("Some longer description"); @@ -453,7 +530,7 @@ describe("discussions edit:", () => { expect(updateModelSpy.calls.count()).toBe(1); expect(updateSettingsSpy.calls.count()).toBe(1); const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; - expect(modelToUpdate.item.description).toBe(prj.description); + expect(modelToUpdate.item.description).toBe(disc.description); expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); // should transform location and persist to settings expect(arcgisToGeoJSONSpy.calls.count()).toBe(1); diff --git a/packages/common/test/extent/extent.test.ts b/packages/common/test/extent/extent.test.ts index baa8202a5da..9f586cdea4b 100644 --- a/packages/common/test/extent/extent.test.ts +++ b/packages/common/test/extent/extent.test.ts @@ -1,30 +1,146 @@ -import { isValidExtent } from "../../src/extent"; +import { + isValidExtent, + allCoordinatesPossiblyWGS84, + GeoJSONPolygonToBBox, +} from "../../src/extent"; import { IExtent } from "@esri/arcgis-rest-types"; -describe("isValidExtent", function() { - it("identifies valid extent coordinate array", function() { - const extent: object = [[-122.68, 45.53], [-122.45, 45.6]]; +describe("isValidExtent", function () { + it("identifies valid extent coordinate array", function () { + const extent: object = [ + [-122.68, 45.53], + [-122.45, 45.6], + ]; const result = isValidExtent(extent); expect(result).toBeTruthy(); }); - it("identifies valid extent JSON", function() { + it("identifies valid extent JSON", function () { const extent: IExtent = { xmin: -122.68, ymin: 45.53, xmax: -122.45, ymax: 45.6, spatialReference: { - wkid: 4326 - } + wkid: 4326, + }, }; const result = isValidExtent(extent); expect(result).toBeTruthy(); }); - it("identifies invalid extent", function() { + it("identifies invalid extent", function () { const extent: object = { - str: "I am invalid" + str: "I am invalid", }; const result = isValidExtent(extent); expect(result).toBeFalsy(); }); }); + +describe("allCoordinatesPossiblyWGS84", function () { + it("returns true for valid WGS84 coordinates", function () { + const coordinates: number[][] = [ + [-122.68, 45.53], + [-122.45, 45.6], + ]; + const result = allCoordinatesPossiblyWGS84(coordinates); + expect(result).toBeTruthy(); + }); + + it("returns false for invalid WGS84 coordinates", function () { + const coordinates: number[][] = [ + [-122.68, 45.53], + [-122.45, 95.6], + ]; + const result = allCoordinatesPossiblyWGS84(coordinates); + expect(result).toBeFalsy(); + }); + + it("returns true for valid single coordinate", function () { + const coordinates: number[] = [-122.68, 45.53]; + const result = allCoordinatesPossiblyWGS84(coordinates); + expect(result).toBeTruthy(); + }); + + it("returns false for invalid single coordinate", function () { + const coordinates: number[] = [-122.68, 195.53]; + const result = allCoordinatesPossiblyWGS84(coordinates); + expect(result).toBeFalsy(); + }); +}); + +describe("GeoJSONPolygonToBBox", function () { + it("returns the correct bounding box for a polygon", function () { + const polygon = { + coordinates: [ + [ + [-122.68, 45.53], + [-122.45, 45.6], + [-122.6, 45.7], + [-122.8, 45.8], + [-122.68, 45.53], + ], + ], + }; + const result = GeoJSONPolygonToBBox(polygon); + expect(result).toEqual([ + [-122.8, 45.53], + [-122.45, 45.8], + ]); + }); + + it("returns the correct bounding box for a polygon with multiple rings", function () { + const polygon = { + coordinates: [ + [ + [-122.68, 45.53], + [-122.45, 45.6], + [-122.6, 45.7], + [-122.8, 45.8], + [-122.68, 45.53], + ], + [ + [-122.7, 45.55], + [-122.55, 45.62], + [-122.65, 45.72], + [-122.85, 45.82], + [-122.7, 45.55], + ], + ], + }; + const result = GeoJSONPolygonToBBox(polygon); + expect(result).toEqual([ + [-122.85, 45.53], + [-122.45, 45.82], + ]); + }); + + it("returns the correct bounding box for a polygon with negative coordinates", function () { + const polygon = { + coordinates: [ + [ + [-122.68, -45.53], + [-122.45, -45.6], + [-122.6, -45.7], + [-122.8, -45.8], + [-122.68, -45.53], + ], + ], + }; + const result = GeoJSONPolygonToBBox(polygon); + expect(result).toEqual([ + [-122.8, -45.8], + [-122.45, -45.53], + ]); + }); + + it("returns the correct bounding box for a polygon with a single point", function () { + const polygon = { + coordinates: [[[-122.68, 45.53]]], + }; + const result = GeoJSONPolygonToBBox(polygon); + expect(result).toEqual([ + [-122.68, 45.53], + [-122.68, 45.53], + ]); + }); +}); diff --git a/packages/common/test/search/_internal/hubSearchItems.test.ts b/packages/common/test/search/_internal/hubSearchItems.test.ts index 8342d67210b..c90eec2d455 100644 --- a/packages/common/test/search/_internal/hubSearchItems.test.ts +++ b/packages/common/test/search/_internal/hubSearchItems.test.ts @@ -11,8 +11,6 @@ import { } from "../../../src"; import { UserSession } from "@esri/arcgis-rest-auth"; -import * as Terraformer from "@terraformer/arcgis"; - import { formatPredicate, formatFilterBlock, @@ -675,23 +673,33 @@ describe("hubSearchItems Module |", () => { }, // TODO: fill this and add some verification rawResult: ogcItemsResponse.features[0].properties as IOgcItem, - geometry: { - geometry: { - rings: [ - [ - [-121.11799999999793, 39.37030746927015], - [-119.00899999999801, 39.37030746927015], - [-119.00899999999801, 38.67499450446548], - [-121.11799999999793, 38.67499450446548], - [-121.11799999999793, 39.37030746927015], + location: { + type: "custom", + extent: [ + [-121.11799999999793, 38.67499450446548], + [-119.00899999999801, 39.37030746927015], + ], + geometries: [ + { + type: "polygon", + rings: [ + [ + [-121.11799999999793, 39.37030746927015], + [-119.00899999999801, 39.37030746927015], + [-119.00899999999801, 38.67499450446548], + [-121.11799999999793, 38.67499450446548], + [-121.11799999999793, 39.37030746927015], + ], ], - ], - spatialReference: { - wkid: 4326, - }, + spatialReference: { + wkid: 4326, + } as any, + } as any, + ], + spatialReference: { + wkid: 4326, }, - } as any, - location: undefined, + }, }, ]; @@ -778,7 +786,6 @@ describe("hubSearchItems Module |", () => { ...mockedItemToSearchResultResponse, source: "my-source", license: "CC-BY-4.0", - location: undefined, }); }); it("adds item.properties.location on result", async () => { @@ -790,9 +797,7 @@ describe("hubSearchItems Module |", () => { const delegateSpy = spyOn( portalSearchItemsModule, "itemToSearchResult" - ).and.returnValue( - Promise.resolve(cloneObject(mockedItemToSearchResultResponse)) - ); + ).and.callThrough(); const _ogcItemProperties = { ...cloneObject(ogcItemProperties), properties: { @@ -821,40 +826,7 @@ describe("hubSearchItems Module |", () => { includes, requestOptions ); - expect(result).toEqual({ - ...mockedItemToSearchResultResponse, - source: "my-source", - license: "CC-BY-4.0", - location: LOCATION, - }); - }); - it("adds arcgis geometry to result", async () => { - const geojsonToArcGISSpy = spyOn( - Terraformer, - "geojsonToArcGIS" - ).and.callThrough(); - const includes: string[] = []; - const requestOptions: IHubRequestOptions = {}; - await ogcItemToSearchResult( - ogcItemsResponse.features[0], - includes, - requestOptions - ); - expect(geojsonToArcGISSpy.calls.count()).toBe( - 1, - "Calls geojsonToArcGIS()" - ); - expect(geojsonToArcGISSpy.calls.allArgs()[0]).toEqual([ - ogcItemsResponse.features[0].geometry, - ]); - - geojsonToArcGISSpy.and.throwError("Error"); - await ogcItemToSearchResult( - ogcItemsResponse.features[0], - includes, - requestOptions - ); - expect(geojsonToArcGISSpy).toThrowError("Error"); + expect(result.location).toEqual(ogcItem.properties.properties.location); }); });