diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 81d7aee92..f94236acb 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -269,9 +269,6 @@ components: nonTransit: Alternative options DeleteUser: deleteMyAccount: Delete my account - EnhancedStopMarker: - stopID: "Stop ID:" - stopViewer: Stop Viewer ErrorMessage: header: Could Not Plan Trip warning: Warning diff --git a/i18n/es.yml b/i18n/es.yml index 5cb005567..8ddaa7901 100644 --- a/i18n/es.yml +++ b/i18n/es.yml @@ -274,9 +274,6 @@ components: nonTransit: Alternativas DeleteUser: deleteMyAccount: Eliminar mi cuenta - EnhancedStopMarker: - stopID: "Parada:" - stopViewer: Visor de paradas ErrorMessage: header: No se pudo planificar el viaje warning: Advertencia diff --git a/i18n/fr.yml b/i18n/fr.yml index a9d07c904..f034e6b44 100644 --- a/i18n/fr.yml +++ b/i18n/fr.yml @@ -282,9 +282,6 @@ components: nonTransit: Alternatives DeleteUser: deleteMyAccount: Supprimer mon compte - EnhancedStopMarker: - stopID: Arrêt nº - stopViewer: Info arrêt ErrorMessage: header: Impossible de planifier le trajet warning: Attention diff --git a/i18n/ko.yml b/i18n/ko.yml index 1b339d183..c36644127 100644 --- a/i18n/ko.yml +++ b/i18n/ko.yml @@ -240,9 +240,6 @@ components: multiModeSummary: "{accessMode} + {transitMode}" DeleteUser: deleteMyAccount: 내 계정 삭제 - EnhancedStopMarker: - stopID: "정류장 ID:" - stopViewer: 정류장 뷰어 ErrorMessage: header: 트립을 계획할 수 없음 warning: 경고 diff --git a/i18n/nb_NO.yml b/i18n/nb_NO.yml index 8bc2acc78..83be84573 100644 --- a/i18n/nb_NO.yml +++ b/i18n/nb_NO.yml @@ -74,9 +74,6 @@ components: now: Nå DeleteUser: deleteMyAccount: Slett kontoen min - EnhancedStopMarker: - stopID: "Stopp-ID:" - stopViewer: Stopp-viser ErrorMessage: header: Kunne ikke planlegge tur warning: Advarsel diff --git a/i18n/vi.yml b/i18n/vi.yml index 69f5c7f9e..ba852fde4 100644 --- a/i18n/vi.yml +++ b/i18n/vi.yml @@ -256,9 +256,6 @@ components: multiModeSummary: "{accessMode} + {transitMode}" DeleteUser: deleteMyAccount: Xóa tài khoản của tôi - EnhancedStopMarker: - stopID: Điểm dừng số - stopViewer: Xem điểm dừng ErrorMessage: header: Không thể lên kế hoạch cho chuyến đi warning: Cảnh báo diff --git a/i18n/zh.yml b/i18n/zh.yml index 58e9a6d0c..021e91a97 100644 --- a/i18n/zh.yml +++ b/i18n/zh.yml @@ -240,9 +240,6 @@ components: multiModeSummary: "{accessMode} + {transitMode}" DeleteUser: deleteMyAccount: 删除我的账户 - EnhancedStopMarker: - stopID: "车站 ID:" - stopViewer: 车站查看器 ErrorMessage: header: 无法计划行程 warning: 警告 diff --git a/lib/components/map/connected-park-and-ride-overlay.tsx b/lib/components/map/connected-park-and-ride-overlay.tsx index bc6a465e7..5ca13ce1e 100644 --- a/lib/components/map/connected-park-and-ride-overlay.tsx +++ b/lib/components/map/connected-park-and-ride-overlay.tsx @@ -5,6 +5,7 @@ import React, { useEffect } from 'react' import { parkAndRideQuery } from '../../actions/api' import { setLocation } from '../../actions/map' +import { SetLocationHandler } from '../util/types' type ParkAndRideParams = { maxTransitDistance?: number @@ -14,11 +15,7 @@ type Props = ParkAndRideParams & { keyboard?: boolean parkAndRideLocations?: { name: string; x: number; y: number }[] parkAndRideQuery: (params: ParkAndRideParams) => void - setLocation: (location: { - location: Location - locationType: string - reverseGeocode: boolean - }) => void + setLocation: SetLocationHandler } function ConnectedParkAndRideOverlay(props: Props): JSX.Element { diff --git a/lib/components/map/default-map.tsx b/lib/components/map/default-map.tsx index 500f7bcbc..265a5fd50 100644 --- a/lib/components/map/default-map.tsx +++ b/lib/components/map/default-map.tsx @@ -47,6 +47,12 @@ const MapContainer = styled.div` * { box-sizing: unset; } + + .maplibregl-popup-content, + .mapboxgl-popup-content { + border-radius: 10px; + box-shadow: 0 3px 14px 4px rgb(0 0 0 / 20%); + } ` /** * Get the configured display names for the specified company ids. diff --git a/lib/components/map/enhanced-stop-marker.js b/lib/components/map/enhanced-stop-marker.js deleted file mode 100644 index ad5d7fa4f..000000000 --- a/lib/components/map/enhanced-stop-marker.js +++ /dev/null @@ -1,183 +0,0 @@ -// TYPESCRIPT TODO: all props here are missing types -/* eslint-disable react/prop-types */ -import { - Styled as BaseMapStyled, - MarkerWithPopup -} from '@opentripplanner/base-map' -import { connect } from 'react-redux' -import { FormattedMessage } from 'react-intl' -import { MapMarker } from '@styled-icons/fa-solid/MapMarker' -import { Styled as MapPopupStyled } from '@opentripplanner/map-popup' -import coreUtils from '@opentripplanner/core-utils' -import FromToLocationPicker from '@opentripplanner/from-to-location-picker' -import React, { Component } from 'react' -import styled from 'styled-components' -import tinycolor from 'tinycolor2' - -import * as mapActions from '../../actions/map' -import * as uiActions from '../../actions/ui' -import { ComponentContext } from '../../util/contexts' -import { getModeFromStop, getStopName } from '../../util/viewer' - -const { ViewStopButton } = MapPopupStyled - -const iconPixels = 32 -const iconPadding = 5 -const caretPixels = iconPixels / 2 + iconPadding -const borderPixels = (props) => (props?.active ? 3 : 1) -const leftPixels = (props) => caretPixels / 2 - borderPixels(props) / 2 -const bottomPixels = (props) => - -((caretPixels * 1.4142) / 4) - borderPixels(props) + iconPadding / 2 - -const DEFAULT_COLOR = '#a6a6a6' -const strokeColor = (props) => (props?.active ? props.mainColor : DEFAULT_COLOR) - -const BaseStopIcon = styled.div` - background: #fff; - border: ${borderPixels}px solid ${strokeColor}; - border-radius: 50%; - fill: ${strokeColor}; - height: ${iconPixels}px; - padding: ${iconPadding}px; - position: relative; - width: ${iconPixels}px; - - svg { - max-height: 100%; - max-width: 100%; - position: relative; - z-index: 200; - } - - &::after { - background: #fff; - border-bottom: ${borderPixels}px solid ${strokeColor}; - border-right: ${borderPixels}px solid ${strokeColor}; - bottom: ${bottomPixels}px; - box-sizing: content-box; - content: ''; - display: block; - height: ${caretPixels}px; - left: ${leftPixels}px; - position: absolute; - transform: rotate(45deg); - width: ${caretPixels}px; - } -` - -class EnhancedStopMarker extends Component { - static contextType = ComponentContext - - onClickView = () => { - const { setViewedStop, stop } = this.props - setViewedStop({ stopId: stop.id }) - } - - onFromClick = () => { - this.setLocation('from') - } - - onToClick = () => { - this.setLocation('to') - } - - setLocation(locationType) { - const { setLocation, stop } = this.props - const { lat, lon, name } = stop - setLocation({ - location: { lat, lon, name }, - locationType, - reverseGeocode: false - }) - } - - render() { - const { activeStopId, highlight, languageConfig, modeColors, stop } = - this.props - const { id, lat, lon, name } = stop - const displayedStopId = coreUtils.itinerary.getDisplayedStopId(stop) - if (!displayedStopId) return null - - const mode = getModeFromStop(stop) - let color = modeColors && modeColors[mode] ? modeColors[mode] : '#121212' - if (highlight) { - // Generates a pretty variant of the color - color = tinycolor(color).monochromatic()[3].toHexString() - } - - const { ModeIcon } = this.context - const stopIcon = mode ? : - - const icon = ( - - {stopIcon} - - ) - - return ( - - {name} - - - - - {' '} - {displayedStopId} - - - {languageConfig.stopViewer || ( - - )} - - - - {/* The 'Set as [from/to]' ButtonGroup */} - - - - - ) - } - position={[lat, lon]} - > - {icon} - - ) - } -} - -const mapStateToProps = (state, ownProps) => { - const { highlightedStop } = state.otp.ui - - const transitModes = state.otp.config.modes.transitModes - const modeColors = {} - transitModes.forEach((mode) => { - modeColors[mode.mode] = mode.color - }) - - return { - activeStopId: state.otp.ui.viewedStop?.stopId, - highlight: highlightedStop === ownProps.stop.id, - languageConfig: state.otp.config.language, - modeColors - } -} - -const mapDispatchToProps = { - setLocation: mapActions.setLocation, - setViewedStop: uiActions.setViewedStop -} - -export default connect(mapStateToProps, mapDispatchToProps)(EnhancedStopMarker) diff --git a/lib/components/map/enhanced-stop-marker.tsx b/lib/components/map/enhanced-stop-marker.tsx new file mode 100644 index 000000000..82beaafa6 --- /dev/null +++ b/lib/components/map/enhanced-stop-marker.tsx @@ -0,0 +1,190 @@ +import { + Styled as BaseMapStyled, + MarkerWithPopup +} from '@opentripplanner/base-map' +import { connect } from 'react-redux' +import { MapMarker } from '@styled-icons/fa-solid/MapMarker' +import { Stop } from '@opentripplanner/types' +import coreUtils from '@opentripplanner/core-utils' +import React, { Component } from 'react' +import StopPopup from '@opentripplanner/map-popup' +import styled from 'styled-components' +import tinycolor from 'tinycolor2' + +import * as mapActions from '../../actions/map' +import * as uiActions from '../../actions/ui' +import { ComponentContext } from '../../util/contexts' +import { + ConfiguredTransitMode, + SetLocationHandler, + SetViewedStopHandler +} from '../util/types' +import { getModeFromStop, getStopName } from '../../util/viewer' + +interface OwnProps { + stop: Stop +} + +type ModeColors = Record + +interface Props extends OwnProps { + activeStopId?: string + highlight: boolean + modeColors: ModeColors + setLocation: SetLocationHandler + setViewedStop: SetViewedStopHandler +} + +interface MarkerProps { + active?: boolean + mainColor?: string +} + +const iconPixels = 32 +const iconPadding = 5 +const caretPixels = iconPixels / 2 + iconPadding +const borderPixels = (props: MarkerProps) => (props?.active ? 3 : 1) +const leftPixels = (props: MarkerProps) => + caretPixels / 2 - borderPixels(props) / 2 +const bottomPixels = (props: MarkerProps) => + -((caretPixels * 1.4142) / 4) - borderPixels(props) + iconPadding / 2 + +const DEFAULT_COLOR = '#a6a6a6' +const strokeColor = (props: MarkerProps) => + props?.active ? props.mainColor : DEFAULT_COLOR + +const BaseStopIcon = styled.div` + background: #fff; + border: ${borderPixels}px solid ${strokeColor}; + border-radius: 50%; + fill: ${strokeColor}; + height: ${iconPixels}px; + padding: ${iconPadding}px; + position: relative; + width: ${iconPixels}px; + + svg { + max-height: 100%; + max-width: 100%; + position: relative; + z-index: 200; + } + + &::after { + background: #fff; + border-bottom: ${borderPixels}px solid ${strokeColor}; + border-right: ${borderPixels}px solid ${strokeColor}; + bottom: ${bottomPixels}px; + box-sizing: content-box; + content: ''; + display: block; + height: ${caretPixels}px; + left: ${leftPixels}px; + position: absolute; + transform: rotate(45deg); + width: ${caretPixels}px; + } +` + +const activeContentId = 'enh-stop-popup' + +class EnhancedStopMarker extends Component { + static contextType = ComponentContext + + onMarkerClick = () => { + // Make a copy because getElementsByClassName returns a live collection. + const closeButtons = Array.from( + document.getElementsByClassName('maplibregl-popup-close-button') + ) + // HACK: If an OTP2 tile stop is right underneath the marker, the tile event handler in OTP-UI + // will fire before this one, and the popup from the regular stop layer will appear + // (even if we render the StopViewerOverlay after the OTP2 overlays in DefaultMap), + // so there will be two (duplicate) stop popups. + // We want to show the popup for the enhanced marker instead of the one from the tile handler + // because the stop marker has a much larger UI surface than the circle from the tile layer. + // FIXME: One case that escapes this trick deals with when the active stop marker is below an inactive stop marker. + // When clicking the corner of the inactive stop marker, this handler is not triggered, + // and two popups for two different stops will be shown. + closeButtons.forEach((btn) => { + const popup = btn.parentElement?.firstChild?.firstChild as HTMLElement + if (popup?.id !== activeContentId) { + const button = btn as HTMLButtonElement + button.click() + } + }) + } + + render() { + const { + activeStopId, + highlight, + modeColors, + setLocation, + setViewedStop, + stop + } = this.props + const { id, lat, lon } = stop + const displayedStopId = coreUtils.itinerary.getDisplayedStopId(stop) + if (!displayedStopId) return null + + const mode = getModeFromStop(stop) + let color = modeColors && modeColors[mode] ? modeColors[mode] : '#121212' + if (highlight) { + // Generates a pretty variant of the color + color = tinycolor(color).monochromatic()[3].toHexString() + } + + const { ModeIcon } = this.context + const stopIcon = mode ? : + + return ( + + + + ) + } + position={[lat, lon]} + > + + {stopIcon} + + + ) + } +} + +const mapStateToProps = (state: any, ownProps: OwnProps) => { + const { highlightedStop } = state.otp.ui + + const transitModes = state.otp.config.modes.transitModes + const modeColors: ModeColors = {} + transitModes.forEach((mode: ConfiguredTransitMode) => { + modeColors[mode.mode] = mode.color + }) + + return { + activeStopId: state.otp.ui.viewedStop?.stopId, + highlight: highlightedStop === ownProps.stop.id, + modeColors + } +} + +const mapDispatchToProps = { + setLocation: mapActions.setLocation, + setViewedStop: uiActions.setViewedStop +} + +export default connect(mapStateToProps, mapDispatchToProps)(EnhancedStopMarker) diff --git a/lib/components/map/point-popup.tsx b/lib/components/map/point-popup.tsx index e8c91cee0..8422aff0d 100644 --- a/lib/components/map/point-popup.tsx +++ b/lib/components/map/point-popup.tsx @@ -11,6 +11,7 @@ import type { Location } from '@opentripplanner/types' import * as mapActions from '../../actions/map' import { Icon } from '../util/styledIcon' import { renderCoordinates } from '../../util/i18n' +import { SetLocationHandler } from '../util/types' const PopupTitleWrapper = styled.div` align-items: flex-start; @@ -34,7 +35,7 @@ const ZoomButton = styled.button` type Props = { clearMapPopupLocation: () => void mapPopupLocation: Location - setLocation: (arg: unknown) => void + setLocation: SetLocationHandler zoomToPlace: ( map?: MapRef, place?: { lat: number; lon: number }, diff --git a/lib/components/mobile/stop-viewer.tsx b/lib/components/mobile/stop-viewer.tsx index f48553786..628d5b42d 100644 --- a/lib/components/mobile/stop-viewer.tsx +++ b/lib/components/mobile/stop-viewer.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl' import React, { useCallback } from 'react' import * as uiActions from '../../actions/ui' +import { SetViewedStopHandler } from '../util/types' import DefaultMap from '../map/default-map' import StopViewer from '../viewers/stop-viewer' @@ -10,7 +11,7 @@ import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' interface Props { - setViewedStop: (payload: { stopId: string } | null) => void + setViewedStop: SetViewedStopHandler } const MobileStopViewer = ({ setViewedStop }: Props) => { diff --git a/lib/components/narrative/line-itin/connected-transit-leg-subheader.tsx b/lib/components/narrative/line-itin/connected-transit-leg-subheader.tsx index 884caa8ef..1ffc8b3a2 100644 --- a/lib/components/narrative/line-itin/connected-transit-leg-subheader.tsx +++ b/lib/components/narrative/line-itin/connected-transit-leg-subheader.tsx @@ -4,15 +4,16 @@ import React, { Component } from 'react' import TransitLegSubheader from '@opentripplanner/itinerary-body/lib/otp-react-redux/transit-leg-subheader' import { setMainPanelContent, setViewedStop } from '../../../actions/ui' +import { SetViewedStopHandler } from '../../util/types' interface Props { leg: Leg setMainPanelContent: (content: number | null) => void - setViewedStop: (payload: { stopId: string }) => void + setViewedStop: SetViewedStopHandler } class ConnectedTransitLegSubheader extends Component { - onClick = (payload: { stopId: string }) => { + onClick: SetViewedStopHandler = (payload) => { const { setMainPanelContent, setViewedStop } = this.props setMainPanelContent(null) setViewedStop(payload) diff --git a/lib/components/util/types.ts b/lib/components/util/types.ts index febaa585d..8988c7126 100644 --- a/lib/components/util/types.ts +++ b/lib/components/util/types.ts @@ -1,4 +1,4 @@ -import { Route } from '@opentripplanner/types' +import { MapLocationActionArg, Route } from '@opentripplanner/types' // TYPESCRIPT TODO: move this to a larger shared types file, preferably within otp-ui export interface StopData { @@ -92,4 +92,13 @@ export interface ViewedRouteObject extends Route { export type SetViewedRouteHandler = (route?: ViewedRouteState) => void -export type SetViewedStopHandler = (payload: { stopId: string }) => void +export type SetViewedStopHandler = (payload: { stopId: string } | null) => void + +export type SetLocationHandler = (payload: MapLocationActionArg) => void + +export interface ConfiguredTransitMode { + color?: string + label?: string + mode: string + showWheelchairSetting?: boolean +}