Skip to content

Commit

Permalink
feat(measure-distance-mode): Add multipoint support (#504)
Browse files Browse the repository at this point in the history
* feat(measure-distance-mode): Add multipoint support

* fix(measure-distance-mode): Rearrange impors order to fix build

* Fix(measure-distance-mode): Change code according to eslint to fix build

* fix: Add tests to cover new measure distance mode

* fix(tests): Skip tests that cannot run without headless-gl

* chore: Fix build
  • Loading branch information
andrei-kucherov committed Jan 22, 2021
1 parent e7d5903 commit 8c86eca
Show file tree
Hide file tree
Showing 15 changed files with 2,349 additions and 2,193 deletions.
16 changes: 11 additions & 5 deletions examples/advanced/src/example.tsx
Expand Up @@ -564,9 +564,13 @@ export default class Example extends React.Component<
this.state.modeConfig.turfOptions.units) ||
'kilometers'
}
onChange={(event) =>
this.setState({ modeConfig: { turfOptions: { units: event.target.value } } })
}
onChange={(event) => {
const modeConfig = {
...this.state.modeConfig,
turfOptions: { units: event.target.value },
};
this.setState({ modeConfig });
}}
>
<option value="kilometers">kilometers</option>
<option value="miles">miles</option>
Expand Down Expand Up @@ -744,8 +748,10 @@ export default class Example extends React.Component<
}

renderStaticMap(viewport: Record<string, any>) {
// @ts-ignore
return <StaticMap {...viewport} mapStyle={'mapbox://styles/mapbox/dark-v10'} />;
return (
// @ts-ignore
<StaticMap {...viewport} mapStyle={'mapbox://styles/mapbox/dark-v10'} />
);
}

_featureMenuClick(action: string) {
Expand Down
7 changes: 4 additions & 3 deletions jest.config.js
@@ -1,8 +1,9 @@
require("core-js/stable");
require('core-js/stable');

module.exports = {
verbose: true,
testURL: 'http://localhost/',
collectCoverageFrom: ['modules/*/src/**/*.{ts,tsx}', '!**/node_modules/**'],
testPathIgnorePatterns: ['/node_modules/', '/lib/', '/website/'],
"preset": "ts-jest"
testPathIgnorePatterns: ['/node_modules/', '/website/'],
preset: 'ts-jest',
};
3 changes: 2 additions & 1 deletion modules/edit-modes/package.json
Expand Up @@ -43,7 +43,8 @@
"test-node": "(cd ../.. && node test/node.js)",
"test-dist": "(cd ../.. && node test/node-dist.js)",
"test-browser": "webpack-dev-server --env.test --progress --hot --open",
"jest": "(cd ../.. && jest src)",
"jest": "(cd ../.. && jest modules/edit-modes)",
"jestWatch": "(cd ../.. && jest modules/edit-modes --watch)",
"bench": "node test/bench/node.js",
"bench-browser": "webpack-dev-server --env.bench --progress --hot --open",
"test-rendering": "(cd test/rendering-test && webpack-dev-server --config webpack.config.test-rendering.js --progress --hot --open)"
Expand Down
258 changes: 156 additions & 102 deletions modules/edit-modes/src/lib/measure-distance-mode.ts
@@ -1,137 +1,191 @@
// @ts-ignore
import turfDistance from '@turf/distance';
// @ts-ignore
import memoize from '../memoize';
import {
ClickEvent,
PointerMoveEvent,
Tooltip,
ModeProps,
GuideFeatureCollection,
EditHandleFeature,
} from '../types';
import { FeatureCollection, Position } from '../geojson-types';
import { FeatureCollection } from '../geojson-types';
import { ClickEvent, PointerMoveEvent, ModeProps, GuideFeatureCollection, Tooltip } from '../types';
import { getPickedEditHandle } from '../utils';
import { GeoJsonEditMode } from './geojson-edit-mode';

const DEFAULT_TOOLTIPS = [];

export class MeasureDistanceMode extends GeoJsonEditMode {
startingPoint: Readonly<EditHandleFeature> | null | undefined = null;
endingPoint: Readonly<EditHandleFeature> | null | undefined = null;
endingPointLocked = false;
_isMeasuringSessionFinished = false;
_currentTooltips = [];
_currentDistance = 0;

_setEndingPoint(mapCoords: Position) {
this.endingPoint = {
type: 'Feature',
properties: {
guideType: 'editHandle',
editHandleType: 'existing',
featureIndex: -1,
positionIndexes: [],
},
geometry: {
type: 'Point',
coordinates: mapCoords,
},
};
}
_calculateDistanceForTooltip = ({ positionA, positionB, modeConfig }) => {
const { turfOptions, measurementCallback } = modeConfig || {};
const distance = turfDistance(positionA, positionB, turfOptions);

_getTooltips = memoize(({ modeConfig, startingPoint, endingPoint }) => {
let tooltips = DEFAULT_TOOLTIPS;
if (measurementCallback) {
measurementCallback(distance);
}

if (startingPoint && endingPoint) {
const { formatTooltip, turfOptions, measurementCallback } = modeConfig || {};
const units = (turfOptions && turfOptions.units) || 'kilometers';
return distance;
};

const distance = turfDistance(startingPoint, endingPoint, turfOptions);
_formatTooltip(distance, modeConfig?) {
const { formatTooltip, turfOptions } = modeConfig || {};
const units = (turfOptions && turfOptions.units) || 'kilometers';

let text;
if (formatTooltip) {
text = formatTooltip(distance);
} else {
// By default, round to 2 decimal places and append units
text = `${parseFloat(distance).toFixed(2)} ${units}`;
}
let text;
if (formatTooltip) {
text = formatTooltip(distance);
} else {
// By default, round to 2 decimal places and append units
text = `${parseFloat(distance).toFixed(2)} ${units}`;
}

if (measurementCallback) {
measurementCallback(distance);
}
return text;
}

tooltips = [
{
position: endingPoint.geometry.coordinates,
text,
},
];
handleClick(event: ClickEvent, props: ModeProps<FeatureCollection>) {
const { modeConfig, data, onEdit } = props;

// restart measuring session
if (this._isMeasuringSessionFinished) {
this._isMeasuringSessionFinished = false;
this.resetClickSequence();
this._currentTooltips = [];
this._currentDistance = 0;
}

return tooltips;
});
const { picks } = event;
const clickedEditHandle = getPickedEditHandle(picks);

handleClick(event: ClickEvent, props: ModeProps<FeatureCollection>): void {
if (!this.startingPoint || this.endingPointLocked) {
this.startingPoint = {
type: 'Feature',
properties: {
guideType: 'editHandle',
editHandleType: 'existing',
featureIndex: -1,
positionIndexes: [],
},
geometry: {
type: 'Point',
coordinates: event.mapCoords,
let positionAdded = false;
if (!clickedEditHandle) {
// Don't add another point right next to an existing one
this.addClickSequence(event);
positionAdded = true;
}
const clickSequence = this.getClickSequence();

if (
clickSequence.length > 1 &&
clickedEditHandle &&
Array.isArray(clickedEditHandle.properties.positionIndexes) &&
clickedEditHandle.properties.positionIndexes[0] === clickSequence.length - 1
) {
// They clicked the last point (or double-clicked), so add the LineString
this._isMeasuringSessionFinished = true;
} else if (positionAdded) {
if (clickSequence.length > 1) {
this._currentDistance += this._calculateDistanceForTooltip({
positionA: clickSequence[clickSequence.length - 2],
positionB: clickSequence[clickSequence.length - 1],
modeConfig,
});
this._currentTooltips.push({
position: event.mapCoords,
text: this._formatTooltip(this._currentDistance, modeConfig),
});
}

// new tentative point
onEdit({
// data is the same
updatedData: data,
editType: 'addTentativePosition',
editContext: {
position: event.mapCoords,
},
};
this.endingPoint = null;
this.endingPointLocked = false;
} else if (this.startingPoint) {
this._setEndingPoint(event.mapCoords);
this.endingPointLocked = true;
});
}
}

// Called when the pointer moved, regardless of whether the pointer is down, up, and whether something was picked
handlePointerMove(event: PointerMoveEvent, props: ModeProps<FeatureCollection>): void {
if (this.startingPoint && !this.endingPointLocked) {
this._setEndingPoint(event.mapCoords);
handleKeyUp(event: KeyboardEvent, props: ModeProps<FeatureCollection>) {
if (this._isMeasuringSessionFinished) return;

event.stopPropagation();
const { key } = event;

const clickSequenceLength = this.getClickSequence().length;

switch (key) {
case 'Escape':
this._isMeasuringSessionFinished = true;
if (clickSequenceLength === 1) {
this.resetClickSequence();
this._currentTooltips = [];
}
// force update drawings
props.onUpdateCursor('cell');
break;
case 'Enter':
this.handleClick(props.lastPointerMoveEvent, props);
this._isMeasuringSessionFinished = true;
break;
default:
break;
}

props.onUpdateCursor('cell');
}

// Return features that can be used as a guide for editing the data
getGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection {
const guides: GuideFeatureCollection = { type: 'FeatureCollection', features: [] };
const { features } = guides;
const { lastPointerMoveEvent } = props;
const clickSequence = this.getClickSequence();

if (this.startingPoint) {
features.push(this.startingPoint);
}
if (this.endingPoint) {
features.push(this.endingPoint);
}
if (this.startingPoint && this.endingPoint) {
features.push({
const lastCoords =
lastPointerMoveEvent && !this._isMeasuringSessionFinished
? [lastPointerMoveEvent.mapCoords]
: [];

const guides = {
type: 'FeatureCollection',
features: [],
};

if (clickSequence.length > 0) {
guides.features.push({
type: 'Feature',
properties: { guideType: 'tentative' },
properties: {
guideType: 'tentative',
},
geometry: {
type: 'LineString',
coordinates: [
this.startingPoint.geometry.coordinates,
this.endingPoint.geometry.coordinates,
],
coordinates: [...clickSequence, ...lastCoords],
},
});
}

const editHandles = clickSequence.map((clickedCoord, index) => ({
type: 'Feature',
properties: {
guideType: 'editHandle',
editHandleType: 'existing',
featureIndex: -1,
positionIndexes: [index],
},
geometry: {
type: 'Point',
coordinates: clickedCoord,
},
}));

guides.features.push(...editHandles);
// @ts-ignore
return guides;
}

handlePointerMove(event: PointerMoveEvent, props: ModeProps<FeatureCollection>) {
props.onUpdateCursor('cell');
}

getTooltips(props: ModeProps<FeatureCollection>): Tooltip[] {
return this._getTooltips({
modeConfig: props.modeConfig,
startingPoint: this.startingPoint,
endingPoint: this.endingPoint,
});
const { lastPointerMoveEvent, modeConfig } = props;
const positions = this.getClickSequence();

if (positions.length > 0 && lastPointerMoveEvent && !this._isMeasuringSessionFinished) {
const distance = this._calculateDistanceForTooltip({
positionA: positions[positions.length - 1],
positionB: lastPointerMoveEvent.mapCoords,
modeConfig: props.modeConfig,
});
return [
...this._currentTooltips,
{
position: lastPointerMoveEvent.mapCoords,
text: this._formatTooltip(this._currentDistance + distance, modeConfig),
},
];
}

return this._currentTooltips;
}
}

0 comments on commit 8c86eca

Please sign in to comment.