Skip to content

Commit

Permalink
feat(hub-common): consolidates entity location construction and depre… (
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerjt authored Jan 24, 2024
1 parent 4e87143 commit d067225
Show file tree
Hide file tree
Showing 29 changed files with 678 additions and 222 deletions.
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"program": "${workspaceRoot}/node_modules/.bin/jasmine",
"args": [
"--config=jasmine.json"
]
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
Expand Down
33 changes: 4 additions & 29 deletions packages/common/src/content/_internal/computeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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) {
Expand Down
81 changes: 80 additions & 1 deletion packages/common/src/content/_internal/internalContentUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 5 additions & 15 deletions packages/common/src/content/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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[] = [];

Expand Down
20 changes: 20 additions & 0 deletions packages/common/src/core/_internal/computeBaseProps.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Partial<IHubItemEntity>>(
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;
}
3 changes: 3 additions & 0 deletions packages/common/src/discussions/_internal/computeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/discussions/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
46 changes: 46 additions & 0 deletions packages/common/src/extent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Polygon>): 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],
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/initiative-templates/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -82,6 +85,7 @@ export async function enrichInitiativeTemplateSearchResult(
thumbnail: "not-implemented",
workspaceRelative: "not-implemented",
},
location: deriveLocationFromItem(item),
rawResult: item,
};

Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/initiatives/HubInitiatives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -269,6 +272,7 @@ export async function enrichInitiativeSearchResult(
thumbnail: "not-implemented",
workspaceRelative: "not-implemented",
},
location: deriveLocationFromItem(item),
rawResult: item,
};

Expand Down
Loading

0 comments on commit d067225

Please sign in to comment.