Skip to content

Commit

Permalink
feat: make polygon support geojson feature and fix autozoom (#11)
Browse files Browse the repository at this point in the history
* feat: support standard geojson feature in polygon

* fix: viewport autozoom

* fix: type

* fix: lint

* refactor: renames

* fix: travis

* build: add yarn.lock

* fix: travis

* fix: error message

* fix: storybook

* fix: improt

* fix: address comments

* fix: storybook

* fix: remove yarn.lock

* refactor: viewport

* fix: extension

* fix: extension
  • Loading branch information
kristw authored and zhaoyongjie committed Nov 30, 2021
1 parent 1a93f58 commit 940e449
Show file tree
Hide file tree
Showing 16 changed files with 18,353 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ webpack.config.js
# Lock files, libs should not have lock files
npm-shrinkwrap.json
package-lock.json
yarn.lock
old-yarn.lock
.*.swp
_gh-pages
_gh-pages
# Now only allow yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"access": "public"
},
"dependencies": {
"@types/d3-array": "^2.0.0",
"bootstrap-slider": "^10.0.0",
"d3-array": "^1.2.4",
"d3-color": "^1.2.0",
Expand All @@ -41,7 +42,7 @@
"react-map-gl": "^4.0.10",
"underscore": "^1.8.3",
"urijs": "^1.18.10",
"viewport-mercator-project": "^6.1.1"
"@math.gl/web-mercator": "^3.1.3"
},
"peerDependencies": {
"@superset-ui/chart": "^0.12.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import Legend from './components/Legend';
import { hexToRGB } from './utils/colors';
import { getPlaySliderParams } from './utils/time';
import sandboxedEval from './utils/sandbox';
import { fitViewport } from './layers/common';
// eslint-disable-next-line import/extensions
import fitViewport from './utils/fitViewport';

const { getScale } = CategoricalColorNamespace;

Expand Down Expand Up @@ -119,9 +120,15 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {

const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);

const viewport = props.formData.autozoom
? fitViewport(props.viewport, props.getPoints(features))
: props.viewport;
const { width, height, formData } = props;
let { viewport } = props;
if (formData.autozoom) {
viewport = fitViewport(viewport, {
width,
height,
points: props.getPoints(features),
});
}

return {
start,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import { isEqual } from 'lodash';

import DeckGLContainer from './DeckGLContainer';
import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
import { fitViewport } from './layers/common';
// eslint-disable-next-line import/extensions
import fitViewport from './utils/fitViewport';

const propTypes = {
formData: PropTypes.object.isRequired,
Expand All @@ -48,10 +49,17 @@ export function createDeckGLComponent(getLayer, getPoints) {
class Component extends React.PureComponent {
constructor(props) {
super(props);
const originalViewport = props.viewport;
const viewport = props.formData.autozoom
? fitViewport(originalViewport, getPoints(props.payload.data.features))
: originalViewport;

const { width, height, formData } = props;
let { viewport } = props;
if (formData.autozoom) {
viewport = fitViewport(viewport, {
width,
height,
points: getPoints(props.payload.data.features),
});
}

this.state = {
viewport,
layer: this.computeLayer(props),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ import Legend from '../../components/Legend';
import TooltipRow from '../../TooltipRow';
import { getBuckets, getBreakPointColorScaler } from '../../utils';

import { commonLayerProps, fitViewport } from '../common';
import { commonLayerProps } from '../common';
import { getPlaySliderParams } from '../../utils/time';
import sandboxedEval from '../../utils/sandbox';
// eslint-disable-next-line import/extensions
import getPointsFromPolygon from '../../utils/getPointsFromPolygon';
// eslint-disable-next-line import/extensions
import fitViewport from '../../utils/fitViewport';

const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds

function getPoints(features) {
return features.flatMap(d => d.polygon);
}

function getElevation(d, colorScaler) {
/* in deck.gl 5.3.4 (used in Superset as of 2018-10-24), if a polygon has
* opacity zero it will make everything behind it have opacity zero,
Expand Down Expand Up @@ -114,7 +114,7 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o
pickable: true,
filled: fd.filled,
stroked: fd.stroked,
getPolygon: d => d.polygon,
getPolygon: getPointsFromPolygon,
getFillColor: colorScaler,
getLineColor: [sc.r, sc.g, sc.b, 255 * sc.a],
getLineWidth: fd.line_width,
Expand Down Expand Up @@ -154,26 +154,32 @@ class DeckGLPolygon extends React.Component {
}

static getDerivedStateFromProps(props, state) {
const { width, height, formData, payload } = props;

// the state is computed only from the payload; if it hasn't changed, do
// not recompute state since this would reset selections and/or the play
// slider position due to changes in form controls
if (state && props.payload.form_data === state.formData) {
if (state && payload.form_data === state.formData) {
return null;
}

const features = props.payload.data.features || [];
const features = payload.data.features || [];
const timestamps = features.map(f => f.__timestamp);

// the granularity has to be read from the payload form_data, not the
// props formData which comes from the instantaneous controls state
const granularity =
props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D';
const granularity = payload.form_data.time_grain_sqla || payload.form_data.granularity || 'P1D';

const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);

const viewport = props.formData.autozoom
? fitViewport(props.viewport, getPoints(features))
: props.viewport;
let { viewport } = props;
if (formData.autozoom) {
viewport = fitViewport(viewport, {
width,
height,
points: features.flatMap(getPointsFromPolygon),
});
}

return {
start,
Expand All @@ -184,7 +190,7 @@ class DeckGLPolygon extends React.Component {
viewport,
selected: [],
lastClick: 0,
formData: props.payload.form_data,
formData: payload.form_data,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import { t } from '@superset-ui/translation';
import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
import { getPlaySliderParams } from '../../utils/time';
import sandboxedEval from '../../utils/sandbox';
import { commonLayerProps, fitViewport } from '../common';
import { commonLayerProps } from '../common';
import TooltipRow from '../../TooltipRow';
// eslint-disable-next-line import/extensions
import fitViewport from '../../utils/fitViewport';

function getPoints(data) {
return data.map(d => d.position);
Expand Down Expand Up @@ -123,10 +125,16 @@ class DeckGLScreenGrid extends React.PureComponent {
props.payload.form_data.time_grain_sqla || props.payload.form_data.granularity || 'P1D';

const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, granularity);

const viewport = props.formData.autozoom
? fitViewport(props.viewport, getPoints(features))
: props.viewport;
const { width, height, formData } = props;

let { viewport } = props;
if (formData.autozoom) {
viewport = fitViewport(viewport, {
width,
height,
points: getPoints(features),
});
}

return {
start,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,77 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { fitBounds } from 'viewport-mercator-project';
import * as d3array from 'd3-array';
import sandboxedEval from '../utils/sandbox';

const PADDING = 0.25;
const GEO_BOUNDS = {
LAT_MAX: 90,
LAT_MIN: -90,
LNG_MAX: 180,
LNG_MIN: -180,
};

/**
* Get the latitude bounds if latitude is a single coordinate
* @param latExt Latitude range
*/
function getLatBoundsForSingleCoordinate(latExt) {
const latMin =
latExt[0] - PADDING < GEO_BOUNDS.LAT_MIN ? GEO_BOUNDS.LAT_MIN : latExt[0] - PADDING;
const latMax =
latExt[1] + PADDING > GEO_BOUNDS.LAT_MAX ? GEO_BOUNDS.LAT_MAX : latExt[1] + PADDING;

return [latMin, latMax];
}

/**
* Get the longitude bounds if longitude is a single coordinate
* @param lngExt Longitude range
*/
function getLngBoundsForSingleCoordinate(lngExt) {
const lngMin =
lngExt[0] - PADDING < GEO_BOUNDS.LNG_MIN ? GEO_BOUNDS.LNG_MIN : lngExt[0] - PADDING;
const lngMax =
lngExt[1] + PADDING > GEO_BOUNDS.LNG_MAX ? GEO_BOUNDS.LNG_MAX : lngExt[1] + PADDING;

return [lngMin, lngMax];
}

export function getBounds(points) {
const latExt = d3array.extent(points, d => d[1]);
const lngExt = d3array.extent(points, d => d[0]);
const latBounds = latExt[0] === latExt[1] ? getLatBoundsForSingleCoordinate(latExt) : latExt;
const lngBounds = lngExt[0] === lngExt[1] ? getLngBoundsForSingleCoordinate(lngExt) : lngExt;

return [
[lngBounds[0], latBounds[0]],
[lngBounds[1], latBounds[1]],
];
}

export function fitViewport(viewport, points, padding = 10) {
try {
const bounds = getBounds(points);

return {
...viewport,
...fitBounds({
bounds,
height: viewport.height,
padding,
width: viewport.width,
}),
};
} catch (error) {
/* eslint no-console: 0 */
console.error('Could not auto zoom', error);

return viewport;
}
}

export function commonLayerProps(formData, setTooltip, setTooltipContent, onSelect) {
const fd = formData;
let onHover;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { extent as d3Extent } from 'd3-array';
import { Point, Range } from './types';

const LAT_LIMIT: Range = [-90, 90];
const LNG_LIMIT: Range = [-180, 180];

/**
* Expand a coordinate range by `padding` and within limits, if needed
*/
function expandIfNeeded([curMin, curMax]: Range, [minBound, maxBound]: Range, padding = 0.25) {
return curMin < curMax
? [curMin, curMax]
: [Math.max(minBound, curMin - padding), Math.min(maxBound, curMax + padding)];
}

export default function computeBoundsFromPoints(points: Point[]) {
const latBounds = expandIfNeeded(d3Extent(points, (x: Point) => x[1]) as Range, LAT_LIMIT);
const lngBounds = expandIfNeeded(d3Extent(points, (x: Point) => x[0]) as Range, LNG_LIMIT);
return [
[lngBounds[0], latBounds[0]],
[lngBounds[1], latBounds[1]],
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { fitBounds } from '@math.gl/web-mercator';
import computeBoundsFromPoints from './computeBoundsFromPoints';
import { Point } from './types';

type Viewport = {
longtitude: number;
latitude: number;
zoom: number;
bearing?: number;
pitch?: number;
};

type FitViewportOptions = {
points: Point[];
width: number;
height: number;
minExtent?: number;
maxZoom?: number;
offset?: [number, number];
padding?: number;
};

export default function fitViewport(
originalViewPort: Viewport,
{ points, width, height, minExtent, maxZoom, offset, padding = 20 }: FitViewportOptions,
) {
const { bearing, pitch } = originalViewPort;
const bounds = computeBoundsFromPoints(points);

try {
return {
...fitBounds({
bounds,
width,
height,
minExtent,
maxZoom,
offset,
padding,
}),
bearing,
pitch,
};
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not fit viewport', error);
}

return originalViewPort;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Point } from './types';

/** Format originally used by the Polygon plugin */
type CustomPolygonFeature = {
polygon: Point[];
};

/**
* Format that is geojson standard
* https://geojson.org/geojson-spec.html
*/
type GeojsonPolygonFeature = {
polygon: {
type: 'Feature';
geometry: {
type: 'Polygon';
coordinates: Point[][];
};
};
};

export default function getPointsFromPolygon(
feature: CustomPolygonFeature | GeojsonPolygonFeature,
) {
return 'geometry' in feature.polygon ? feature.polygon.geometry.coordinates[0] : feature.polygon;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// range and point actually have different value ranges
// and also are different concept-wise

export type Range = [number, number];
export type Point = [number, number];
Loading

0 comments on commit 940e449

Please sign in to comment.