Skip to content

Commit

Permalink
cleanup WKT code
Browse files Browse the repository at this point in the history
(re: #1387)

All minor stuff:
- move private functions to end of file
- move variable cleanup stuff into a `_clear()` function
- cleanup lint warnings, whitespace
- `parseAsWkt` needs to return `null` if it wants to do nothing
  (before it returned `{}` which evaluates as truthy)
- call .toUpperCase() on the wkt string, so it writes back to the url hash that way
- don't report a `dataUsed` for wkt sources (similar to task boundaries)
- make sure to check `typeof newData === 'string'` in `_hashchange`
  (sending non-string data to the wktparser fails)
- lots of testing
  • Loading branch information
bhousel committed Apr 30, 2024
1 parent fa6a54d commit cd29674
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 80 deletions.
2 changes: 1 addition & 1 deletion modules/core/UiSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
uiMap3dViewer, uiPhotoViewer, uiRapidServiceLicense,
uiRapidWhatsNew, uiRapidSplash, uiRestore, uiScale, uiShortcuts,
uiSidebar, uiSourceSwitch, uiSpinner, uiStatus, uiTooltip,
uiTopToolbar, uiVersion, uiZoom, uiZoomToSelection, uiCmd,
uiTopToolbar, uiVersion, uiZoom, uiZoomToSelection
} from '../ui/index.js';

import {
Expand Down
3 changes: 1 addition & 2 deletions modules/core/UrlHashSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ export class UrlHashSystem extends AbstractSystem {
*
* Responsive (user can change)
* __`background`__ - Imagery sourceID for the background imagery layer
* __`data`__ - A custom data URL for loading a gpx track, vector data source,
* or well-known POLYGON or MULTIPOLYGON text string to render as custom data.
* __`data`__ - A custom data URL for loading a gpx track, vector data source, [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) POLYGON or MULTIPOLYGON text string to render as custom data.
* __`gpx`__ - Same as `data`, it's just the legacy name for the same thing
* __`datasets`__ - A comma-separated list of Rapid/Esri datasetIDs to enable
* __`disable_features`__ - Disables features in the list.
Expand Down
160 changes: 84 additions & 76 deletions modules/pixi/PixiLayerCustomData.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as PIXI from 'pixi.js';
import { gpx, kml } from '@tmcw/togeojson';
import { Extent } from '@rapid-sdk/math';
import geojsonRewind from '@mapbox/geojson-rewind';
import { parse as wktParse } from 'wkt';

import { AbstractLayer } from './AbstractLayer.js';
import { PixiFeatureLine } from './PixiFeatureLine.js';
import { PixiFeaturePoint } from './PixiFeaturePoint.js';
import { PixiFeaturePolygon } from './PixiFeaturePolygon.js';
import { utilFetchResponse } from '../util/index.js';
import { parse as wktParse } from 'wkt';

const CUSTOM_COLOR = 0x00ffff;

Expand All @@ -29,12 +29,7 @@ export class PixiLayerCustomData extends AbstractLayer {
constructor(scene, layerID) {
super(scene, layerID);

this._dataUsed = null;
this._fileList = null;
this._template = null;
this._url = null;
this._geojson = null;
this._geojsonExtent = null;
this._clear();
this._fileReader = new FileReader();

// Ensure methods used as callbacks always have `this` bound correctly.
Expand Down Expand Up @@ -79,8 +74,7 @@ export class PixiLayerCustomData extends AbstractLayer {
* @param zoom Effective zoom to use for rendering
*/
render(frame, viewport, zoom) {

if (!this.enabled || !(this.hasData() || this.hasWkt())) return;
if (!this.enabled || !(this.hasData())) return;

const vtService = this.context.services.vectortile;
let geoData = [];
Expand All @@ -101,61 +95,29 @@ export class PixiLayerCustomData extends AbstractLayer {
this.renderLines(frame, viewport, zoom, lines);
this.renderPoints(frame, viewport, zoom, points);


//Now render any extras, like gridlines in square bounding boxes or arbitrary WKT polygons/multipolys.
// Now render any extras, like gridlines in square bounding boxes or arbitrary WKT polygons/multipolys.
const gridLines = this.createGridLines(lines);
const gridStyle = { stroke: { width: 0.5, color: 0x0ffff, alpha: 0.5, cap: PIXI.LINE_CAP.ROUND }};
this.renderLines(frame, viewport, zoom, gridLines, gridStyle);
}


/**
* createWktPolys
* creates WKT Polys from a raw string supplied by the data url param'.
*
* @param wktString - the poly or multipoly string(s) in wkt format
* i.e. 'POLYGON((-2.2 1.9, -2.3 1.7, -0.8 1.7, -0.8 1.9, -2.2 1.9))'
* or
* 'MULTIPOLYGON (((-1.5 1.3, -1.5 1.3, -1.5 1.3, -1.5 1.3, -1.4 1.3, -1.4 1.3, -1.5 1.3)),
* ((-1.5 1.3, -1.5 1.3, -1.5 1.3, -1.4 1.3, -1.5 1.3)))'
* @returns a list containing polygons to draw as a custom shape.
*/
createWktPolys(wktString) {
const parsedWkt = wktParse(wktString);

let poly = {};
// If it couldn't be parsed, or if it isn't a poly/multipoly, we can't render it. Issue an error.
if (!parsedWkt || (parsedWkt.type !== 'Polygon' && parsedWkt.type !== 'MultiPolygon')) {
return poly;
}


poly = {
type: 'Feature',
geometry: {
type: parsedWkt.type,
coordinates: parsedWkt.coordinates,
},
id: 'customWktPoly',
properties: {}
};

return poly;
}


/**
* createGridLines
* creates interstitial grid lines inside the rectangular bounding box, if specified.
* @param lines - the line string(s) that may contain a rectangular bounding box
* @returns a list of linestrings to draw as gridlines.
*/
createGridLines(lines) {
const numSplits = this.context.systems.imagery.numGridSplits;
const context = this.context;
const imagery = context.systems.imagery;
const rapid = context.systems.rapid;

const numSplits = imagery.numGridSplits;
let gridLines = [];

//'isTaskRectangular' implies one and only one rectangular linestring.
if (this.context.systems.rapid.isTaskRectangular && numSplits > 0) {
// 'isTaskRectangular' implies one and only one rectangular linestring.
if (rapid.isTaskRectangular && numSplits > 0) {
const box = lines[0];

const lats = box.geometry.coordinates.map((f) => f[0]);
Expand Down Expand Up @@ -422,12 +384,8 @@ export class PixiLayerCustomData extends AbstractLayer {
* @param {FileList|null} fileList Files to process (only first one is used), or null to reset
*/
setFileList(fileList) {
this._clear();
this._fileList = fileList;
this._dataUsed = null;
this._geojson = null;
this._geojsonExtent = null;
this._template = null;
this._url = null;
this.scene.disableLayers(this.layerID); // emits 'layerchange', so UI gets updated

if (!fileList || !fileList.length) return;
Expand All @@ -450,12 +408,8 @@ export class PixiLayerCustomData extends AbstractLayer {
* @param {string} url
*/
setUrl(url) {
this._clear();
this._url = url;
this._dataUsed = null;
this._fileList = null;
this._geojson = null;
this._geojsonExtent = null;
this._template = null;
this.scene.disableLayers(this.layerID); // emits 'layerchange', so UI gets updated

if (!url) return;
Expand Down Expand Up @@ -607,7 +561,7 @@ export class PixiLayerCustomData extends AbstractLayer {
}


/**
/**
* _calcExtent
* @param {Object} geojson - a GeoJSON Feature or FeatureCollection
* @return {Extent}
Expand Down Expand Up @@ -677,15 +631,18 @@ export class PixiLayerCustomData extends AbstractLayer {
const newData = currParams.get('data') || currParams.get('gpx');
const oldData = prevParams.get('data') || prevParams.get('gpx');
if (newData !== oldData) {

//First attempt to parse the data string as a WKT.
const wktPolys = this.createWktPolys(newData);

// If it is, treat it as such. If not, treat it as a URL.
if (wktPolys) {
this._setFile(wktPolys, '.geojson');
} else {
this.setUrl(newData);
this._clear();
if (typeof newData === 'string') {
// Attempt to parse the data string as a WKT.
// If it is, treat it as such. If not, treat it as a URL.
const geojson = this._parseAsWkt(newData);
if (geojson) {
this._wkt = newData.toUpperCase(); // convention is to use uppercase
this._setFile(geojson, '.geojson');
this._dataUsed = null; // A wkt area of interest is not really a data source
} else {
this.setUrl(newData);
}
}
}
}
Expand All @@ -702,14 +659,65 @@ export class PixiLayerCustomData extends AbstractLayer {
urlhash.setParam('gpx', null);
urlhash.setParam('data', null);

if (!this.enabled || typeof this._url !== 'string') return;
if (!this.enabled) return;

// 'gpx' is considered a "legacy" param..
// We'll only set it if the url really does seem to be for a gpx file
if (/gpx/i.test(this._url)) {
urlhash.setParam('gpx', this._url);
} else {
urlhash.setParam('data', this._url);
if (typeof this._wkt === 'string') {
urlhash.setParam('data', this._wkt);
} else if (typeof this._url === 'string') {
// 'gpx' is considered a "legacy" param..
// We'll only set it if the url really does seem to be for a gpx file
if (/gpx/i.test(this._url)) {
urlhash.setParam('gpx', this._url);
} else {
urlhash.setParam('data', this._url);
}
}
}


/**
* _parseAsWkt
* creates WKT Polys from a raw string supplied by the `data` url param.
*
* @param wktString - the poly or multipoly string(s) in wkt format
* i.e. 'POLYGON((-10 10, -10 -10, 10 -10, 10 10, -10 10))'
* or
* 'MULTIPOLYGON (((-1.5 1.3, -1.5 1.3, -1.5 1.3, -1.5 1.3, -1.4 1.3, -1.4 1.3, -1.5 1.3)),
* ((-1.5 1.3, -1.5 1.3, -1.5 1.3, -1.4 1.3, -1.5 1.3)))'
* @returns a list containing polygons to draw as a custom shape, or null
*/
_parseAsWkt(wktString) {
const parsedWkt = wktParse(wktString);

// If it couldn't be parsed, or if it isn't a poly/multipoly, we can't render it.
if (!parsedWkt || (parsedWkt.type !== 'Polygon' && parsedWkt.type !== 'MultiPolygon')) {
return null;
}

return {
type: 'Feature',
geometry: {
type: parsedWkt.type,
coordinates: parsedWkt.coordinates,
},
id: 'customWktPoly',
properties: {}
};
}


/**
* _clear
* Clear state to prepare for new custom data
*/
_clear() {
this._dataUsed = null;
this._fileList = null;
this._template = null;
this._wkt = null;
this._url = null;
this._geojson = null;
this._geojsonExtent = null;
}

}
8 changes: 7 additions & 1 deletion package-lock.json

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

0 comments on commit cd29674

Please sign in to comment.