Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hub-common): consolidates entity location construction and depre… #1382

Merged
merged 13 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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),
tannerjt marked this conversation as resolved.
Show resolved Hide resolved
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);
tannerjt marked this conversation as resolved.
Show resolved Hide resolved
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);
tannerjt marked this conversation as resolved.
Show resolved Hide resolved

// 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),
tannerjt marked this conversation as resolved.
Show resolved Hide resolved
rawResult: item,
};

Expand Down
Loading