);
};
@@ -136,6 +188,31 @@ const NaviCardExtension = (
);
}
if (legType === LEGTYPE.MOVE && nextLeg?.transitLeg) {
+ if (showIndoorRoute) {
+ const indoorRouteSteps = getIndoorStepsWithVerticalTransportationUse(
+ previousLeg,
+ leg,
+ nextLeg,
+ );
+ return (
+
+ );
+ }
const { headsign, route, start } = nextLeg;
const hs = headsign || nextLeg.trip?.tripHeadsign;
const remainingDuration =
;
@@ -146,8 +223,9 @@ const NaviCardExtension = (
};
const routeMode = getRouteMode(route, config);
return (
-
- {stopInformation()}
+
+
+ {stopInformation(false, true)}
+
{stopInformation(true)}
- >
+
);
};
NaviCardExtension.propTypes = {
+ focusToPoint: PropTypes.func.isRequired,
+ previousLeg: legShape,
leg: legShape,
nextLeg: legShape,
legType: PropTypes.string,
time: PropTypes.number.isRequired,
platformUpdated: PropTypes.bool,
+ showIndoorRoute: PropTypes.bool,
+ toggleShowIndoorRoute: PropTypes.func.isRequired,
};
NaviCardExtension.defaultProps = {
legType: '',
+ previousLeg: undefined,
leg: undefined,
nextLeg: undefined,
platformUpdated: false,
+ showIndoorRoute: false,
};
NaviCardExtension.contextTypes = {
diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js
index 03dccacbfc..7b63b3bb76 100644
--- a/app/component/itinerary/navigator/NaviContainer.js
+++ b/app/component/itinerary/navigator/NaviContainer.js
@@ -25,6 +25,7 @@ const START_BUFFER = 120000; // 2 min in ms
function NaviContainer(
{
focusToLeg,
+ focusToPoint,
relayEnvironment,
setNavigation,
isNavigatorIntroDismissed,
@@ -154,6 +155,7 @@ function NaviContainer(
-
-
-
- {legDestination(intl, leg, null, nextLeg)}
-
-
- {displayDistance(tailLength, config, intl.formatNumber)}
-
- {nextLeg?.transitLeg && (
-
- )}
-
+ {!showIndoorRoute && (
+
+
+
+ {legDestination(intl, leg, null, nextLeg)}
+
+
+ {displayDistance(tailLength, config, intl.formatNumber)}
+
+ {nextLeg?.transitLeg && (
+
+ )}
+
+ )}
{nextLeg?.transitLeg && (
{
+ e.stopPropagation();
+ toggleShowIndoorRoute();
+ }}
+ onKeyPress={e => {
+ if (isKeyboardSelectionEvent(e)) {
+ e.stopPropagation();
+ toggleShowIndoorRoute();
+ }
+ }}
+ >
+ {showIndoorRoute ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+NaviIndoorRouteButton.propTypes = {
+ showIndoorRoute: PropTypes.bool,
+ toggleShowIndoorRoute: PropTypes.func.isRequired,
+};
+NaviIndoorRouteButton.defaultProps = {
+ showIndoorRoute: false,
+};
diff --git a/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteContainer.js b/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteContainer.js
new file mode 100644
index 0000000000..54d89d4420
--- /dev/null
+++ b/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteContainer.js
@@ -0,0 +1,89 @@
+import PropTypes from 'prop-types';
+import React, { useEffect, useState } from 'react';
+import { configShape } from '../../../../util/shapes';
+import { IndoorRouteStepType, VerticalDirection } from '../../../../constants';
+import NaviIndoorRouteStepInfo from './NaviIndoorRouteStepInfo';
+import { getStepFocusAction } from '../../../../util/indoorUtils';
+
+function NaviIndoorRouteContainer({ focusToPoint, indoorRouteSteps }) {
+ const [indoorBackgroundImageUrl, setIndoorBackgroundImageUrl] = useState();
+ useEffect(() => {
+ import(
+ /* webpackChunkName: "indoor-dotted-line" */ `../../../../configurations/images/default/indoor-dotted-line.svg`
+ ).then(insideImageUrl => {
+ setIndoorBackgroundImageUrl(`url(${insideImageUrl.default})`);
+ });
+ }, []);
+
+ return (
+
+
+
+ {indoorRouteSteps.map((step, i) => (
+
+
+
+
+
+ ))}
+
+
+
+
+ {indoorRouteSteps.map((step, i) => (
+
+ ))}
+
+
+ );
+}
+
+NaviIndoorRouteContainer.propTypes = {
+ focusToPoint: PropTypes.func.isRequired,
+ indoorRouteSteps: PropTypes.arrayOf(
+ PropTypes.shape({
+ type: PropTypes.oneOf(Object.values(IndoorRouteStepType)),
+ feature: PropTypes.shape({
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ to: PropTypes.shape({
+ name: PropTypes.string,
+ }),
+ }),
+ }),
+ ),
+};
+
+NaviIndoorRouteContainer.defaultProps = {
+ indoorRouteSteps: [],
+};
+
+NaviIndoorRouteContainer.contextTypes = {
+ config: configShape.isRequired,
+};
+
+export default NaviIndoorRouteContainer;
diff --git a/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteStepInfo.js b/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteStepInfo.js
new file mode 100644
index 0000000000..2219ba01ec
--- /dev/null
+++ b/app/component/itinerary/navigator/indoorroute/NaviIndoorRouteStepInfo.js
@@ -0,0 +1,66 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { configShape } from '../../../../util/shapes';
+import Icon from '../../../Icon';
+import {
+ getIndoorRouteTranslationId,
+ getVerticalTransportationUseIconId,
+} from '../../../../util/indoorUtils';
+import { IndoorRouteStepType, VerticalDirection } from '../../../../constants';
+import ItineraryMapAction from '../../ItineraryMapAction';
+import { isKeyboardSelectionEvent } from '../../../../util/browser';
+
+function NaviIndoorRouteStepInfo({
+ focusAction,
+ type,
+ verticalDirection,
+ toLevelName,
+}) {
+ const indoorTranslationId = getIndoorRouteTranslationId(
+ type,
+ verticalDirection,
+ toLevelName,
+ );
+
+ return (
+
isKeyboardSelectionEvent(e) && focusAction(e)}
+ >
+
+
+
+
+
+
+ );
+}
+
+NaviIndoorRouteStepInfo.propTypes = {
+ focusAction: PropTypes.func.isRequired,
+ type: PropTypes.oneOf(Object.values(IndoorRouteStepType)).isRequired,
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ toLevelName: PropTypes.string,
+};
+
+NaviIndoorRouteStepInfo.defaultProps = {
+ verticalDirection: undefined,
+ toLevelName: undefined,
+};
+
+NaviIndoorRouteStepInfo.contextTypes = {
+ config: configShape.isRequired,
+};
+
+export default NaviIndoorRouteStepInfo;
diff --git a/app/component/itinerary/navigator/navigator.scss b/app/component/itinerary/navigator/navigator.scss
index 8f79623522..d87d471c0e 100644
--- a/app/component/itinerary/navigator/navigator.scss
+++ b/app/component/itinerary/navigator/navigator.scss
@@ -138,18 +138,6 @@
display: flex;
align-self: flex-start;
margin-bottom: var(--space-xs);
-
- .expand {
- margin-left: var(--space-m);
- display: flex;
-
- .icon {
- margin-right: var(--space-s);
- margin-top: 5px;
- width: 16px;
- height: 16px;
- }
- }
}
.extension-routenumber,
@@ -195,8 +183,6 @@
flex-direction: column;
&.with-icon {
- margin-left: 40px;
-
.wait-duration {
margin-left: var(--space-l);
}
@@ -292,6 +278,7 @@
flex-direction: column;
width: 100%;
margin-right: var(--space-m);
+ justify-content: center;
&.expanded {
margin-bottom: 0;
@@ -313,148 +300,316 @@
display: flex;
}
- .extension {
+ .extension-container {
flex-direction: column;
transition: all 0.4s ease;
overflow-y: hidden;
- &.no-gap {
- margin-top: 0;
- margin-bottom: 0;
- }
+ .extension {
+ margin-left: calc(32px + var(--space-s));
- .extension-divider {
- height: 1px;
- background: #ddd;
- width: 85%;
- margin-left: 35px;
- margin-top: var(--space-s);
- margin-bottom: var(--space-s);
- }
+ &.no-vertical-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
- .stop-count {
- display: flex;
- margin-left: 35px;
+ .extension-divider {
+ height: 1px;
+ background: #ddd;
+ width: 95%;
+ margin-top: var(--space-s);
+ margin-bottom: var(--space-s);
+ }
- .icon-container {
+ .stop-count {
display: flex;
- align-items: center;
- .icon {
- height: 16px;
- width: 16px;
+ .icon-container {
+ display: flex;
+ align-items: center;
+
+ .icon {
+ height: 16px;
+ width: 16px;
+ }
}
}
- }
- .extension-routenumber {
- display: flex;
- flex-direction: row;
- margin-left: 40px;
- margin-bottom: var(--space-s);
- text-align: left;
- margin-top: var(--space-m);
+ .extension-routenumber {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: var(--space-s);
+ text-align: left;
+ margin-top: var(--space-m);
+
+ .bar {
+ border-radius: $border-radius;
+ }
+
+ .headsign {
+ display: flex;
+ flex-direction: column;
+ margin-left: var(--space-xs);
+ justify-content: center;
+ font-size: $font-size-small;
+ max-width: 85%;
+ line-height: 100%;
+ }
+ }
- .bar {
- border-radius: $border-radius;
+ .extension-indoor-route-button,
+ .extension-indoor-route-container,
+ .extension-walk {
+ margin-bottom: var(--space-s);
+ margin-top: var(--space-xs);
}
- .headsign {
+ .wait-in-vehicle {
display: flex;
- flex-direction: column;
- margin-left: var(--space-xs);
- justify-content: center;
- font-size: $font-size-small;
- max-width: 85%;
- line-height: 100%;
+ align-items: flex-start;
+ text-align: start;
}
- }
- .extension-walk {
- display: flex;
- margin-left: var(--space-xl);
- margin-bottom: var(--space-s);
- margin-top: var(--space-xs);
- }
+ .icon-expand {
+ margin-top: 5px;
+ width: 24px;
+ height: 24px;
+ margin-right: 10px;
+ }
- .wait-in-vehicle {
- display: flex;
- align-items: flex-start;
- text-align: start;
- margin-left: var(--space-xl);
- }
+ .icon-expand-small {
+ margin-top: 5px;
+ width: 16px;
+ height: 16px;
+ margin-right: var(--space-s);
+ }
- .wait-leg {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- margin-left: var(--space-m);
+ .destination-container {
+ display: flex;
- .icon {
- margin-top: 2px;
- height: 25px;
- width: 25px;
- }
- }
+ .destination-icon {
+ margin-right: 10px;
- .icon-expand {
- margin-top: 5px;
- width: 24px;
- height: 24px;
- }
+ &.place {
+ fill: $to-color;
+ }
- .icon-expand-small {
- margin-top: 5px;
- width: 16px;
- height: 16px;
- margin-right: var(--space-s);
- }
+ &.bus-stop {
+ color: $bus-color;
+ }
- .destination-icon {
- margin: 0 10px;
+ &.bus-express {
+ color: $bus-express-color;
+ }
- &.place {
- fill: $to-color;
- }
- }
+ &.speedtram {
+ color: $speedtram-color;
+ }
- .destination {
- text-align: left;
+ &.replacement-bus {
+ color: $replacement-bus-color;
+ }
+
+ &.tram-stop {
+ color: $tram-color;
+ }
- .details {
+ &.subway-stop {
+ color: $metro-color;
+ }
+
+ &.rail-stop {
+ color: $rail-color;
+ }
+
+ &.ferry-stop {
+ color: $ferry-color;
+ }
+
+ &.ferry-external-stop {
+ color: $external-feed-color;
+ }
+
+ &.funicular-stop {
+ color: $funicular-color;
+ }
+
+ &.speedtram-stop {
+ color: $speedtram-color;
+ }
+ }
+
+ .destination {
+ text-align: left;
+
+ .details {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ .address {
+ color: #888;
+ }
+
+ .platform-short {
+ width: unset;
+ font-family: $font-family;
+ font-size: $font-size-small;
+ letter-spacing: $letter-spacing;
+ display: inline-flex;
+ align-items: center;
+
+ .platform-number-wrapper {
+ padding: 0 var(--space-xxs);
+ min-height: 16px;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ line-height: 13px;
+ font-size: 11px;
+ }
+ }
+
+ .zone-icon-container {
+ .circle {
+ width: 16px;
+ height: 16px;
+ font-size: 0.9rem;
+ padding: 0 2px 0 2px;
+ }
+ }
+ }
+ }
+ }
+
+ .indoor-route-container-clickable {
display: flex;
- flex-direction: row;
- align-items: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
- .address {
- color: #888;
+ .indoor-route-text {
+ display: flex;
+ color: $primary-color;
+ font-weight: $font-weight-medium;
+ line-height: 1.2;
+ flex: 1;
}
- .platform-short {
- width: unset;
- font-family: $font-family;
- font-size: $font-size-small;
- letter-spacing: $letter-spacing;
- display: inline-flex;
- align-items: center;
+ .indoor-route-arrow-icon {
+ margin-right: 11px;
+
+ span {
+ display: flex;
+ align-items: center;
+
+ svg {
+ color: $primary-color;
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+ }
+
+ .navi-indoor-route-one-step-info-container {
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+
+ .navi-indoor-route-step-info {
+ display: flex;
+ flex: 1;
+ font-size: 0.9375rem;
+
+ .navi-indoor-route-step-icon {
+ width: 24px;
+ height: 100%;
+ vertical-align: middle;
+ display: flex;
+ margin-right: 8px;
+ }
- .platform-number-wrapper {
- padding: 0 var(--space-xxs);
- min-height: 16px;
- display: inline-flex;
- justify-content: center;
+ .navi-indoor-route-step-text {
align-items: center;
- line-height: 13px;
- font-size: 11px;
+ display: flex;
+ flex: 1;
+ }
+
+ .itinerary-map-action {
+ padding-bottom: 0;
+
+ .icon-container {
+ margin-top: 0;
+ }
}
}
+ }
- .zone-icon-container {
- .circle {
- width: 16px;
- height: 16px;
- font-size: 0.9rem;
- padding: 0 2px 0 2px;
+ .extension-indoor-route-container {
+ max-height: 130px;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ .navi-indoor-route-step-container {
+ display: flex;
+
+ .navi-indoor-route-step-info-container {
+ flex: 1;
+
+ .navi-indoor-route-step-info {
+ display: flex;
+ font-size: 0.9375rem;
+
+ .navi-indoor-route-step-icon {
+ width: 24px;
+ height: 100%;
+ vertical-align: middle;
+ display: flex;
+ margin-right: 8px;
+ }
+
+ .navi-indoor-route-step-text {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ }
+ }
+ }
+
+ .navi-indoor-route-step-line-container {
+ min-width: 28px;
+ position: relative;
+
+ .navi-indoor-route-step-line {
+ position: relative;
+ background-size: 100% auto;
+ background-position-y: 0;
+ background-position-x: 0;
+ background-repeat: no-repeat repeat;
+ border: none;
+ border-radius: 3px;
+ width: 6px;
+ left: 8px;
+ height: 100%;
+ top: 0;
+ }
+
+ .navi-indoor-route-step-line-circle-container {
+ position: absolute;
+
+ .navi-indoor-route-step-line-circle {
+ position: relative;
+ z-index: 9;
+ min-height: 37px;
+
+ > svg > circle.indoor-route-step-marker {
+ fill: #fff;
+ stroke: #666;
+ }
+ }
+ }
}
}
}
diff --git a/app/component/itinerary/queries/ItineraryDetailsFragment.js b/app/component/itinerary/queries/ItineraryDetailsFragment.js
index 5a09c8406c..d45b775886 100644
--- a/app/component/itinerary/queries/ItineraryDetailsFragment.js
+++ b/app/component/itinerary/queries/ItineraryDetailsFragment.js
@@ -39,6 +39,39 @@ export const ItineraryDetailsFragment = graphql`
publicCode
wheelchairAccessible
}
+ ... on ElevatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on EscalatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on StairsUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
}
lat
lon
diff --git a/app/component/itinerary/queries/PlanConnection.js b/app/component/itinerary/queries/PlanConnection.js
index f4d4538e28..4c5987d525 100644
--- a/app/component/itinerary/queries/PlanConnection.js
+++ b/app/component/itinerary/queries/PlanConnection.js
@@ -127,6 +127,39 @@ export const planConnection = graphql`
publicCode
wheelchairAccessible
}
+ ... on ElevatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on EscalatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on StairsUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
}
lat
lon
diff --git a/app/component/map/ClusterNumberMarker.js b/app/component/map/ClusterNumberMarker.js
new file mode 100644
index 0000000000..1205cd0b4e
--- /dev/null
+++ b/app/component/map/ClusterNumberMarker.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { default as L } from 'leaflet';
+
+import { configShape, locationShape } from '../../util/shapes';
+import GenericMarker from './GenericMarker';
+
+export default function ClusterNumberMarker({ position, number }, { config }) {
+ const objs = [];
+
+ const getIcon = () => {
+ const radius = 20;
+ const iconSvg = `
+
`;
+
+ return L.divIcon({
+ html: iconSvg,
+ iconSize: [radius * 2, radius * 2],
+ className: 'map-cluster-number-marker disable-icon-border',
+ });
+ };
+
+ objs.push(
+
,
+ );
+
+ return
{objs}
;
+}
+
+ClusterNumberMarker.contextTypes = {
+ config: configShape.isRequired,
+};
+
+ClusterNumberMarker.propTypes = {
+ position: locationShape.isRequired,
+ number: PropTypes.number.isRequired,
+};
diff --git a/app/component/map/EntranceMarker.js b/app/component/map/EntranceMarker.js
index 3d96aeb028..2054e5da16 100644
--- a/app/component/map/EntranceMarker.js
+++ b/app/component/map/EntranceMarker.js
@@ -2,50 +2,71 @@ import React from 'react';
import PropTypes from 'prop-types';
import { default as L } from 'leaflet';
-import Icon from '../Icon';
import { locationShape } from '../../util/shapes';
import GenericMarker from './GenericMarker';
-import { getCaseRadius, renderAsString } from '../../util/mapIconUtils';
+import { renderAsString, getIndexedIconFields } from '../../util/mapIconUtils';
+import { WheelchairBoarding } from '../../constants';
+import Icon from '../Icon';
-export default function EntranceMarker({ position, code }) {
+export default function EntranceMarker({ position, code, entranceAccessible }) {
const objs = [];
- const getSubwayIcon = zoom => {
- const iconSize = Math.max(getCaseRadius(zoom) * 2, 8);
+ const codeIndex = entranceAccessible === WheelchairBoarding.Possible ? 1 : 0;
+ const entranceIndex = (code ? 1 : 0) + codeIndex;
+ const getSubwayEntranceIcon = zoom => {
+ const { iconSize, iconAnchor } = getIndexedIconFields(zoom, entranceIndex);
return L.divIcon({
html: renderAsString(
),
- iconSize: [iconSize, iconSize],
- iconAnchor: [iconSize / 2, 2.5 * iconSize + 1],
+ iconSize,
+ iconAnchor,
className: 'map-subway-entrance-info-icon-metro',
});
};
-
- const getCodeIcon = zoom => {
- const iconSize = Math.max(getCaseRadius(zoom) * 2, 8);
-
+ const getSubwayEntranceCodeIcon = zoom => {
+ const { iconSize, iconAnchor } = getIndexedIconFields(zoom, codeIndex);
return L.divIcon({
html: renderAsString(
),
- iconSize: [iconSize, iconSize],
- iconAnchor: [iconSize / 2, 1.5 * iconSize],
+ iconSize,
+ iconAnchor,
+ className: 'map-subway-entrance-info-icon-metro',
+ });
+ };
+ const getSubwayEntranceAccessibleIcon = zoom => {
+ const { iconSize, iconAnchor } = getIndexedIconFields(zoom, 0);
+ return L.divIcon({
+ html: renderAsString(
),
+ iconSize,
+ iconAnchor,
className: 'map-subway-entrance-info-icon-metro',
});
};
objs.push(
,
);
-
if (code) {
objs.push(
,
+ );
+ }
+ if (entranceAccessible === WheelchairBoarding.Possible) {
+ objs.push(
+
,
);
}
@@ -55,8 +76,10 @@ export default function EntranceMarker({ position, code }) {
EntranceMarker.propTypes = {
position: locationShape.isRequired,
code: PropTypes.string,
+ entranceAccessible: PropTypes.oneOf(Object.values(WheelchairBoarding)),
};
EntranceMarker.defaultProps = {
code: undefined,
+ entranceAccessible: WheelchairBoarding.NoInformation,
};
diff --git a/app/component/map/GenericMarker.js b/app/component/map/GenericMarker.js
index be722b9509..3815f1979b 100644
--- a/app/component/map/GenericMarker.js
+++ b/app/component/map/GenericMarker.js
@@ -21,6 +21,7 @@ class GenericMarker extends React.Component {
renderName: PropTypes.bool,
name: PropTypes.string,
maxWidth: PropTypes.number,
+ minWidth: PropTypes.number,
children: PropTypes.node,
leaflet: PropTypes.shape({
map: PropTypes.shape({
@@ -30,6 +31,7 @@ class GenericMarker extends React.Component {
}).isRequired,
}).isRequired,
onClick: PropTypes.func,
+ zIndexOffset: PropTypes.number,
};
static defaultProps = {
@@ -38,7 +40,9 @@ class GenericMarker extends React.Component {
renderName: false,
name: '',
maxWidth: undefined,
+ minWidth: undefined,
children: undefined,
+ zIndexOffset: undefined,
};
state = { zoom: this.props.leaflet.map.getZoom() };
@@ -59,6 +63,7 @@ class GenericMarker extends React.Component {
icon={this.props.getIcon(this.state.zoom)}
onClick={this.props.onClick}
keyboard={false}
+ zIndexOffset={this.props.zIndexOffset}
>
{this.props.children && (
{this.props.children}
@@ -98,6 +106,7 @@ class GenericMarker extends React.Component {
iconAnchor: [-8, 7],
})}
keyboard={false}
+ zIndexOffset={this.props.zIndexOffset}
/>
);
}
diff --git a/app/component/map/IndoorRouteStepMarker.js b/app/component/map/IndoorRouteStepMarker.js
new file mode 100644
index 0000000000..344d293a6d
--- /dev/null
+++ b/app/component/map/IndoorRouteStepMarker.js
@@ -0,0 +1,151 @@
+import React, { useEffect, useState } from 'react';
+/* eslint-disable react/no-array-index-key */
+
+import PropTypes from 'prop-types';
+import { default as L } from 'leaflet';
+
+import { intlShape } from 'react-intl';
+import cx from 'classnames';
+import { configShape, locationShape } from '../../util/shapes';
+import GenericMarker from './GenericMarker';
+
+import Card from '../Card';
+import PopupHeader from './PopupHeader';
+import Icon from '../Icon';
+import { IndoorRouteStepType, VerticalDirection } from '../../constants';
+import { getVerticalTransportationUseIconId } from '../../util/indoorUtils';
+
+export default function IndoorRouteStepMarker(
+ { position, index, indoorRouteSteps },
+ { intl },
+) {
+ const [indoorBackgroundImageUrl, setIndoorBackgroundImageUrl] = useState();
+ useEffect(() => {
+ import(
+ /* webpackChunkName: "indoor-dotted-line-horizontal" */ `../../configurations/images/default/indoor-dotted-line-horizontal.svg`
+ ).then(insideImageUrl => {
+ setIndoorBackgroundImageUrl(`url(${insideImageUrl.default})`);
+ });
+ }, []);
+
+ const objs = [];
+
+ const getIcon = () => {
+ const radius = 10;
+ const iconSvg = `
+ `;
+
+ return L.divIcon({
+ html: iconSvg,
+ iconSize: [radius * 2, radius * 2],
+ className: 'map-indoor-route-step-marker disable-icon-border',
+ });
+ };
+
+ objs.push(
+
+
+
+
+
+ {indoorRouteSteps.map((obj, i, filteredObjs) => (
+
+
+ {filteredObjs.length !== i + 1 ? (
+
+ ) : null}
+
+ ))}
+
+
+
+ {indoorRouteSteps.map((obj, i) => (
+
+
+
+
+
+ ))}
+
+
+
+
+
+ ,
+ );
+
+ return {objs}
;
+}
+
+IndoorRouteStepMarker.contextTypes = {
+ config: configShape.isRequired,
+ intl: intlShape.isRequired,
+};
+
+IndoorRouteStepMarker.propTypes = {
+ position: locationShape.isRequired,
+ index: PropTypes.number.isRequired,
+ indoorRouteSteps: PropTypes.arrayOf(
+ PropTypes.shape({
+ type: PropTypes.oneOf(Object.values(IndoorRouteStepType)),
+ feature: PropTypes.shape({
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ }),
+ }),
+ ),
+};
+
+IndoorRouteStepMarker.defaultProps = {
+ indoorRouteSteps: [],
+};
diff --git a/app/component/map/ItineraryLine.js b/app/component/map/ItineraryLine.js
index 1fa1954801..06e3e3e205 100644
--- a/app/component/map/ItineraryLine.js
+++ b/app/component/map/ItineraryLine.js
@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
/* eslint-disable react/no-array-index-key */
+import Supercluster from 'supercluster';
+import { withLeaflet } from 'react-leaflet';
import polyUtil from 'polyline-encoded';
import React from 'react';
import { getMiddleOf } from '../../util/geo-utils';
@@ -14,6 +16,21 @@ import TransitLegMarkers from './non-tile-layer/TransitLegMarkers';
import VehicleMarker from './non-tile-layer/VehicleMarker';
import SpeechBubble from './SpeechBubble';
import EntranceMarker from './EntranceMarker';
+import ClusterNumberMarker from './ClusterNumberMarker';
+import IndoorRouteStepMarker from './IndoorRouteStepMarker';
+import { createFeatureObjects } from '../../util/clusterUtils';
+import {
+ IndoorRouteStepType,
+ IndoorRouteLegType,
+ WheelchairBoarding,
+} from '../../constants';
+import {
+ getEntranceObject,
+ getEntranceWheelchairAccessibility,
+ getIndoorRouteLegType,
+ getIndoorStepsWithVerticalTransportationUse,
+ isVerticalTransportationUse,
+} from '../../util/indoorUtils';
class ItineraryLine extends React.Component {
static contextTypes = {
@@ -28,6 +45,13 @@ class ItineraryLine extends React.Component {
showDurationBubble: PropTypes.bool,
streetMode: PropTypes.string,
realtimeTransfers: PropTypes.bool,
+ leaflet: PropTypes.shape({
+ map: PropTypes.shape({
+ getZoom: PropTypes.func.isRequired,
+ on: PropTypes.func.isRequired,
+ off: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
};
static defaultProps = {
@@ -39,6 +63,10 @@ class ItineraryLine extends React.Component {
realtimeTransfers: false,
};
+ state = {
+ zoom: this.props.leaflet.map.getZoom(),
+ };
+
checkStreetMode(leg) {
if (this.props.streetMode === 'walk') {
return leg.mode === 'WALK';
@@ -49,11 +77,298 @@ class ItineraryLine extends React.Component {
return false;
}
+ handleEntrance(
+ leg,
+ nextLeg,
+ mode,
+ i,
+ geometry,
+ objs,
+ clusterObjs,
+ entranceObject,
+ indoorRouteLegType,
+ ) {
+ const entranceCoordinates = [entranceObject.lat, entranceObject.lon];
+ const getDistance = (coord1, coord2) => {
+ const [lat1, lon1] = coord1;
+ const [lat2, lon2] = coord2;
+ return Math.sqrt((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2);
+ };
+
+ const entranceIndex = geometry.reduce(
+ (closestIndex, currentCoord, currentIndex) => {
+ const currentDistance = getDistance(entranceCoordinates, currentCoord);
+ const closestDistance = getDistance(
+ entranceCoordinates,
+ geometry[closestIndex],
+ );
+ return currentDistance < closestDistance ? currentIndex : closestIndex;
+ },
+ 0,
+ );
+
+ if (
+ entranceCoordinates[0] &&
+ entranceCoordinates[1] &&
+ !this.props.passive
+ ) {
+ clusterObjs.push({
+ lat: entranceCoordinates[0],
+ lon: entranceCoordinates[1],
+ properties: {
+ iconCount:
+ 1 +
+ (entranceObject.feature.publicCode ? 1 : 0) +
+ (entranceObject.feature.wheelchairAccessible ===
+ WheelchairBoarding.Possible
+ ? 1
+ : 0),
+ type: IndoorRouteStepType.Entrance,
+ code: entranceObject.feature.publicCode?.toLowerCase(),
+ },
+ });
+ }
+
+ objs.push(
+ ,
+ );
+ objs.push(
+ ,
+ );
+ }
+
+ handleLine(previousLeg, leg, nextLeg, mode, i, geometry, objs, clusterObjs) {
+ const entranceObject = getEntranceObject(previousLeg, leg);
+ const indoorRouteLegType = getIndoorRouteLegType(previousLeg, leg, nextLeg);
+ if (indoorRouteLegType !== IndoorRouteLegType.NoStepsInside) {
+ this.handleEntrance(
+ leg,
+ nextLeg,
+ mode,
+ i,
+ geometry,
+ objs,
+ clusterObjs,
+ entranceObject,
+ indoorRouteLegType,
+ );
+ } else {
+ objs.push(
+ ,
+ );
+ }
+ }
+
+ handleDurationBubble(leg, mode, i, objs, middle) {
+ if (
+ this.props.showDurationBubble ||
+ (this.checkStreetMode(leg) && leg.distance > 100)
+ ) {
+ const duration = durationToString(leg.duration * 1000);
+ objs.push(
+ ,
+ );
+ }
+ }
+
+ handleIntermediateStops(leg, mode, objs) {
+ if (
+ !this.props.passive &&
+ this.props.showIntermediateStops &&
+ leg.intermediatePlaces != null
+ ) {
+ leg.intermediatePlaces
+ .filter(place => place.stop)
+ .forEach(place =>
+ objs.push(
+ ,
+ ),
+ );
+ }
+ }
+
+ /**
+ * Add dynamic transit leg and transfer stop markers.
+ */
+ handleTransitLegMarkers(transitLegs, objs) {
+ if (!this.props.passive) {
+ objs.push(
+ ,
+ );
+ }
+ }
+
+ handleIndoorRouteStepMarkers(previousLeg, leg, nextLeg, clusterObjs) {
+ if (!this.props.passive) {
+ const indoorRouteSteps = getIndoorStepsWithVerticalTransportationUse(
+ previousLeg,
+ leg,
+ nextLeg,
+ );
+
+ if (indoorRouteSteps) {
+ indoorRouteSteps.forEach((indoorRouteStep, i) => {
+ if (indoorRouteStep.lat && indoorRouteStep.lon) {
+ clusterObjs.push({
+ lat: indoorRouteStep.lat,
+ lon: indoorRouteStep.lon,
+ properties: {
+ iconCount: 1,
+ // eslint-disable-next-line no-underscore-dangle
+ type: indoorRouteStep.feature?.__typename,
+ verticalDirection: indoorRouteStep.feature?.verticalDirection,
+ index: i,
+ },
+ });
+ }
+ });
+ }
+ }
+ }
+
+ componentDidMount() {
+ this.props.leaflet.map.on('zoomend', this.onMapZoom);
+ }
+
+ componentWillUnmount() {
+ this.props.leaflet.map.off('zoomend', this.onMapZoom);
+ }
+
+ onMapZoom = () => {
+ const zoom = this.props.leaflet.map.getZoom();
+ this.setState({ zoom });
+ };
+
+ handleClusterObjects(previousLeg, leg, nextLeg, objs, clusterObjs) {
+ if (!this.props.passive) {
+ const index = new Supercluster({
+ radius: 60, // in pixels
+ maxZoom: 15,
+ minPoints: 2,
+ extent: 512, // tile size (512)
+ map: properties => ({
+ iconCount: properties.iconCount,
+ }),
+ reduce: (accumulated, properties) => {
+ // eslint-disable-next-line no-param-reassign
+ accumulated.iconCount += properties.iconCount;
+ },
+ });
+
+ index.load(createFeatureObjects(clusterObjs));
+ const bbox = [-180, -85, 180, 85]; // Bounding box covers the entire world
+ // TODO Fix to use smaller bbox, probably requires moveend event listening?
+ // The same fix should also be applied to RentalVehicles where supercluster is also used.
+ //
+ // const bounds = this.props.leaflet.map.getBounds();
+ // const bbox = [
+ // bounds.getWest(),
+ // bounds.getSouth(),
+ // bounds.getEast(),
+ // bounds.getNorth(),
+ // ];
+
+ const clusters = index.getClusters(bbox, this.state.zoom);
+ clusters.forEach(clusterFeature => {
+ const { coordinates } = clusterFeature.geometry;
+ const { properties } = clusterFeature;
+ if (properties.cluster) {
+ // Handle a cluster.
+ objs.push(
+ ,
+ );
+ } else {
+ // Handle a single point.
+ // eslint-disable-next-line no-lonely-if
+ if (properties.type === IndoorRouteStepType.Entrance) {
+ objs.push(
+ ,
+ );
+ } else if (isVerticalTransportationUse(properties.type)) {
+ objs.push(
+ ,
+ );
+ }
+ }
+ });
+ }
+ }
+
render() {
const objs = [];
const transitLegs = [];
this.props.legs.forEach((leg, i) => {
+ const clusterObjs = [];
+
if (!leg || leg.mode === LegMode.Wait) {
return;
}
@@ -103,142 +418,22 @@ class ItineraryLine extends React.Component {
end = interliningLegs[interliningLegs.length - 1].end;
}
- if (
- leg.mode === 'WALK' &&
- (nextLeg?.mode === 'SUBWAY' || previousLeg?.mode === 'SUBWAY')
- ) {
- const entranceObjects = leg?.steps?.filter(
- step =>
- // eslint-disable-next-line no-underscore-dangle
- step?.feature?.__typename === 'Entrance' || step?.feature?.code,
- );
-
- // Select the entrance to the outside if there are multiple entrances
- const entranceObject =
- previousLeg?.mode === 'SUBWAY'
- ? entranceObjects[entranceObjects.length - 1]
- : entranceObjects[0];
-
- if (entranceObject) {
- const entranceCoordinates = [entranceObject.lat, entranceObject.lon];
- const getDistance = (coord1, coord2) => {
- const [lat1, lon1] = coord1;
- const [lat2, lon2] = coord2;
- return Math.sqrt((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2);
- };
-
- const entranceIndex = geometry.reduce(
- (closestIndex, currentCoord, currentIndex) => {
- const currentDistance = getDistance(
- entranceCoordinates,
- currentCoord,
- );
- const closestDistance = getDistance(
- entranceCoordinates,
- geometry[closestIndex],
- );
- return currentDistance < closestDistance
- ? currentIndex
- : closestIndex;
- },
- 0,
- );
-
- if (entranceCoordinates && !this.props.passive) {
- objs.push(
- ,
- );
- }
-
- objs.push(
- ,
- );
- objs.push(
- ,
- );
- } else {
- objs.push(
- ,
- );
- }
- } else {
- objs.push(
- ,
- );
- }
-
- if (
- this.props.showDurationBubble ||
- (this.checkStreetMode(leg) && leg.distance > 100)
- ) {
- const duration = durationToString(leg.duration * 1000);
- objs.push(
- ,
- );
- }
+ this.handleLine(
+ previousLeg,
+ leg,
+ nextLeg,
+ mode,
+ i,
+ geometry,
+ objs,
+ clusterObjs,
+ );
+ this.handleDurationBubble(leg, mode, i, objs, middle);
+ this.handleIntermediateStops(leg, mode, objs);
+ this.handleIndoorRouteStepMarkers(previousLeg, leg, nextLeg, clusterObjs);
+ this.handleClusterObjects(previousLeg, leg, nextLeg, objs, clusterObjs);
if (!this.props.passive) {
- if (
- this.props.showIntermediateStops &&
- leg.intermediatePlaces != null
- ) {
- leg.intermediatePlaces
- .filter(place => place.stop)
- .forEach(place =>
- objs.push(
- ,
- ),
- );
- }
-
if (rentalId) {
objs.push(
,
- );
- }
+ this.handleTransitLegMarkers(transitLegs, objs);
return {objs}
;
}
}
-export default ItineraryLine;
+export default withLeaflet(ItineraryLine);
diff --git a/app/component/map/NearYouMap.js b/app/component/map/NearYouMap.js
index 71690b963e..30d193ce86 100644
--- a/app/component/map/NearYouMap.js
+++ b/app/component/map/NearYouMap.js
@@ -160,6 +160,17 @@ function NearYouMap(
const fetchPlan = stop => {
if (stop.distance < walkRoutingThreshold) {
const settings = getSettings(context.config);
+ let location = {
+ coordinate: {
+ latitude: stop.lat,
+ longitude: stop.lon,
+ },
+ };
+ if (stop.gtfsId) {
+ location = {
+ stopLocation: { stopLocationId: stop.gtfsId },
+ };
+ }
const variables = {
origin: {
location: {
@@ -167,9 +178,7 @@ function NearYouMap(
},
},
destination: {
- location: {
- coordinate: { latitude: stop.lat, longitude: stop.lon },
- },
+ location,
},
walkSpeed: settings.walkSpeed,
wheelchair: !!settings.accessibilityOption,
diff --git a/app/component/map/StopPageMap.js b/app/component/map/StopPageMap.js
index ac199f1a7a..a927b4db90 100644
--- a/app/component/map/StopPageMap.js
+++ b/app/component/map/StopPageMap.js
@@ -65,6 +65,17 @@ function StopPageMap(
if (locationState.hasLocation) {
if (distance(locationState, stop) < maxShowRouteDistance) {
const settings = getSettings(config);
+ let location = {
+ coordinate: {
+ latitude: targetStop.lat,
+ longitude: targetStop.lon,
+ },
+ };
+ if (targetStop.gtfsId) {
+ location = {
+ stopLocation: { stopLocationId: targetStop.gtfsId },
+ };
+ }
const variables = {
origin: {
location: {
@@ -75,12 +86,7 @@ function StopPageMap(
},
},
destination: {
- location: {
- coordinate: {
- latitude: targetStop.lat,
- longitude: targetStop.lon,
- },
- },
+ location,
},
walkSpeed: settings.walkSpeed,
wheelchair: !!settings.accessibilityOption,
diff --git a/app/component/map/WalkQuery.js b/app/component/map/WalkQuery.js
index 4fda6c94ea..56d685a9b1 100644
--- a/app/component/map/WalkQuery.js
+++ b/app/component/map/WalkQuery.js
@@ -46,6 +46,39 @@ const walkQuery = graphql`
publicCode
wheelchairAccessible
}
+ ... on ElevatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on EscalatorUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
+ ... on StairsUse {
+ from {
+ level
+ name
+ }
+ verticalDirection
+ to {
+ level
+ name
+ }
+ }
}
lat
lon
@@ -86,6 +119,7 @@ const walkQuery = graphql`
gtfsId
code
platformCode
+ vehicleMode
}
}
to {
@@ -107,6 +141,7 @@ const walkQuery = graphql`
gtfsId
code
platformCode
+ vehicleMode
}
}
intermediatePlaces {
diff --git a/app/component/map/map.scss b/app/component/map/map.scss
index 9be6630fe2..ab79968549 100644
--- a/app/component/map/map.scss
+++ b/app/component/map/map.scss
@@ -259,6 +259,26 @@ div.leaflet-marker-icon.parking {
div.leaflet-marker-icon.map-subway-entrance-info-icon-metro {
color: #0074bf;
+ cursor: grab;
+}
+
+div.leaflet-marker-icon.map-cluster-number-marker {
+ cursor: grab;
+}
+
+div.leaflet-marker-icon.map-indoor-route-step-marker {
+ > svg > circle.indoor-route-step-marker {
+ fill: #fff;
+ stroke: #666;
+ transition: all 0.3s ease;
+ transform-origin: center;
+ }
+
+ > svg > circle.indoor-route-step-marker:hover {
+ transform: scale(1.3);
+ fill: #666;
+ stroke: #fff;
+ }
}
div.leaflet-marker-icon.via {
@@ -632,6 +652,74 @@ div.leaflet-marker-icon.vehicle-icon {
background: $white;
border: solid 1px #ddd;
+ &.indoor-route-step-popup-container {
+ line-height: 0;
+ margin: 0;
+ padding: 10px 0 10px;
+ border: none;
+ border-top: 1px solid #ddd;
+ border-radius: 0;
+ flex-direction: column;
+
+ .indoor-route-step-popup-icons {
+ flex-direction: row;
+ font-size: 20px;
+
+ .icon-container {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ > svg.icon.indoor-route-step-popup-icon {
+ width: 2em;
+ }
+
+ > svg.icon.arrow-popup-icon {
+ color: black;
+ height: 0.5em;
+ width: 0.5em;
+ }
+ }
+ }
+
+ .indoor-route-step-popup-line-container {
+ .indoor-route-step-popup-line {
+ background-size: auto 100%;
+ background-position-y: 0;
+ background-position-x: 0;
+ background-repeat: repeat no-repeat;
+ border: none;
+ border-radius: 3px;
+ margin: 10%;
+ height: 6px;
+ min-width: 50px;
+ }
+
+ .indoor-route-step-popup-line-circle-container {
+ position: absolute;
+ min-width: 0;
+
+ .indoor-route-step-popup-line-circle {
+ border: none;
+ min-width: 0;
+ margin: 11px;
+
+ > svg > circle.indoor-route-step-marker {
+ fill: #fff;
+ stroke: #666;
+
+ &.selected {
+ fill: #666;
+ stroke: #fff;
+ filter: drop-shadow(0 1px 1px rgba(51, 51, 51, 1));
+ r: 8;
+ }
+ }
+ }
+ }
+ }
+ }
+
div,
a,
button {
diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js
index 44a0af5fd3..38c05cc0fa 100644
--- a/app/component/map/tile-layer/RentalVehicles.js
+++ b/app/component/map/tile-layer/RentalVehicles.js
@@ -13,6 +13,7 @@ import { fetchWithLanguageAndSubscription } from '../../../util/fetchUtils';
import { getLayerBaseUrl } from '../../../util/mapLayerUtils';
import { TransportMode } from '../../../constants';
import { getSettings } from '../../../util/planParamUtil';
+import { createFeatureObjects } from '../../../util/clusterUtils';
class RentalVehicles {
constructor(tile, config, mapLayers, relayEnvironment) {
@@ -170,22 +171,20 @@ class RentalVehicles {
static getName = () => 'scooter';
pointsInSuperclusterFormat = () => {
- return this.features.map(feature => {
- // Convert the feature's x/y to lat/lon for clustering
- const latLon = this.tile.project({
- x: feature.geom.x,
- y: feature.geom.y,
- });
- return {
- type: 'Feature',
- properties: { ...feature.properties },
- geom: { ...feature.geom },
- geometry: {
- type: 'Point',
- coordinates: [latLon.lat, latLon.lon],
- },
- };
- });
+ return createFeatureObjects(
+ this.features.map(feature => {
+ // Convert the feature's x/y to lat/lon for clustering
+ const coordinates = this.tile.project({
+ x: feature.geom.x,
+ y: feature.geom.y,
+ });
+ return {
+ properties: feature.properties,
+ lat: coordinates.lat,
+ lon: coordinates.lon,
+ };
+ }),
+ );
};
featureWithGeom = clusterFeature => {
diff --git a/app/component/stop/StopPageMapContainer.js b/app/component/stop/StopPageMapContainer.js
index 39d008ad83..3b0677ab9a 100644
--- a/app/component/stop/StopPageMapContainer.js
+++ b/app/component/stop/StopPageMapContainer.js
@@ -34,6 +34,7 @@ const containerComponent = createFragmentContainer(StopPageMapContainer, {
desc
vehicleMode
locationType
+ gtfsId
}
`,
});
diff --git a/app/component/stop/TerminalPageMapContainer.js b/app/component/stop/TerminalPageMapContainer.js
index 79b89dd628..8c25d7719b 100644
--- a/app/component/stop/TerminalPageMapContainer.js
+++ b/app/component/stop/TerminalPageMapContainer.js
@@ -39,6 +39,7 @@ const containerComponent = createFragmentContainer(TerminalPageMapContainer, {
desc
vehicleMode
locationType
+ gtfsId
}
`,
});
diff --git a/app/configurations/images/default/indoor-dotted-line-horizontal.svg b/app/configurations/images/default/indoor-dotted-line-horizontal.svg
new file mode 100644
index 0000000000..d4d2c228ed
--- /dev/null
+++ b/app/configurations/images/default/indoor-dotted-line-horizontal.svg
@@ -0,0 +1,21 @@
+
+
diff --git a/app/configurations/images/default/indoor-dotted-line.svg b/app/configurations/images/default/indoor-dotted-line.svg
new file mode 100644
index 0000000000..9cf130fc10
--- /dev/null
+++ b/app/configurations/images/default/indoor-dotted-line.svg
@@ -0,0 +1,21 @@
+
+
diff --git a/app/constants.js b/app/constants.js
index f91c32a11d..9ff4319c10 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -129,6 +129,32 @@ export const PlannerMessageType = Object.freeze({
SystemError: 'SYSTEM_ERROR',
});
+export const VerticalDirection = Object.freeze({
+ Up: 'UP',
+ Down: 'DOWN',
+ Unknown: 'UNKNOWN',
+});
+
+export const IndoorRouteStepType = Object.freeze({
+ Entrance: 'Entrance',
+ ElevatorUse: 'ElevatorUse',
+ EscalatorUse: 'EscalatorUse',
+ StairsUse: 'StairsUse',
+});
+
+export const IndoorRouteLegType = Object.freeze({
+ AllStepsInside: 'ALL_STEPS_INSIDE',
+ StepsAfterEntranceInside: 'STEPS_AFTER_ENTRANCE_INSIDE',
+ StepsBeforeEntranceInside: 'STEPS_BEFORE_ENTRANCE_INSIDE',
+ NoStepsInside: 'NO_STEPS_INSIDE',
+});
+
+export const WheelchairBoarding = Object.freeze({
+ NotPossible: 'NOT_POSSIBLE',
+ NoInformation: 'NO_INFORMATION',
+ Possible: 'POSSIBLE',
+});
+
/**
* OpenTripPlanner (v2) via point types.
*/
diff --git a/app/translations.js b/app/translations.js
index c5901d710b..278105af5f 100644
--- a/app/translations.js
+++ b/app/translations.js
@@ -1133,6 +1133,14 @@ const translations = {
'in-addition': 'In addition',
'include-estonia': 'Include Estonia',
'index.title': 'Journey Planner',
+ 'indoor-step-message-elevator': 'Elevator',
+ 'indoor-step-message-elevator-to-floor': 'Elevator to floor {toLevelName}',
+ 'indoor-step-message-escalator': 'Escalator',
+ 'indoor-step-message-escalator-down': 'Escalator down',
+ 'indoor-step-message-escalator-up': 'Escalator up',
+ 'indoor-step-message-stairs': 'Stairs',
+ 'indoor-step-message-stairs-down': 'Stairs down',
+ 'indoor-step-message-stairs-up': 'Stairs up',
inquiry: 'How did you find the new Journey Planner? Please tell us!',
instructions: 'Instructions',
'is-open': 'Open:',
@@ -1170,11 +1178,13 @@ const translations = {
'itinerary-feedback-message': "Couldn't find what you were looking for?",
'itinerary-feedback-placeholder': 'Description (optional)',
'itinerary-hide-alternative-legs': 'Hide alternatives',
+ 'itinerary-hide-indoor-route': 'Hide the indoor route',
'itinerary-hide-stops': 'Hide stops',
'itinerary-in-the-past':
'The route search falls within a period that is in the past.',
'itinerary-in-the-past-link': 'Depart now ›',
'itinerary-in-the-past-title': 'The route options cannot be displayed',
+ 'itinerary-indoor-route': 'Indoor route',
'itinerary-page.description': 'Itinerary',
'itinerary-page.hide-details': 'Hide itinerary details',
'itinerary-page.itineraries-loaded': 'Search results downloaded',
@@ -2485,6 +2495,14 @@ const translations = {
'in-addition': 'Lisäksi',
'include-estonia': 'Sisällytä Viron liikenne',
'index.title': 'Reittiopas',
+ 'indoor-step-message-elevator': 'Hissi',
+ 'indoor-step-message-elevator-to-floor': 'Hissi kerrokseen {toLevelName}',
+ 'indoor-step-message-escalator': 'Liukuportaat',
+ 'indoor-step-message-escalator-down': 'Liukuportaat alaspäin',
+ 'indoor-step-message-escalator-up': 'Liukuportaat ylöspäin',
+ 'indoor-step-message-stairs': 'Portaat',
+ 'indoor-step-message-stairs-down': 'Portaat alaspäin',
+ 'indoor-step-message-stairs-up': 'Portaat ylöspäin',
inquiry: 'Mitä pidät uudesta Reittioppaasta? Kerro se meille! ',
instructions: 'Ohjeet',
'is-open': 'Avoinna:',
@@ -2521,10 +2539,12 @@ const translations = {
'itinerary-feedback-message': 'Etkö löytänyt mitä etsit?',
'itinerary-feedback-placeholder': 'Kuvaus (valinnainen)',
'itinerary-hide-alternative-legs': 'Piilota vaihtoehdot',
+ 'itinerary-hide-indoor-route': 'Piilota kulkureitti sisällä',
'itinerary-hide-stops': 'Piilota pysäkit',
'itinerary-in-the-past': 'Reittihaun ajankohta on menneisyydessä.',
'itinerary-in-the-past-link': 'Muuta lähtöajaksi nyt ›',
'itinerary-in-the-past-title': 'Reittivaihtoehtoja ei voida näyttää',
+ 'itinerary-indoor-route': 'Kulkureitti sisällä',
'itinerary-page.description': 'Reittiohje',
'itinerary-page.hide-details': 'Piilota reittiohje',
'itinerary-page.itineraries-loaded': 'Hakutulokset ladattu',
@@ -5460,6 +5480,14 @@ const translations = {
'in-addition': 'Även',
'include-estonia': 'Inkludera Estland',
'index.title': 'Reseplaneraren',
+ 'indoor-step-message-elevator': 'Hiss',
+ 'indoor-step-message-elevator-to-floor': 'Hiss till våning {toLevelName}',
+ 'indoor-step-message-escalator': 'Rulltrappa',
+ 'indoor-step-message-escalator-down': 'Rulltrappa nedåt',
+ 'indoor-step-message-escalator-up': 'Rulltrappa uppåt',
+ 'indoor-step-message-stairs': 'Trappa',
+ 'indoor-step-message-stairs-down': 'Trappa nedåt',
+ 'indoor-step-message-stairs-up': 'Trappa uppåt',
inquiry: 'Vad tycker du om den nya Reseplaneraren. Berätta för oss!',
instructions: 'Anvisningar',
'is-open': 'Öppet:',
@@ -5498,10 +5526,12 @@ const translations = {
'itinerary-feedback-message': 'Hittade du inte vad du sökte?',
'itinerary-feedback-placeholder': 'Beskrivning (valfri)',
'itinerary-hide-alternative-legs': 'Dölj alternativen',
+ 'itinerary-hide-indoor-route': 'Dölj gångrutt inomhus',
'itinerary-hide-stops': 'Dölj hållplatserna',
'itinerary-in-the-past': 'Datumet kan inte vara i det förflutna.',
'itinerary-in-the-past-link': 'Jag vill åka nu ›',
'itinerary-in-the-past-title': 'Ruttalternativen kan inte visas',
+ 'itinerary-indoor-route': 'Gångrutt inomhus',
'itinerary-page.description': 'Ruttinformation',
'itinerary-page.hide-details': 'Göm ruttbeskrivningen',
'itinerary-page.itineraries-loaded': 'Ruttbeskrivningen laddade',
diff --git a/app/util/clusterUtils.js b/app/util/clusterUtils.js
new file mode 100644
index 0000000000..1c2d4081bc
--- /dev/null
+++ b/app/util/clusterUtils.js
@@ -0,0 +1,13 @@
+/**
+ * Create Features for a given list of objects
+ */
+export function createFeatureObjects(objects) {
+ return objects.map(object => ({
+ type: 'Feature',
+ properties: { ...object.properties },
+ geometry: {
+ type: 'Point',
+ coordinates: [object.lat, object.lon],
+ },
+ }));
+}
diff --git a/app/util/indoorUtils.js b/app/util/indoorUtils.js
index 127e601fa8..86d5cb3c80 100644
--- a/app/util/indoorUtils.js
+++ b/app/util/indoorUtils.js
@@ -1,3 +1,10 @@
+import {
+ IndoorRouteLegType,
+ IndoorRouteStepType,
+ VerticalDirection,
+} from '../constants';
+import { addAnalyticsEvent } from './analyticsUtils';
+
export function subwayTransferUsesSameStation(prevLeg, nextLeg) {
return (
prevLeg?.mode === 'SUBWAY' &&
@@ -6,3 +13,158 @@ export function subwayTransferUsesSameStation(prevLeg, nextLeg) {
nextLeg.from.stop.parentStation?.gtfsId
);
}
+
+const iconMappings = {
+ elevator: 'icon_elevator',
+ 'elevator-filled': 'icon_elevator_filled',
+ escalator: 'icon_escalator_down',
+ 'escalator-filled': 'icon_escalator_down_filled',
+ stairs: 'icon_stairs_down',
+ 'stairs-filled': 'icon_stairs_down_filled',
+ 'stairs-down': 'icon_stairs_down',
+ 'stairs-down-filled': 'icon_stairs_down_filled',
+ 'stairs-up': 'icon_stairs_up',
+ 'stairs-up-filled': 'icon_stairs_up_filled',
+ 'escalator-down': 'icon_escalator_down_arrow',
+ 'escalator-down-filled': 'icon_escalator_down_arrow_filled',
+ 'escalator-up': 'icon_escalator_up_arrow',
+ 'escalator-up-filled': 'icon_escalator_up_arrow_filled',
+};
+
+export function getVerticalTransportationUseIconId(
+ verticalDirection,
+ type,
+ filled,
+) {
+ if (
+ verticalDirection === undefined ||
+ verticalDirection === VerticalDirection.Unknown ||
+ type === IndoorRouteStepType.ElevatorUse
+ ) {
+ return iconMappings[
+ `${type?.toLowerCase().replace('use', '')}${filled ? '-filled' : ''}`
+ ];
+ }
+ return iconMappings[
+ `${type
+ ?.toLowerCase()
+ .replace('use', '')}-${verticalDirection.toLowerCase()}${
+ filled ? '-filled' : ''
+ }`
+ ];
+}
+
+export function getEntranceObject(previousLeg, leg) {
+ const entranceObjects = leg?.steps
+ ?.map((step, index) => ({ ...step, index }))
+ .filter(
+ step =>
+ // eslint-disable-next-line no-underscore-dangle
+ step?.feature?.__typename === 'Entrance' || step?.feature?.code,
+ );
+ // Select the entrance to the outside if there are multiple entrances
+ const entranceObject =
+ previousLeg?.mode === 'SUBWAY'
+ ? entranceObjects[entranceObjects.length - 1]
+ : entranceObjects[0];
+
+ return entranceObject;
+}
+
+export function getEntranceStepIndex(previousLeg, leg) {
+ return getEntranceObject(previousLeg, leg)?.index;
+}
+
+export function getIndoorRouteLegType(previousLeg, leg, nextLeg) {
+ const entranceObject = getEntranceObject(previousLeg, leg);
+ // Outdoor routing starts from an entrance if the leg started from the subway.
+ if (
+ entranceObject &&
+ ((leg.mode === 'WALK' && previousLeg?.mode === 'SUBWAY') ||
+ leg.from.stop?.vehicleMode === 'SUBWAY')
+ ) {
+ return IndoorRouteLegType.StepsBeforeEntranceInside;
+ }
+ // Indoor routing starts from an entrance if the leg ends in the subway.
+ if (
+ entranceObject &&
+ ((leg.mode === 'WALK' && nextLeg?.mode === 'SUBWAY') ||
+ leg.to.stop?.vehicleMode === 'SUBWAY')
+ ) {
+ return IndoorRouteLegType.StepsAfterEntranceInside;
+ }
+ return IndoorRouteLegType.NoStepsInside;
+}
+
+export function getIndoorSteps(previousLeg, leg, nextLeg) {
+ const entranceIndex = getEntranceStepIndex(previousLeg, leg);
+ if (!entranceIndex) {
+ return [];
+ }
+ const indoorRouteLegType = getIndoorRouteLegType(previousLeg, leg, nextLeg);
+ if (indoorRouteLegType === IndoorRouteLegType.StepsBeforeEntranceInside) {
+ return leg.steps.slice(0, entranceIndex + 1);
+ }
+ if (indoorRouteLegType === IndoorRouteLegType.StepsAfterEntranceInside) {
+ return leg.steps.slice(entranceIndex);
+ }
+ return [];
+}
+
+export function isVerticalTransportationUse(type) {
+ return (
+ type === IndoorRouteStepType.ElevatorUse ||
+ type === IndoorRouteStepType.EscalatorUse ||
+ type === IndoorRouteStepType.StairsUse
+ );
+}
+
+export function getIndoorStepsWithVerticalTransportationUse(
+ previousLeg,
+ leg,
+ nextLeg,
+) {
+ return getIndoorSteps(previousLeg, leg, nextLeg).filter(step =>
+ // eslint-disable-next-line no-underscore-dangle
+ isVerticalTransportationUse(step?.feature?.__typename),
+ );
+}
+
+export function getIndoorRouteTranslationId(
+ type,
+ verticalDirection,
+ toLevelName,
+) {
+ if (type === IndoorRouteStepType.ElevatorUse && toLevelName) {
+ return 'indoor-step-message-elevator-to-floor';
+ }
+ return `indoor-step-message-${type?.toLowerCase().replace('use', '')}${
+ verticalDirection &&
+ verticalDirection !== VerticalDirection.Unknown &&
+ type !== IndoorRouteStepType.ElevatorUse
+ ? `-${verticalDirection.toLowerCase()}`
+ : ''
+ }`;
+}
+
+export function getEntranceWheelchairAccessibility(leg) {
+ return leg?.steps?.find(
+ // eslint-disable-next-line no-underscore-dangle
+ step =>
+ // eslint-disable-next-line no-underscore-dangle
+ step?.feature?.__typename === 'Entrance' ||
+ step?.feature?.wheelchairAccessible,
+ )?.feature?.wheelchairAccessible;
+}
+
+export function getStepFocusAction(lat, lon, focusToPoint) {
+ return e => {
+ e.stopPropagation();
+ focusToPoint(lat, lon);
+ addAnalyticsEvent({
+ category: 'Itinerary',
+ action: 'ZoomMapToStep',
+ name: null,
+ });
+ };
+}
diff --git a/app/util/mapIconUtils.js b/app/util/mapIconUtils.js
index 789ad693bd..bfe4791199 100644
--- a/app/util/mapIconUtils.js
+++ b/app/util/mapIconUtils.js
@@ -3,8 +3,8 @@ import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import glfun from './glfun';
import { transitIconName } from './modeUtils';
-import { ParkTypes, TransportMode } from '../constants';
import { getModeIconColor } from './colorUtils';
+import { ParkTypes, TransportMode } from '../constants';
/**
* Corresponds to an arc forming a full circle (Math.PI * 2).
@@ -850,3 +850,11 @@ export function renderAsString(children) {
ReactDOM.unmountComponentAtNode(div);
return html;
}
+
+export function getIndexedIconFields(zoom, index) {
+ const iconEdgeSize = Math.max(getCaseRadius(zoom) * 2, 8);
+ const iconSize = [iconEdgeSize, iconEdgeSize];
+ const iconAnchor = [iconEdgeSize / 2, (1.5 + index) * iconEdgeSize + index];
+
+ return { iconSize, iconAnchor };
+}
diff --git a/app/util/shapes.js b/app/util/shapes.js
index eb783d5204..987ca132eb 100644
--- a/app/util/shapes.js
+++ b/app/util/shapes.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import { PlannerMessageType } from '../constants';
+import { VerticalDirection, PlannerMessageType } from '../constants';
export const agencyShape = PropTypes.shape({
name: PropTypes.string,
@@ -208,10 +208,50 @@ export const legTimeShape = PropTypes.shape({
});
export const entranceShape = PropTypes.shape({
+ __typename: PropTypes.oneOf(['Entrance']).isRequired,
publicCode: PropTypes.string,
wheelchairAccessible: PropTypes.string,
});
+export const elevatorUseShape = PropTypes.shape({
+ __typename: PropTypes.oneOf(['ElevatorUse']).isRequired,
+ from: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ to: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+});
+
+export const escalatorUseShape = PropTypes.shape({
+ __typename: PropTypes.oneOf(['EscalatorUse']).isRequired,
+ from: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ to: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+});
+
+export const stairsUseShape = PropTypes.shape({
+ __typename: PropTypes.oneOf(['StairsUse']).isRequired,
+ from: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+ verticalDirection: PropTypes.oneOf(Object.values(VerticalDirection)),
+ to: PropTypes.shape({
+ level: PropTypes.number,
+ name: PropTypes.string,
+ }),
+});
+
export const legShape = PropTypes.shape({
start: legTimeShape,
end: legTimeShape,
@@ -226,7 +266,12 @@ export const legShape = PropTypes.shape({
fare: fareShape,
steps: PropTypes.arrayOf(
PropTypes.shape({
- entrance: entranceShape,
+ feature: PropTypes.oneOfType([
+ entranceShape,
+ elevatorUseShape,
+ escalatorUseShape,
+ stairsUseShape,
+ ]),
lat: PropTypes.number,
lon: PropTypes.number,
}),
@@ -241,7 +286,6 @@ export const legShape = PropTypes.shape({
name: PropTypes.string,
stop: stopShape,
vehicleRentalStation: vehicleRentalStationShape,
-
bikePark: parkShape,
carPark: parkShape,
}),
diff --git a/build/schema.graphql b/build/schema.graphql
index d7ed4db3ba..f89ec7f85d 100644
--- a/build/schema.graphql
+++ b/build/schema.graphql
@@ -97,7 +97,7 @@ union CallStopLocation = Location | LocationGroup | Stop
union RentalPlace = RentalVehicle | VehicleRentalStation
"A feature for a step"
-union StepFeature = Entrance
+union StepFeature = ElevatorUse | Entrance | EscalatorUse | StairsUse
union StopPosition = PositionAtStop | PositionBetweenStops
@@ -525,6 +525,15 @@ type DependentFareProduct implements FareProduct {
riderCategory: RiderCategory
}
+"A single use of an elevator."
+type ElevatorUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
type Emissions {
"CO₂ emissions in grams."
co2: Grams
@@ -542,6 +551,15 @@ type Entrance {
wheelchairAccessible: WheelchairBoarding
}
+"A single use of an escalator."
+type EscalatorUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
"Real-time estimates for an arrival or departure at a certain place."
type EstimatedTime {
"""
@@ -902,6 +920,14 @@ type LegTime {
scheduledTime: OffsetDateTime!
}
+"A level with a name and comparable number. Levels can sometimes contain half levels, e.g. '1.5'."
+type Level {
+ "0-based comparable number where 0 is the ground level."
+ level: Float!
+ "Name of the level, e.g. 'M', 'P1', or '1'. Can be equal or different to the numerical representation."
+ name: String!
+}
+
"A span of time."
type LocalTimeSpan {
"The start of the time timespan as seconds from midnight."
@@ -2172,6 +2198,15 @@ type RoutingError {
inputField: InputField
}
+"A single use of a set of stairs."
+type StairsUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
"""
Stop can represent either a single public transport stop, where passengers can
board and/or disembark vehicles, or a station, which contains multiple stops.
@@ -2978,7 +3013,7 @@ type step {
elevationProfile: [elevationProfileComponent]
"When exiting a highway or traffic circle, the exit name/number."
exit: String
- "Information about an feature associated with a step e.g. an station entrance or exit"
+ "Information about a feature associated with a step e.g. a station entrance or exit."
feature: StepFeature
"The latitude of the start of the step."
lat: Float
@@ -3697,6 +3732,7 @@ enum RelativeDirection {
More information about the entrance is in the `step.feature` field.
"""
ENTER_STATION
+ ESCALATOR
"""
Exiting a public transport station. If it's not known if the passenger is entering or exiting
then `CONTINUE` is used.
@@ -3712,6 +3748,7 @@ enum RelativeDirection {
RIGHT
SLIGHTLY_LEFT
SLIGHTLY_RIGHT
+ STAIRS
UTURN_LEFT
UTURN_RIGHT
}
@@ -3927,6 +3964,13 @@ enum VertexType {
TRANSIT
}
+"The vertical direction e.g. for a set of stairs."
+enum VerticalDirection {
+ DOWN
+ UNKNOWN
+ UP
+}
+
"Categorization for via locations."
enum ViaLocationType {
"""
@@ -4932,4 +4976,4 @@ input WheelchairPreferencesInput {
that the itineraries are wheelchair accessible as there can be data issues.
"""
enabled: Boolean
-}
\ No newline at end of file
+}
diff --git a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql
index d7ed4db3ba..f89ec7f85d 100644
--- a/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql
+++ b/digitransit-search-util/packages/digitransit-search-util-query-utils/schema/schema.graphql
@@ -97,7 +97,7 @@ union CallStopLocation = Location | LocationGroup | Stop
union RentalPlace = RentalVehicle | VehicleRentalStation
"A feature for a step"
-union StepFeature = Entrance
+union StepFeature = ElevatorUse | Entrance | EscalatorUse | StairsUse
union StopPosition = PositionAtStop | PositionBetweenStops
@@ -525,6 +525,15 @@ type DependentFareProduct implements FareProduct {
riderCategory: RiderCategory
}
+"A single use of an elevator."
+type ElevatorUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
type Emissions {
"CO₂ emissions in grams."
co2: Grams
@@ -542,6 +551,15 @@ type Entrance {
wheelchairAccessible: WheelchairBoarding
}
+"A single use of an escalator."
+type EscalatorUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
"Real-time estimates for an arrival or departure at a certain place."
type EstimatedTime {
"""
@@ -902,6 +920,14 @@ type LegTime {
scheduledTime: OffsetDateTime!
}
+"A level with a name and comparable number. Levels can sometimes contain half levels, e.g. '1.5'."
+type Level {
+ "0-based comparable number where 0 is the ground level."
+ level: Float!
+ "Name of the level, e.g. 'M', 'P1', or '1'. Can be equal or different to the numerical representation."
+ name: String!
+}
+
"A span of time."
type LocalTimeSpan {
"The start of the time timespan as seconds from midnight."
@@ -2172,6 +2198,15 @@ type RoutingError {
inputField: InputField
}
+"A single use of a set of stairs."
+type StairsUse {
+ "The level the use begins at."
+ from: Level
+ "The level the use ends at."
+ to: Level
+ verticalDirection: VerticalDirection!
+}
+
"""
Stop can represent either a single public transport stop, where passengers can
board and/or disembark vehicles, or a station, which contains multiple stops.
@@ -2978,7 +3013,7 @@ type step {
elevationProfile: [elevationProfileComponent]
"When exiting a highway or traffic circle, the exit name/number."
exit: String
- "Information about an feature associated with a step e.g. an station entrance or exit"
+ "Information about a feature associated with a step e.g. a station entrance or exit."
feature: StepFeature
"The latitude of the start of the step."
lat: Float
@@ -3697,6 +3732,7 @@ enum RelativeDirection {
More information about the entrance is in the `step.feature` field.
"""
ENTER_STATION
+ ESCALATOR
"""
Exiting a public transport station. If it's not known if the passenger is entering or exiting
then `CONTINUE` is used.
@@ -3712,6 +3748,7 @@ enum RelativeDirection {
RIGHT
SLIGHTLY_LEFT
SLIGHTLY_RIGHT
+ STAIRS
UTURN_LEFT
UTURN_RIGHT
}
@@ -3927,6 +3964,13 @@ enum VertexType {
TRANSIT
}
+"The vertical direction e.g. for a set of stairs."
+enum VerticalDirection {
+ DOWN
+ UNKNOWN
+ UP
+}
+
"Categorization for via locations."
enum ViaLocationType {
"""
@@ -4932,4 +4976,4 @@ input WheelchairPreferencesInput {
that the itineraries are wheelchair accessible as there can be data issues.
"""
enabled: Boolean
-}
\ No newline at end of file
+}
diff --git a/docs/ZIndex.md b/docs/ZIndex.md
index 5742e8cf12..a5dc28c560 100644
--- a/docs/ZIndex.md
+++ b/docs/ZIndex.md
@@ -28,3 +28,6 @@ Selector | Component | Z-Index | Comment
`.itinerary-summary-row { .itinerary-legs { .line` | Summary result row leg lines | 1 |
`.itinerary-summary-row { .itinerary-legs { .line { :after` | Hides the Summary result row leg lines behind the mode icon. | -1 |
`.mobile.top-bar | Mobile top bar | 1000 |
+`.map-cluster-number-marker` | Cluster group marker for indoor route steps | 13000 |
+`.map-indoor-route-step-marker` | Indoor route step markers | 13050 |
+`.map-subway-entrance-info-icon-metro` | Entrance markers for indoor route | 13100 |
diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg
index 0e338d98ef..48e760bf2e 100644
--- a/static/assets/svg-sprite.default.svg
+++ b/static/assets/svg-sprite.default.svg
@@ -370,6 +370,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg
index 35ba43f811..04ecbbe738 100644
--- a/static/assets/svg-sprite.hsl.svg
+++ b/static/assets/svg-sprite.hsl.svg
@@ -216,6 +216,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/WalkLeg.test.js b/test/unit/WalkLeg.test.js
index fc634ab982..2e262003a1 100644
--- a/test/unit/WalkLeg.test.js
+++ b/test/unit/WalkLeg.test.js
@@ -11,6 +11,7 @@ describe('', () => {
const props = {
focusAction: () => {},
focusToLeg: () => {},
+ focusToPoint: () => {},
index: 2,
leg: {
distance: 284.787,
@@ -27,6 +28,19 @@ describe('', () => {
rentedBike: false,
start: { scheduledTime: new Date(1529589709000).toISOString() },
end: { scheduledTime: new Date(1529589701000).toISOString() },
+ steps: [
+ {
+ streetName: 'entrance',
+ area: false,
+ absoluteDirection: null,
+ feature: {
+ __typename: 'Entrance',
+ publicCode: 'A',
+ entranceId: 'osm:123',
+ wheelchairAccessible: 'POSSIBLE',
+ },
+ },
+ ],
},
};
@@ -43,6 +57,7 @@ describe('', () => {
const props = {
focusAction: () => {},
focusToLeg: () => {},
+ focusToPoint: () => {},
index: 2,
leg: {
distance: 284.787,
@@ -59,6 +74,19 @@ describe('', () => {
rentedBike: false,
start: { scheduledTime: new Date(1529589709000).toISOString() },
end: { scheduledTime: new Date(1529589701000).toISOString() },
+ steps: [
+ {
+ streetName: 'entrance',
+ area: false,
+ absoluteDirection: null,
+ feature: {
+ __typename: 'Entrance',
+ publicCode: 'A',
+ entranceId: 'osm:123',
+ wheelchairAccessible: 'POSSIBLE',
+ },
+ },
+ ],
},
previousLeg: {
distance: 3297.017000000001,
@@ -94,6 +122,7 @@ describe('', () => {
const props = {
focusAction: () => {},
focusToLeg: () => {},
+ focusToPoint: () => {},
index: 2,
leg: {
distance: 284.787,
@@ -119,6 +148,19 @@ describe('', () => {
rentedBike: false,
start: { scheduledTime: new Date(startTime).toISOString() },
end: { scheduledTime: new Date(1529589701000).toISOString() },
+ steps: [
+ {
+ streetName: 'entrance',
+ area: false,
+ absoluteDirection: null,
+ feature: {
+ __typename: 'Entrance',
+ publicCode: 'A',
+ entranceId: 'osm:123',
+ wheelchairAccessible: 'POSSIBLE',
+ },
+ },
+ ],
},
};
@@ -135,6 +177,7 @@ describe('', () => {
const props = {
focusAction: () => {},
focusToLeg: () => {},
+ focusToPoint: () => {},
index: 1,
leg: {
distance: 1.23,
@@ -157,6 +200,19 @@ describe('', () => {
rentedBike: false,
start: { scheduledTime: new Date(1668600030868).toISOString() },
end: { scheduledTime: new Date(1668600108525).toISOString() },
+ steps: [
+ {
+ streetName: 'entrance',
+ area: false,
+ absoluteDirection: null,
+ feature: {
+ __typename: 'Entrance',
+ publicCode: 'A',
+ entranceId: 'osm:123',
+ wheelchairAccessible: 'POSSIBLE',
+ },
+ },
+ ],
},
};