-
Notifications
You must be signed in to change notification settings - Fork 81
/
Copy pathEditShapePanel.js
381 lines (365 loc) · 14.1 KB
/
EditShapePanel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// @flow
import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
import {Alert, Button, ButtonGroup, ButtonToolbar, OverlayTrigger, Tooltip} from 'react-bootstrap'
import ll from '@conveyal/lonlat'
import numeral from 'numeral'
import lineDistance from 'turf-line-distance'
import lineString from 'turf-linestring'
import * as activeActions from '../../actions/active'
import * as mapActions from '../../actions/map'
import {ARROW_MAGENTA, PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS} from '../../constants'
import * as tripPatternActions from '../../actions/tripPattern'
import OptionButton from '../../../common/components/OptionButton'
import * as statusActions from '../../../manager/actions/status'
import {polyline as getPolyline} from '../../../scenario-editor/utils/valhalla'
import {
controlPointsFromSegments,
generateControlPointsFromPatternStops,
getPatternDistance,
isValidStopControlPoint
} from '../../util/map'
import type {ControlPoint, LatLng, Pattern, GtfsStop} from '../../../types'
import type {EditSettingsUndoState} from '../../../types/reducers'
import EditSettings from './EditSettings'
type Props = {
activePattern: Pattern,
controlPoints: Array<ControlPoint>,
editSettings: EditSettingsUndoState,
patternSegment: number,
resetActiveGtfsEntity: typeof activeActions.resetActiveGtfsEntity,
saveActiveGtfsEntity: typeof activeActions.saveActiveGtfsEntity,
setActivePatternSegment: typeof tripPatternActions.setActivePatternSegment,
setErrorMessage: typeof statusActions.setErrorMessage,
showConfirmModal: any,
stops: Array<GtfsStop>,
togglePatternEditing: typeof tripPatternActions.togglePatternEditing,
undoActiveTripPatternEdits: typeof tripPatternActions.undoActiveTripPatternEdits,
updateActiveGtfsEntity: typeof activeActions.updateActiveGtfsEntity,
updateEditSetting: typeof activeActions.updateEditSetting,
updatePatternGeometry: typeof mapActions.updatePatternGeometry,
}
export default class EditShapePanel extends Component<Props> {
/**
* Construct new pattern geometry from the pattern stop locations.
*/
async drawPatternFromStops (pattern: Pattern, stopsCoordinates: Array<LatLng>): Promise<any> {
const {editSettings, saveActiveGtfsEntity, setErrorMessage, updatePatternGeometry} = this.props
let patternSegments = []
if (editSettings.present.snapToOption !== 'NONE') {
patternSegments = await getPolyline(stopsCoordinates, true, editSettings.present.avoidMotorways, editSettings.present.snapToOption)
} else {
// Construct straight-line segments using stop coordinates
stopsCoordinates
.forEach((stop, i) => {
if (i < stopsCoordinates.length - 1) {
const segment = [ll.toCoordinates(stop), ll.toCoordinates(stopsCoordinates[i + 1])]
patternSegments.push(segment)
}
})
}
if (patternSegments && patternSegments.length > 0) {
const controlPoints = controlPointsFromSegments(pattern.patternStops, patternSegments)
updatePatternGeometry({
controlPoints,
patternSegments
})
saveActiveGtfsEntity('trippattern')
return true
} else {
setErrorMessage({message: 'Error drawing pattern from stops! Some stops may be unreachable by streets.'})
return false
}
}
_cancelEdits = () => {
const {activePattern, resetActiveGtfsEntity, togglePatternEditing} = this.props
if (this._hasEdits()) {
if (!window.confirm('You have unsaved shape edits. Are you sure you want to cancel and revert these changes?')) {
return
}
}
togglePatternEditing()
resetActiveGtfsEntity({
component: 'trippattern',
entity: activePattern
})
}
_generateShapeFromStops = () => {
const {activePattern, stops} = this.props
const stopLocations = stops && activePattern.patternStops && activePattern.patternStops.length
? activePattern.patternStops
.map((s, index) => {
const stop = stops.find(st => st.stop_id === s.stopId)
if (!stop) {
console.warn(`Could not locate stop with stop_id=${s.stopId}`)
return {lng: 0, lat: 0}
}
return {lng: stop.stop_lon, lat: stop.stop_lat}
})
: []
this.drawPatternFromStops(activePattern, stopLocations)
}
_confirmCreateFromStops = () => {
const title = 'Create pattern shape from stops?'
const onConfirm = this._generateShapeFromStops
const body = this._hasShapePoints()
? 'Are you sure you want to overwrite the existing shape for this trip pattern?'
: 'Are you sure you want to create an auto-generated shape for this trip pattern?'
this.props.showConfirmModal({title, body, onConfirm})
}
_deleteShape = () => {
const {
activePattern,
saveActiveGtfsEntity,
showConfirmModal,
stops,
updateActiveGtfsEntity,
updatePatternGeometry
} = this.props
const shapeId = activePattern.shapeId || '(undefined)'
showConfirmModal({
title: `Delete shape for trip pattern?`,
body: `Are you sure you would like to delete this trip pattern shape (shape_id: ${shapeId})?`,
onConfirm: () => {
// FIXME: Do we need to update pattern geometry, too?
updatePatternGeometry(generateControlPointsFromPatternStops(activePattern.patternStops, stops))
updateActiveGtfsEntity({
component: 'trippattern',
entity: activePattern,
props: {shapePoints: [], shapeId: null}
})
saveActiveGtfsEntity('trippattern')
}
})
}
/**
* Checks the control points for stop control points that are located too far
* from the actual stop location. This is used to give instructions to the
* user on resolving the issue.
*/
_getPatternStopsWithShapeIssues = () => {
const {controlPoints, stops} = this.props
return controlPoints
.filter(isValidStopControlPoint)
.map((controlPoint, index) => {
const {point, stopId} = controlPoint
let exceedsThreshold = false
const {coordinates: cpCoord} = point.geometry
// Find stop entity for control point.
const stop = stops.find(s => s.stop_id === stopId)
if (!stop) {
// If no stop entity found, do not attempt to draw a line to the
// missing stop.
return {controlPoint, index, stop: null, distance: 0, exceedsThreshold}
}
const coordinates = [[cpCoord[1], cpCoord[0]], [stop.stop_lat, stop.stop_lon]]
const distance: number = lineDistance(lineString(coordinates), 'meters')
exceedsThreshold = distance > PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS
return {
controlPoint,
distance,
exceedsThreshold,
index,
stop
}
})
// TODO: This can be removed if at some point we need to show stops where
// the distance threshold is not exceeded.
.filter(item => item.exceedsThreshold)
}
_beginEditing = () => {
const {togglePatternEditing} = this.props
togglePatternEditing()
}
_hasShapePoints = () => this.props.activePattern.shapePoints &&
this.props.activePattern.shapePoints.length > 0
save = () => {
const {editSettings, saveActiveGtfsEntity, updateEditSetting} = this.props
saveActiveGtfsEntity('trippattern')
// $FlowFixMe action is actually wrapped in promise when connected
.then(() => updateEditSetting({
setting: 'editGeometry',
value: !editSettings.present.editGeometry
}))
}
_hasEdits = () => this.props.editSettings.past.length > 0
render () {
const {
activePattern,
controlPoints, // FIXME use to describe which segment user is editing
patternSegment,
editSettings: editSettingsState,
setActivePatternSegment,
updateEditSetting,
undoActiveTripPatternEdits
} = this.props
const {present: editSettings} = editSettingsState
const hasEdits = this._hasEdits()
const fromStopsButton = <OverlayTrigger
placement='bottom'
overlay={
<Tooltip id='from-stops'>Generate pattern shape from stops</Tooltip>
}>
<Button
onClick={this._confirmCreateFromStops}
bsSize='small'
style={{width: '102px'}}>
<span><Icon type='map-marker' /> From stops</span>
</Button>
</OverlayTrigger>
const dist = getPatternDistance(activePattern, controlPoints)
const formattedShapeDistance = numeral(dist).format('0,0.00')
const nextSegment = (!patternSegment && patternSegment !== 0)
? 0
: patternSegment + 1
const patternStopsWithShapeIssues = this._getPatternStopsWithShapeIssues()
return (
<div>
<h4 className='line'>
Pattern shape
{' '}
({formattedShapeDistance} miles)
</h4>
<div style={{margin: '5px 0'}}>
{!activePattern.shapeId
? <small className='text-warning'>
<Icon type='exclamation-triangle' />{' '}
No shape associated with this pattern.
</small>
: <small>
<span className='overflow' style={{width: '250px'}}>
shape_id:{' '}
<span title={activePattern.shapeId}>{activePattern.shapeId}</span>
</span>
<Button
bsStyle='link'
bsSize='small'
style={{padding: '0 2px 10px 2px'}}
title='Delete shape for pattern'
onClick={this._deleteShape}>
<span className='text-danger'><Icon type='trash' /></span>
</Button>
</small>
}
</div>
{patternStopsWithShapeIssues.length > 0
? <Alert bsStyle='warning' style={{fontSize: 'small'}}>
<h4><Icon type='exclamation-triangle' /> Pattern stop snapping issue</h4>
<ul className='list-unstyled' style={{marginBottom: '5px'}}>
{patternStopsWithShapeIssues
.map(item => {
const {distance, index, stop} = item
if (!stop) return null
const roundedDist = Math.round(distance * 100) / 100
return (
<li key={index}>
#{index + 1} {stop.stop_name}{' '}
<span style={{color: 'red'}}>
{roundedDist} m
</span>
</li>
)
})
}
</ul>
<p>
The stop(s) listed above are located
too far (max = {PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS}{' '}
meters) from the pattern shape.
</p>
<p>
This can be resolved by:
<ol>
<li>
moving the stop itself closer to the street's edge;
</li>
<li>
changing where the stop is "snapped" to the shape: click{' '}
<strong>Edit pattern geometry</strong>, uncheck{' '}
<strong>Hide stop handles</strong>, and move the stop handle
closer to the stop. Checking <strong>Hide inactive segments</strong>{' '}
can help isolate the problematic stop handle; or
</li>
<li>
regenerating the shape from existing stops: click{' '}
<strong>From stops</strong>.
</li>
</ol>
</p>
</Alert>
: null
}
{editSettings.editGeometry
? <div>
<ButtonToolbar>
<Button
block
style={{width: '167px'}}
onClick={this._cancelEdits}
bsSize='small'>
<Icon type='ban' /> Cancel shape editing
</Button>
{fromStopsButton}
</ButtonToolbar>
<ButtonGroup style={{margin: '5px 0'}} block>
<OptionButton
onClick={setActivePatternSegment}
value={patternSegment - 1}
disabled={!patternSegment || patternSegment < 1}
bsSize='xsmall'>
<Icon type='caret-left' style={{color: 'blue'}} /> Prev
</OptionButton>
<OptionButton
onClick={setActivePatternSegment}
style={{minWidth: '165px', fontSize: '80%', padding: '2px 0'}}
disabled={patternSegment >= controlPoints.length - 1}
value={nextSegment}
bsSize='xsmall'>
{!patternSegment && patternSegment !== 0
? `Click line to begin editing`
: `Editing anchor ${patternSegment + 1} of ${controlPoints.length}`
}
</OptionButton>
<OptionButton
onClick={setActivePatternSegment}
className='pull-right'
value={nextSegment}
disabled={patternSegment >= controlPoints.length - 1}
bsSize='xsmall'>
Next <Icon type='caret-right' style={{color: ARROW_MAGENTA}} />
</OptionButton>
</ButtonGroup>
<ButtonToolbar>
<Button
bsSize='small'
disabled={!hasEdits}
onClick={this.save}>
<Icon type='check' /> Save
</Button>
<Button
bsSize='small'
disabled={!hasEdits}
onClick={undoActiveTripPatternEdits}>
<Icon type='undo' /> Undo
</Button>
</ButtonToolbar>
<EditSettings
editSettings={editSettings}
patternSegment={patternSegment}
updateEditSetting={updateEditSetting} />
</div>
: <ButtonToolbar>
<Button
onClick={this._beginEditing}
bsSize='small'
style={{width: '167px'}}
bsStyle='warning'>
<span><Icon type='pencil' /> Edit pattern geometry</span>
</Button>
{fromStopsButton}
</ButtonToolbar>
}
</div>
)
}
}