diff --git a/lib/editor/components/map/pattern-debug-lines.js b/lib/editor/components/map/pattern-debug-lines.js
index 99da76d6c..db0ee930d 100644
--- a/lib/editor/components/map/pattern-debug-lines.js
+++ b/lib/editor/components/map/pattern-debug-lines.js
@@ -5,8 +5,8 @@ import {Polyline} from 'react-leaflet'
import lineDistance from 'turf-line-distance'
import lineString from 'turf-linestring'
-import {POINT_TYPE} from '../../constants'
-import {isValidPoint} from '../../util/map'
+import {PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS} from '../../constants'
+import {getStopControlPoints} from '../../util/map'
import type {ControlPoint, GtfsStop, Pattern} from '../../../types'
import type {EditSettingsState} from '../../../types/reducers'
@@ -20,8 +20,6 @@ type Props = {
stops: Array
}
-const DISTANCE_THRESHOLD = 50
-
/**
* This react-leaflet component draws connecting lines between a pattern
* geometry's anchor points (that are associated with stops) and their
@@ -44,33 +42,33 @@ export default class PatternDebugLines extends PureComponent {
// i.e., whether the line should be rendered.
.map((cp, index) => ({...cp, cpIndex: index}))
// Filter out the user-added anchors
- .filter(cp => cp.pointType === POINT_TYPE.STOP)
+ .filter(getStopControlPoints)
// The remaining number should match the number of stops
.map((cp, index) => {
const {cpIndex, point, stopId} = cp
- if (!isValidPoint(point)) {
- return null
- }
+ // If hiding inactive segments (and this control point is not along
+ // a visible segment), do not show debug line.
if (editSettings.hideInactiveSegments && (cpIndex > patternSegment + 1 || cpIndex < patternSegment - 1)) {
return null
}
const patternStopIsActive = patternStop.index === index
// Do not render if some other pattern stop is active
- // $FlowFixMe
- if ((patternStop.index || patternStop.index === 0) && !patternStopIsActive) {
+ if (typeof patternStop.index === 'number' && !patternStopIsActive) {
return null
}
const {coordinates: cpCoord} = point.geometry
+ // Find stop entity for control point.
const stop = stops.find(s => s.stop_id === stopId)
if (!stop) {
- // console.warn(`Could not find stop for pattern stop index=${index} patternStop#stopId=${stopId}`)
+ // If no stop entity found, do not attempt to draw a line to the
+ // missing stop.
return null
}
const coordinates = [[cpCoord[1], cpCoord[0]], [stop.stop_lat, stop.stop_lon]]
const distance: number = lineDistance(lineString(coordinates), 'meters')
- const distanceGreaterThanThreshold = distance > DISTANCE_THRESHOLD
+ const distanceGreaterThanThreshold = distance > PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS
if (distanceGreaterThanThreshold) {
- console.warn(`Distance from pattern stop index=${index} to projected point is greater than ${DISTANCE_THRESHOLD} (${distance}).`)
+ console.warn(`Distance from pattern stop index=${index} to projected point is greater than ${PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS} (${distance}).`)
}
return (
{
+ _addStop = (index?: number) => {
+ const {activePattern, addStopToPattern, stop} = this.props
+ addStopToPattern(activePattern, stop, index)
+ }
+
+ _matchesStopAtIndex = (index: number) => {
+ const {activePattern, stop} = this.props
+ const patternStopAtIndex = activePattern.patternStops[index]
+ return patternStopAtIndex && patternStopAtIndex.stopId === stop.stop_id
+ }
+
+ _onAddToEnd = () => this._addStop()
+
+ _onSelectStop = (key: number) => this._addStop(key)
+
+ render () {
+ const {activePattern, index, label, size, style} = this.props
+ const {patternStops} = activePattern
+ const lastIndex = patternStops.length - 1
+ // Check that first/last stop is not already set to this stop.
+ let addToEndDisabled = this._matchesStopAtIndex(lastIndex)
+ let addToBeginningDisabled = this._matchesStopAtIndex(0)
+ // Also, disable end/beginning if the current pattern stop being viewed
+ // occupies one of these positions.
+ if (typeof index === 'number') {
+ addToEndDisabled = addToEndDisabled || index >= lastIndex
+ addToBeginningDisabled = addToBeginningDisabled || index === 0
+ }
+ return (
+
+
+
+
+
+ {activePattern.patternStops && activePattern.patternStops.map((s, i) => {
+ // addIndex is in "reverse" order
+ const addIndex = activePattern.patternStops.length - i
+ let disableAdjacent = false
+ // If showing for current pattern stop, do not allow adding as an
+ // adjacent stop.
+ if (typeof index === 'number') {
+ disableAdjacent = (index >= addIndex - 2 && index < addIndex)
+ }
+ // Disable adding stop to current position or directly before/after
+ // current position
+ const addAtIndexDisabled = disableAdjacent ||
+ this._matchesStopAtIndex(addIndex - 2) ||
+ this._matchesStopAtIndex(addIndex - 1)
+ // Skip MenuItem index is the same as the pattern stop index
+ if (index === addIndex - 1 || addIndex === 1) {
+ return null
+ }
+ return (
+
+ )
+ })}
+
+
+
+ )
+ }
+}
diff --git a/lib/editor/components/pattern/EditShapePanel.js b/lib/editor/components/pattern/EditShapePanel.js
index 2b67a033d..354edb338 100644
--- a/lib/editor/components/pattern/EditShapePanel.js
+++ b/lib/editor/components/pattern/EditShapePanel.js
@@ -2,13 +2,15 @@
import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
-import {Button, ButtonGroup, ButtonToolbar, OverlayTrigger, Tooltip} from 'react-bootstrap'
+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} from '../../constants'
+import {ARROW_MAGENTA, PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS} from '../../constants'
import * as tripPatternActions from '../../actions/tripPattern'
import OptionButton from '../../../common/components/OptionButton'
import EditSettings from './EditSettings'
@@ -17,6 +19,7 @@ import {polyline as getPolyline} from '../../../scenario-editor/utils/valhalla'
import {
controlPointsFromSegments,
generateControlPointsFromPatternStops,
+ getStopControlPoints,
getPatternDistance
} from '../../util/map'
@@ -139,6 +142,42 @@ export default class EditShapePanel extends Component {
})
}
+ /**
+ * 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(getStopControlPoints)
+ .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()
@@ -188,6 +227,7 @@ export default class EditShapePanel extends Component {
const nextSegment = (!patternSegment && patternSegment !== 0)
? 0
: patternSegment + 1
+ const patternStopsWithShapeIssues = this._getPatternStopsWithShapeIssues()
return (
+ The stop(s) listed above are located
+ too far (max = {PATTERN_TO_STOP_DISTANCE_THRESHOLD_METERS}{' '}
+ meters) from the pattern shape.
+
+
+ This can be resolved by:
+
+
+ moving the stop itself closer to the street's edge;
+
+
+ changing where the stop is "snapped" to the shape: click{' '}
+ Edit pattern geometry, uncheck{' '}
+ Hide stop handles, and move the stop handle
+ closer to the stop. Checking Hide inactive segments{' '}
+ can help isolate the problematic stop handle; or
+
+
+ regenerating the shape from existing stops: click{' '}
+ From stops.
+
+
+
+
+ : null
+ }
+
{editSettings.editGeometry
?
diff --git a/lib/editor/components/pattern/PatternStopButtons.js b/lib/editor/components/pattern/PatternStopButtons.js
index 293fcf14f..3aa939aa2 100644
--- a/lib/editor/components/pattern/PatternStopButtons.js
+++ b/lib/editor/components/pattern/PatternStopButtons.js
@@ -2,13 +2,14 @@
import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
-import { Button, Dropdown, OverlayTrigger, Tooltip, ButtonGroup, MenuItem } from 'react-bootstrap'
+import { Button, OverlayTrigger, Tooltip, ButtonGroup } from 'react-bootstrap'
import * as activeActions from '../../actions/active'
import * as stopStrategiesActions from '../../actions/map/stopStrategies'
import * as tripPatternActions from '../../actions/tripPattern'
+import AddPatternStopDropdown from './AddPatternStopDropdown'
-import type {Feed, GtfsStop, Pattern, PatternStop} from '../../../types'
+import type {Feed, GtfsStop, Pattern, PatternStop, Style} from '../../../types'
type Props = {
activePattern: Pattern,
@@ -23,7 +24,7 @@ type Props = {
setActiveStop: typeof tripPatternActions.setActiveStop,
size: string,
stop: GtfsStop,
- style?: {[string]: number | string},
+ style?: Style,
updatePatternStops: typeof tripPatternActions.updatePatternStops
}
@@ -44,7 +45,13 @@ export default class PatternStopButtons extends Component {
_onClickRemove = () => {
const {activePattern, index, removeStopFromPattern, stop} = this.props
- removeStopFromPattern(activePattern, stop, index)
+ if (
+ window.confirm(
+ `Are you sure you would like to remove ${stop.stop_name} (${stop.stop_id}) as pattern stop #${index + 1}?`
+ )
+ ) {
+ removeStopFromPattern(activePattern, stop, index)
+ }
}
_onClickSave = () => this.props.saveActiveGtfsEntity('trippattern')
@@ -55,11 +62,16 @@ export default class PatternStopButtons extends Component {
}
render () {
- const {stop, index, activePattern, patternEdited, style, size} = this.props
- const {patternStops} = activePattern
- const lastIndex = patternStops.length - 1
- const addToEndDisabled = index >= lastIndex || patternStops[lastIndex].stopId === stop.id
- const addToBeginningDisabled = index === 0 || patternStops[0].stopId === stop.id
+ const {
+ activePattern,
+ addStopToPattern,
+ index,
+ patternEdited,
+ patternStop,
+ size,
+ stop,
+ style
+ } = this.props
return (
{
onClick={this._onClickSave}>
- Edit stop}>
+ Edit stop}
+ >
- Remove from pattern}>
+ Remove stop from pattern
+ }
+ >
-
-
-
-
-
- {activePattern.patternStops && activePattern.patternStops.map((s, i) => {
- // addIndex is in "reverse" order
- const addIndex = activePattern.patternStops.length - i
- const addAtIndexDisabled = (index >= addIndex - 2 && index < addIndex) ||
- (patternStops[addIndex - 2] && patternStops[addIndex - 2].stopId === stop.id) ||
- (patternStops[addIndex - 1] && patternStops[addIndex - 1].stopId === stop.id)
- // (patternStops[addIndex + 1] && patternStops[addIndex + 1].stopId === stop.id)
- // skip MenuItem index is the same as the pattern stop index
- if (index === addIndex - 1 || addIndex === 1) {
- return null
- }
- // disable adding stop to current position or directly before/after current position
- return (
-
- )
- })}
-
-
-
+
)
}
diff --git a/lib/editor/components/pattern/PatternStopsPanel.js b/lib/editor/components/pattern/PatternStopsPanel.js
index 320fd8f4d..1192ecda8 100644
--- a/lib/editor/components/pattern/PatternStopsPanel.js
+++ b/lib/editor/components/pattern/PatternStopsPanel.js
@@ -8,9 +8,11 @@ import * as activeActions from '../../actions/active'
import * as mapActions from '../../actions/map'
import * as stopStrategiesActions from '../../actions/map/stopStrategies'
import * as tripPatternActions from '../../actions/tripPattern'
+import AddPatternStopDropdown from './AddPatternStopDropdown'
import NormalizeStopTimesModal from './NormalizeStopTimesModal'
import PatternStopContainer from './PatternStopContainer'
import VirtualizedEntitySelect from '../VirtualizedEntitySelect'
+import {getEntityBounds, getEntityName} from '../../util/gtfs'
import type {Pattern, GtfsStop, Feed, ControlPoint, Coordinates} from '../../../types'
import type {EditorStatus, EditSettingsUndoState, MapState} from '../../../types/reducers'
@@ -34,19 +36,26 @@ type Props = {
stops: Array,
updateActiveGtfsEntity: typeof activeActions.updateActiveGtfsEntity,
updateEditSetting: typeof activeActions.updateEditSetting,
+ updateMapSetting: typeof mapActions.updateMapSetting,
updatePatternGeometry: typeof mapActions.updatePatternGeometry,
updatePatternStops: typeof tripPatternActions.updatePatternStops
}
-type State = { showNormalizeStopTimesModal: boolean }
+type State = {
+ patternStopCandidate: ?GtfsStop,
+ showNormalizeStopTimesModal: boolean
+ }
export default class PatternStopsPanel extends Component {
state = {
- showNormalizeStopTimesModal: false
+ showNormalizeStopTimesModal: false,
+ patternStopCandidate: null
}
_toggleAddStopsMode = () => {
const {editSettings, updateEditSetting} = this.props
+ // Clear stop candidate (if defined).
+ this.setState({patternStopCandidate: null})
updateEditSetting({
setting: 'addStops',
value: !editSettings.present.addStops
@@ -58,12 +67,16 @@ export default class PatternStopsPanel extends Component {
_onCloseModal = () => this.setState({showNormalizeStopTimesModal: false})
- _addStopFromSelect = (input: any) => {
+ _selectStop = (input: any) => {
if (!input) {
+ // Clear stop candidate if input is cleared.
+ this.setState({patternStopCandidate: null})
return
}
const stop: GtfsStop = input.entity
- return this.props.addStopToPattern(this.props.activePattern, stop)
+ // Zoom to stop candidate
+ this.props.updateMapSetting({bounds: getEntityBounds(stop), target: +stop.id})
+ this.setState({patternStopCandidate: stop})
}
render () {
@@ -88,6 +101,7 @@ export default class PatternStopsPanel extends Component {
updatePatternStops
} = this.props
const {addStops} = editSettings.present
+ const {patternStopCandidate} = this.state
const patternHasStops = activePattern.patternStops &&
activePattern.patternStops.length > 0
return (
@@ -174,8 +188,24 @@ export default class PatternStopsPanel extends Component {
+ onChange={this._selectStop} />
+ {patternStopCandidate
+ ?