Skip to content

Commit

Permalink
Merge pull request #328 from ibi-group/mapillary-in-itinerary-body
Browse files Browse the repository at this point in the history
Add Mapillary Image Link to Walking Itineraries
  • Loading branch information
miles-grant-ibigroup authored Feb 7, 2022
2 parents a9dceb9 + dde7363 commit f516703
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 6 deletions.
9 changes: 7 additions & 2 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { setupWorker } from "msw";
import handlers from "../packages/location-field/src/mocks/handlers";
import locationFieldHandlers from "../packages/location-field/src/mocks/handlers";
import itineraryBodyHandlers from '../packages/itinerary-body/src/__mocks__/handlers'
import geocoderHandlers from "../packages/geocoder/src/test-fixtures/handlers";

// Only install worker when running in browser
if (typeof global.process === 'undefined') {
const worker = setupWorker(...handlers, ...geocoderHandlers);
const worker = setupWorker(
...locationFieldHandlers,
...itineraryBodyHandlers,
...geocoderHandlers
);
worker.start({onUnhandledRequest: "bypass"})
}

Expand Down
23 changes: 21 additions & 2 deletions packages/itinerary-body/src/AccessLegBody/access-leg-steps.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import coreUtils from "@opentripplanner/core-utils";
import { DirectionIcon } from "@opentripplanner/icons";
import React from "react";
import PropTypes from "prop-types";

import * as Styled from "../styled";
import MapillaryButton from "./mapillary-button";

export default function AccessLegSteps({ steps }) {
export default function AccessLegSteps({
steps,
mapillaryCallback,
mapillaryKey
}) {
return (
<Styled.Steps>
{steps.map((step, k) => {
Expand All @@ -22,6 +28,12 @@ export default function AccessLegSteps({ steps }) {
<Styled.StepStreetName>
{coreUtils.itinerary.getStepStreetName(step)}
</Styled.StepStreetName>
<MapillaryButton
clickCallback={mapillaryCallback}
coords={step}
mapillaryKey={mapillaryKey}
padLeft
/>
</Styled.StepDescriptionContainer>
</Styled.StepRow>
);
Expand All @@ -31,5 +43,12 @@ export default function AccessLegSteps({ steps }) {
}

AccessLegSteps.propTypes = {
steps: coreUtils.types.stepsType.isRequired
steps: coreUtils.types.stepsType.isRequired,
mapillaryCallback: PropTypes.func,
mapillaryKey: PropTypes.string
};

AccessLegSteps.defaultProps = {
mapillaryCallback: null,
mapillaryKey: null
};
22 changes: 20 additions & 2 deletions packages/itinerary-body/src/AccessLegBody/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VelocityTransitionGroup } from "velocity-react";
import AccessLegSteps from "./access-leg-steps";
import AccessLegSummary from "./access-leg-summary";
import LegDiagramPreview from "./leg-diagram-preview";
import MapillaryButton from "./mapillary-button";
import RentedVehicleSubheader from "./rented-vehicle-subheader";
import * as Styled from "../styled";
import TNCLeg from "./tnc-leg";
Expand Down Expand Up @@ -38,6 +39,8 @@ class AccessLegBody extends Component {
followsTransit,
leg,
LegIcon,
mapillaryCallback,
mapillaryKey,
setLegDiagram,
showElevationProfile,
showLegIcon,
Expand Down Expand Up @@ -86,7 +89,12 @@ class AccessLegBody extends Component {
</span>
)}
</Styled.StepsHeader>

<MapillaryButton
coords={leg.from}
clickCallback={mapillaryCallback}
mapillaryKey={mapillaryKey}
padTop
/>
<LegDiagramPreview
diagramVisible={diagramVisible}
leg={leg}
Expand All @@ -97,7 +105,13 @@ class AccessLegBody extends Component {
enter={{ animation: "slideDown" }}
leave={{ animation: "slideUp" }}
>
{expanded && <AccessLegSteps steps={leg.steps} />}
{expanded && (
<AccessLegSteps
steps={leg.steps}
mapillaryCallback={mapillaryCallback}
mapillaryKey={mapillaryKey}
/>
)}
</VelocityTransitionGroup>
</Styled.LegBody>
</>
Expand All @@ -116,6 +130,8 @@ AccessLegBody.propTypes = {
leg: coreUtils.types.legType.isRequired,
LegIcon: PropTypes.elementType.isRequired,
legIndex: PropTypes.number.isRequired,
mapillaryCallback: PropTypes.func,
mapillaryKey: PropTypes.string,
setActiveLeg: PropTypes.func.isRequired,
setLegDiagram: PropTypes.func.isRequired,
showElevationProfile: PropTypes.bool.isRequired,
Expand All @@ -126,6 +142,8 @@ AccessLegBody.propTypes = {
AccessLegBody.defaultProps = {
diagramVisible: null,
followsTransit: false,
mapillaryCallback: null,
mapillaryKey: null,
timeOptions: null
};

Expand Down
116 changes: 116 additions & 0 deletions packages/itinerary-body/src/AccessLegBody/mapillary-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { StreetView } from "@styled-icons/fa-solid";

/**
* Helper method to generate bounding box from a location. Adding the WINDOW to the coordinate
* creates a bounding box of approximately 1 meter around the coordinate, which is likely to
* encompass any imagery available.
* @param coord The coordinate to convert to a bounding box
* @returns A bounding box 1 meter around the passed coordinate
*/
const generateBoundingBoxFromCoordinate = ({
lat,
lon
}: {
lat: number;
lon: number;
}) => {
const WINDOW = 0.000075;
const south = lat - WINDOW;
const north = lat + WINDOW;
const west = lon - WINDOW;
const east = lon + WINDOW;
return [west, south, east, north];
};

const Container = styled.a<{ padLeft?: boolean; padTop?: boolean }>`
display: inline-block;
margin-top: ${props => (props.padTop ? "10px" : "0")};
&:hover {
cursor: pointer;
text-decoration: none;
}
&:active {
color: #111;
}
&::before {
content: "| ";
cursor: auto;
margin-left: ${props => (props.padLeft ? "1ch" : "0")};
}
`;

const Icon = styled(StreetView)`
height: 16px;
padding-left: 2px;
`;

/**
* A component which shows a "street view" button if a Mapillary image is available for a
* passed coordinate
*
* @param coords The coordinates to find imagery for in the format [lat, lon]
* @param mapillaryKey A Mapillary api key used to check for imagery.
* @param padTop Whether to add padding to the top of the container.
* @param clickCallback A method to fire when the button is clicked, which accepts an ID.
* If it is not passsed, a popup window will be opened. */
const MapillaryButton = ({
clickCallback,
coords,
mapillaryKey,
padLeft,
padTop
}: {
clickCallback?: (id: string) => void;
coords: { lat: number; lon: number };
mapillaryKey: string;
padLeft?: boolean;
padTop?: boolean;
}): JSX.Element => {
const [imageId, setImageId] = useState(null);

useEffect(() => {
// useEffect only supports async actions as a child function
const getMapillaryId = async () => {
const bounds = generateBoundingBoxFromCoordinate(coords).join(",");
const raw = await fetch(
`https://graph.mapillary.com/images?fields=id&limit=1&access_token=${mapillaryKey}&bbox=${bounds}`
);
const json = await raw.json();
if (json?.data?.length > 0) {
setImageId(json.data[0].id);
}
};

if (!imageId && !!mapillaryKey) getMapillaryId();
}, [coords]);

const handleClick = () => {
if (clickCallback) clickCallback(imageId);
else {
window.open(
`https://www.mapillary.com/embed?image_key=${imageId}`,
"_blank",
"location=no,height=600,width=600,scrollbars=no,status=no"
);
}
};

if (!imageId) return null;
return (
<Container
onClick={handleClick}
padLeft={padLeft}
padTop={padTop}
title="Show street imagery at this location"
>
<Icon style={{ paddingBottom: 1 }} />
</Container>
);
};

export default MapillaryButton;
13 changes: 13 additions & 0 deletions packages/itinerary-body/src/ItineraryBody/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const ItineraryBody = ({
itinerary,
LegIcon,
LineColumnContent,
mapillaryCallback,
mapillaryKey,
PlaceName,
RouteDescription,
routingType,
Expand Down Expand Up @@ -68,6 +70,8 @@ const ItineraryBody = ({
LegIcon={LegIcon}
legIndex={i}
LineColumnContent={LineColumnContent}
mapillaryCallback={mapillaryCallback}
mapillaryKey={mapillaryKey}
PlaceName={PlaceName}
RouteDescription={RouteDescription}
routingType={routingType}
Expand Down Expand Up @@ -138,6 +142,13 @@ ItineraryBody.propTypes = {
* - toRouteAbbreviation - a function to help abbreviate route names
*/
LineColumnContent: PropTypes.elementType.isRequired,
/** Handler for when a Mapillary button is clicked. */
mapillaryCallback: PropTypes.func,
/**
* Mapillary key used to fetch imagery if available. Key can be obtained from
* https://www.mapillary.com/dashboard/developers
*/
mapillaryKey: PropTypes.string,
/**
* A custom component for rendering the place name of legs.
* The component is sent 3 props:
Expand Down Expand Up @@ -236,6 +247,8 @@ ItineraryBody.defaultProps = {
className: null,
diagramVisible: null,
frameLeg: noop,
mapillaryCallback: null,
mapillaryKey: null,
routingType: "ITINERARY",
showAgencyInfo: false,
showElevationProfile: false,
Expand Down
8 changes: 8 additions & 0 deletions packages/itinerary-body/src/ItineraryBody/place-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const PlaceRow = ({
LegIcon,
legIndex,
LineColumnContent,
mapillaryCallback,
mapillaryKey,
messages,
PlaceName,
RouteDescription,
Expand Down Expand Up @@ -130,6 +132,8 @@ const PlaceRow = ({
leg={leg}
LegIcon={LegIcon}
legIndex={legIndex}
mapillaryCallback={mapillaryCallback}
mapillaryKey={mapillaryKey}
setActiveLeg={setActiveLeg}
setLegDiagram={setLegDiagram}
showElevationProfile={showElevationProfile}
Expand Down Expand Up @@ -184,6 +188,8 @@ PlaceRow.propTypes = {
/** The index value of this specific leg within the itinerary */
legIndex: PropTypes.number.isRequired,
LineColumnContent: PropTypes.elementType.isRequired,
mapillaryCallback: PropTypes.func,
mapillaryKey: PropTypes.string,
messages: messagesType,
PlaceName: PropTypes.elementType.isRequired,
RouteDescription: PropTypes.elementType.isRequired,
Expand Down Expand Up @@ -211,6 +217,8 @@ PlaceRow.defaultProps = {
followsTransit: false,
// can be null if this is the origin place
lastLeg: null,
mapillaryCallback: null,
mapillaryKey: null,
messages: {
mapIconTitle: "View on map"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/itinerary-body/src/__mocks__/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { rest } from "msw";

import mapillary from "./mapillary.json";

// This faked endpoint will always return the same ID
export default [
rest.get("https://graph.mapillary.com/images", (req, res, ctx) => {
return res(ctx.json(mapillary));
})
];
11 changes: 11 additions & 0 deletions packages/itinerary-body/src/__mocks__/mapillary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": [
{
"id": "769386473761940",
"geometry": {
"type": "Point",
"coordinates": [-7.2089327683333, 62.245706682778]
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default class ItineraryBodyDefaultsWrapper extends Component {
itinerary={itinerary}
LegIcon={LegIcon}
LineColumnContent={LineColumnContent || DefaultLineColumnContent}
mapillaryKey="fake key, but ok because the api response is also fake"
PlaceName={PlaceName || DefaultPlaceName}
RouteDescription={RouteDescription || DefaultRouteDescription}
routingType="ITINERARY"
Expand Down

0 comments on commit f516703

Please sign in to comment.