diff --git a/src/components/map/globe-map.js b/src/components/map/globe-map.js index 2fcf7c57..af31216f 100644 --- a/src/components/map/globe-map.js +++ b/src/components/map/globe-map.js @@ -13,7 +13,11 @@ import { SphereGeometry } from "@luma.gl/engine" import PropTypes from "prop-types" import styled from "styled-components" import { getUniquePlatforms } from "../../utils/get-unique-platforms" -import { getLineColorAsRGB, getPlatformIcon } from "../../utils/platform-colors" +import { + getLineColorAsRGB, + getPlatformIcon, + isPlatformVisible, +} from "../../utils/platform-colors" const INITIAL_VIEW_STATE = { longitude: -98, @@ -22,7 +26,14 @@ const INITIAL_VIEW_STATE = { } const MAPBOX_TOKEN = process.env.GATSBY_MAPBOX_TOKEN -export function GlobeMap({ geojson, deployments, mapStyleID }) { +export function GlobeMap({ + geojson, + deployments, + selectedPlatforms, + selectedDeployment, + mapStyleID, + children, +}) { const [initialViewState, setInitialViewState] = useState(INITIAL_VIEW_STATE) const [iconMapping, setIconMapping] = useState({}) const platforms = getUniquePlatforms( @@ -112,7 +123,15 @@ export function GlobeMap({ geojson, deployments, mapStyleID }) { id: "flights", data: { ...geojson, - features: geojson.features.filter(f => f.geometry.type !== "Point"), + features: geojson.features + .filter(f => f.geometry.type !== "Point") + .filter(f => + isPlatformVisible({ + platformProperties: f.properties, + selectedDeployment, + selectedPlatforms, + }) + ), }, lineWidthMinPixels: 0.5, getLineWidth: 1, @@ -126,9 +145,14 @@ export function GlobeMap({ geojson, deployments, mapStyleID }) { pickable: true, iconAtlas: `https://api.mapbox.com/styles/v1/${mapStyleID}/sprite@2x.png?access_token=${MAPBOX_TOKEN}`, iconMapping: iconMapping, - getIcon: f => { - return getPlatformIcon(f.properties.platform_name) - }, + getIcon: f => + isPlatformVisible({ + platformProperties: f.properties, + selectedDeployment, + selectedPlatforms, + }) + ? getPlatformIcon(f.properties.platform_name) + : null, getPosition: f => f.geometry.coordinates, getSize: 12, }) @@ -145,6 +169,7 @@ export function GlobeMap({ geojson, deployments, mapStyleID }) { initialViewState={initialViewState} layers={[backgroundLayers, flights, staticLocations]} > + {children} ) } @@ -153,7 +178,10 @@ export function GlobeMap({ geojson, deployments, mapStyleID }) { GlobeMap.propTypes = { geojson: PropTypes.object, deployments: PropTypes.array, + selectedDeployment: PropTypes.array, + selectedPlatforms: PropTypes.array, mapStyleID: PropTypes.string, + children: PropTypes.node, } const MapContainer = styled.div` diff --git a/src/components/timeline/__tests__/map.test.js b/src/components/timeline/__tests__/map-legend.test.js similarity index 98% rename from src/components/timeline/__tests__/map.test.js rename to src/components/timeline/__tests__/map-legend.test.js index 8e2c4357..cb51c5a3 100644 --- a/src/components/timeline/__tests__/map.test.js +++ b/src/components/timeline/__tests__/map-legend.test.js @@ -1,7 +1,7 @@ import React from "react" import renderer, { act } from "react-test-renderer" -import { MapLegend, PlatformStatus } from "../map" +import { MapLegend, PlatformStatus } from "../map-legend" import { LineIcon } from "../../../icons" import { BalloonIcon, diff --git a/src/components/timeline/map-legend.js b/src/components/timeline/map-legend.js new file mode 100644 index 00000000..55f65d21 --- /dev/null +++ b/src/components/timeline/map-legend.js @@ -0,0 +1,272 @@ +import React from "react" +import PropTypes from "prop-types" +import styled from "styled-components" +import { Tooltip } from "react-tooltip" + +import { LineIcon } from "../../icons" +import { colors } from "../../theme" +import { usePlatformStatus } from "../../utils/use-platform-status" +import { + FALLBACK_COLOR, + MOVING_PLATFORMS_COLORS, + STATIC_PLATFORMS, +} from "../../utils/platform-colors" + +export const LegendItem = ({ + name, + type, + color, + icon, + checked, + disabled, + onClick, + activeDeploymentPlatforms, + platformsWithData, +}) => ( +
+ onClick()} + /> + + {type === "moving" ? ( + + ) : ( + {icon} + )} + {name} + + +
+) + +LegendItem.propTypes = { + name: PropTypes.string, + type: PropTypes.string, + color: PropTypes.string, + icon: PropTypes.node, + checked: PropTypes.bool, + disabled: PropTypes.bool, + platformsWithData: PropTypes.array, + activeDeploymentPlatforms: PropTypes.array, + onClick: PropTypes.func, +} + +export const MapLegend = ({ + platforms = [], + platformsWithData = [], + activeDeploymentPlatforms = [], + setSelectedPlatforms, + selectedPlatforms, +}) => { + const names = platforms.map(i => i.name) + const uniquePlatforms = platforms.filter( + (i, index) => names.indexOf(i.name) === index + ) + const movingPlatforms = uniquePlatforms.filter(platform => + ["Jet", "Prop", "UAV", "Ships/Boats"].includes(platform.type) + ) + const staticPlatforms = uniquePlatforms.filter( + platform => !["Jet", "Prop", "UAV", "Ships/Boats"].includes(platform.type) + ) + + return ( + +
+ Platforms + {movingPlatforms.length > 0 &&

Moving

} + {movingPlatforms.map((platform, index) => ( + + selectedPlatforms.includes(platform.name) + ? setSelectedPlatforms( + selectedPlatforms.filter(i => i !== platform.name) + ) + : setSelectedPlatforms([...selectedPlatforms, platform.name]) + } + activeDeploymentPlatforms={activeDeploymentPlatforms} + platformsWithData={platformsWithData} + /> + ))} + {staticPlatforms.length > 0 &&

Stationary

} + {staticPlatforms.map(platform => ( + i.name === platform.name)?.color} + icon={STATIC_PLATFORMS?.find(i => i.name === platform.name)?.icon} + checked={selectedPlatforms.includes(platform.name)} + disabled={ + (selectedPlatforms.length === 1 && + selectedPlatforms.includes(platform.name)) || + !platformsWithData.includes(platform.name) + } + onClick={() => + selectedPlatforms.includes(platform.name) + ? setSelectedPlatforms( + selectedPlatforms.filter(i => i !== platform.name) + ) + : setSelectedPlatforms([...selectedPlatforms, platform.name]) + } + activeDeploymentPlatforms={activeDeploymentPlatforms} + platformsWithData={platformsWithData} + /> + ))} +
+
+ ) +} + +MapLegend.propTypes = { + platforms: PropTypes.array, + platformsWithData: PropTypes.array, + activeDeploymentPlatforms: PropTypes.array, + setSelectedPlatforms: PropTypes.func, + selectedPlatforms: PropTypes.array, +} + +export const PlatformStatus = ({ + platformName, + platformsWithData, + activeDeploymentPlatforms, +}) => { + const status = usePlatformStatus( + platformName, + platformsWithData, + activeDeploymentPlatforms + ) + + if (status !== "operational") { + return ( + + + {status === "notShown" ? "(Not Shown)" : "(Not Operating)"} + + + + ) + } + return <> +} + +PlatformStatus.propTypes = { + platformName: PropTypes.string, + platformsWithData: PropTypes.array, + activeDeploymentPlatforms: PropTypes.array, +} + +const LegendText = styled.label` + font-weight: ${props => (props.checked ? 600 : 400)}; + font-family: "Titillium Web", sans-serif; + display: inline-flex; + gap: 0.25rem; + margin-left: 0.5rem; + background: transparent; + border: none; + & svg { + vertical-align: middle; + } +` + +const IconSpan = styled.span` + color: ${props => props.color}; + background: #294060; + border: 0.5px solid; + border-color: ${props => props.color}; + display: inline-flex; + height: 1.25rem; + width: 1.25rem; + justify-content: center; + align-items: center; + border-radius: 512px; + & svg { + vertical-align: unset; + font-weight: 600; + } +` + +const LegendBox = styled.div` + display: inline-block; + text-align: left; + min-width: 14rem; + position: absolute; + right: 5px; + margin-top: 5px; + margin-right: 5px; + padding: 8px; + color: ${colors.lightTheme.text}; + background-color: rgba(255, 255, 255, 0.75); + transition: all 0.24s ease-out; + > fieldset { + border: 0px; + } + > fieldset > legend { + padding: 0 0 4px; + font-family: "Titillium Web", sans-serif; + color: ${colors.lightTheme.text}; + font-size: 1.1rem; + font-weight: 600; + } + & input { + cursor: pointer; + } + & h4 { + font-weight: 600; + color: ${colors.lightTheme.text}; + font-size: 0.8rem; + margin: 4px 0 2px; + } + &:hover { + background-color: rgba(255, 255, 255, 0.95); + } +` + +const Tag = styled.div` + display: inline; + > u { + text-decoration-line: underline; + text-decoration-style: dotted; + text-decoration-color: #4d4d4d; + color: #333; + text-decoration-thickness: 2px; + padding-left: 7px; + &:hover { + cursor: pointer; + } + } +` diff --git a/src/components/timeline/map.js b/src/components/timeline/map.js index 945e340b..99f3c3a0 100644 --- a/src/components/timeline/map.js +++ b/src/components/timeline/map.js @@ -1,33 +1,30 @@ import React, { useEffect, useState } from "react" import PropTypes from "prop-types" -import styled from "styled-components" -import { Tooltip } from "react-tooltip" import Map from "../map" import Source from "../map/source" import Layer from "../map/layer" import { getUniquePlatforms } from "../../utils/get-unique-platforms" -import { LineIcon } from "../../icons" import { mapLayerFilter } from "../../utils/filter-utils" -import { colors } from "../../theme" import { replaceSlashes } from "../../utils/helpers" -import { usePlatformStatus } from "../../utils/use-platform-status" import { - FALLBACK_COLOR, - MOVING_PLATFORMS_COLORS, - STATIC_PLATFORMS, getLineColors, getStaticIcons, getIconColors, } from "../../utils/platform-colors" +import Button from "../button" +import { GlobeMap } from "../map/globe-map" +import { POSITIVE } from "../../utils/constants" +import { MapLegend } from "./map-legend" export function DeploymentMap({ geojson, deployments, bounds, selectedDeployment, - mapStyleID, }) { + const MAP_STYLE_ID = "devseed/clx25ggbv076o01ql8k8m03k8" + const [enable3DView, setEnable3DView] = useState(false) const platforms = getUniquePlatforms( deployments.flatMap(d => d.collectionPeriods) ).map(i => ({ name: i.item.shortname, type: i.item.platformType.shortname })) @@ -53,8 +50,6 @@ export function DeploymentMap({ const lineColorsPaint = getLineColors( movingPlatforms.filter((i, index) => movingPlatforms.indexOf(i) === index) ) - const iconImage = getStaticIcons() - const iconColors = getIconColors() const [selectedPlatforms, setSelectedPlatforms] = useState( names @@ -62,6 +57,74 @@ export function DeploymentMap({ .filter(name => platformsWithData.includes(name)) ) + return ( + <> + {enable3DView ? ( + + + + ) : ( + + + + )} +
+ +
+ + ) +} + +DeploymentMap.propTypes = { + geojson: PropTypes.object, + deployments: PropTypes.array, + bounds: PropTypes.array, + selectedDeployment: PropTypes.object, +} + +const MapboxMap = ({ + geojson, + mapStyleID, + bounds, + lineColorsPaint, + selectedDeployment, + selectedPlatforms, + children, +}) => { + const iconImage = getStaticIcons() + const iconColors = getIconColors() return ( - + {children} ) } -DeploymentMap.propTypes = { +MapboxMap.propTypes = { geojson: PropTypes.object, - deployments: PropTypes.array, - bounds: PropTypes.array, + children: PropTypes.node, selectedDeployment: PropTypes.object, + selectedPlatforms: PropTypes.array, + setSelectedPlatforms: PropTypes.func, + lineColorsPaint: PropTypes.array, + bounds: PropTypes.array, mapStyleID: PropTypes.string.isRequired, } @@ -237,262 +297,3 @@ DeploymentLayer.propTypes = { selectedPlatforms: PropTypes.array, onLoad: PropTypes.func, } - -export const LegendItem = ({ - name, - type, - color, - icon, - checked, - disabled, - onClick, - activeDeploymentPlatforms, - platformsWithData, -}) => ( -
- onClick()} - /> - - {type === "moving" ? ( - - ) : ( - {icon} - )} - {name} - - -
-) - -LegendItem.propTypes = { - name: PropTypes.string, - type: PropTypes.string, - color: PropTypes.string, - icon: PropTypes.node, - checked: PropTypes.bool, - disabled: PropTypes.bool, - platformsWithData: PropTypes.array, - activeDeploymentPlatforms: PropTypes.array, - onClick: PropTypes.func, -} - -export const MapLegend = ({ - platforms = [], - platformsWithData = [], - activeDeploymentPlatforms = [], - setSelectedPlatforms, - selectedPlatforms, -}) => { - const names = platforms.map(i => i.name) - const uniquePlatforms = platforms.filter( - (i, index) => names.indexOf(i.name) === index - ) - const movingPlatforms = uniquePlatforms.filter(platform => - ["Jet", "Prop", "UAV", "Ships/Boats"].includes(platform.type) - ) - const staticPlatforms = uniquePlatforms.filter( - platform => !["Jet", "Prop", "UAV", "Ships/Boats"].includes(platform.type) - ) - - return ( - -
- Platforms - {movingPlatforms.length > 0 &&

Moving

} - {movingPlatforms.map((platform, index) => ( - - selectedPlatforms.includes(platform.name) - ? setSelectedPlatforms( - selectedPlatforms.filter(i => i !== platform.name) - ) - : setSelectedPlatforms([...selectedPlatforms, platform.name]) - } - activeDeploymentPlatforms={activeDeploymentPlatforms} - platformsWithData={platformsWithData} - /> - ))} - {staticPlatforms.length > 0 &&

Stationary

} - {staticPlatforms.map(platform => ( - i.name === platform.name)?.color} - icon={STATIC_PLATFORMS?.find(i => i.name === platform.name)?.icon} - checked={selectedPlatforms.includes(platform.name)} - disabled={ - (selectedPlatforms.length === 1 && - selectedPlatforms.includes(platform.name)) || - !platformsWithData.includes(platform.name) - } - onClick={() => - selectedPlatforms.includes(platform.name) - ? setSelectedPlatforms( - selectedPlatforms.filter(i => i !== platform.name) - ) - : setSelectedPlatforms([...selectedPlatforms, platform.name]) - } - activeDeploymentPlatforms={activeDeploymentPlatforms} - platformsWithData={platformsWithData} - /> - ))} -
-
- ) -} - -MapLegend.propTypes = { - platforms: PropTypes.array, - platformsWithData: PropTypes.array, - activeDeploymentPlatforms: PropTypes.array, - setSelectedPlatforms: PropTypes.func, - selectedPlatforms: PropTypes.array, -} - -export const PlatformStatus = ({ - platformName, - platformsWithData, - activeDeploymentPlatforms, -}) => { - const status = usePlatformStatus( - platformName, - platformsWithData, - activeDeploymentPlatforms - ) - - if (status !== "operational") { - return ( - - - {status === "notShown" ? "(Not Shown)" : "(Not Operating)"} - - - - ) - } - return <> -} - -PlatformStatus.propTypes = { - platformName: PropTypes.string, - platformsWithData: PropTypes.array, - activeDeploymentPlatforms: PropTypes.array, -} - -const LegendText = styled.label` - font-weight: ${props => (props.checked ? 600 : 400)}; - font-family: "Titillium Web", sans-serif; - display: inline-flex; - gap: 0.25rem; - margin-left: 0.5rem; - background: transparent; - border: none; - & svg { - vertical-align: middle; - } -` - -const IconSpan = styled.span` - color: ${props => props.color}; - background: #294060; - border: 0.5px solid; - border-color: ${props => props.color}; - display: inline-flex; - height: 1.25rem; - width: 1.25rem; - justify-content: center; - align-items: center; - border-radius: 512px; - & svg { - vertical-align: unset; - font-weight: 600; - } -` - -const LegendBox = styled.div` - display: inline-block; - text-align: left; - min-width: 14rem; - position: absolute; - right: 5px; - margin-top: 5px; - margin-right: 5px; - padding: 8px; - color: ${colors.lightTheme.text}; - background-color: rgba(255, 255, 255, 0.75); - transition: all 0.24s ease-out; - > fieldset { - border: 0px; - } - > fieldset > legend { - padding: 0 0 4px; - font-family: "Titillium Web", sans-serif; - color: ${colors.lightTheme.text}; - font-size: 1.1rem; - font-weight: 600; - } - & input { - cursor: pointer; - } - & h4 { - font-weight: 600; - color: ${colors.lightTheme.text}; - font-size: 0.8rem; - margin: 4px 0 2px; - } - &:hover { - background-color: rgba(255, 255, 255, 0.95); - } -` - -const Tag = styled.div` - display: inline; - > u { - text-decoration-line: underline; - text-decoration-style: dotted; - text-decoration-color: #4d4d4d; - color: #333; - text-decoration-thickness: 2px; - padding-left: 7px; - &:hover { - cursor: pointer; - } - } -` diff --git a/src/components/timeline/timeline-chart.js b/src/components/timeline/timeline-chart.js index bf0e7a49..43709592 100644 --- a/src/components/timeline/timeline-chart.js +++ b/src/components/timeline/timeline-chart.js @@ -13,8 +13,6 @@ import { Disclosure } from "@reach/disclosure" import { DeploymentPanel } from "./deployment-panel" import { DeploymentMap } from "./map" import { replaceSlashes } from "../../utils/helpers" -import { GlobeMap } from "../map/globe-map" -import Button from "../button" const chartSettings = { marginTop: 1, @@ -42,7 +40,6 @@ export const Swatch = styled.div` export const TimelineChart = ({ deployments, bounds, campaignName }) => { const [containerRef, dms] = useChartDimensions(chartSettings) - const MAP_STYLE_ID = "devseed/clx25ggbv076o01ql8k8m03k8" const minDateString = d3 .min( @@ -89,7 +86,6 @@ export const TimelineChart = ({ deployments, bounds, campaignName }) => { const [count, setCount] = useState(1) const [priority, setPriority] = useState({}) const [geojson, setGeojson] = useState({}) - const [enable3DView, setEnable3DView] = useState(false) const [tooltip, setTooltip] = useState({ x: null, y: null }) const [tooltipContent, setTooltipContent] = useState(null) @@ -150,32 +146,12 @@ export const TimelineChart = ({ deployments, bounds, campaignName }) => { return ( {geojson?.features?.length && ( - <> - {enable3DView ? ( - - ) : ( - - )} -
- -
- + )}
{ @@ -72,3 +73,72 @@ describe("getPlatformIcon", () => { ) }) }) + +describe("isPlatformVisible", () => { + const platformProperties = { + platform_name: "DC-8", + deployment: "SPADE_D1_2018", + } + it("returns true if there is not selected platforms and deployments", () => { + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: null, + selectedDeployment: null, + }) + ).toBeTruthy() + }) + it("returns true if it is one of the selected platforms", () => { + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: ["DC-8", "ER-2"], + selectedDeployment: null, + }) + ).toBeTruthy() + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: ["DC-8", "ER-2"], + selectedDeployment: { longname: "SPADE_D1_2018" }, + }) + ).toBeTruthy() + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: null, + selectedDeployment: { longname: "SPADE_D1_2018" }, + }) + ).toBeTruthy() + }) + it("returns false if the selected platforms and deployment does not match", () => { + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: ["DC-8", "ER-2"], + selectedDeployment: { longname: "SPADE_D2_2019" }, + }) + ).toBeFalsy() + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: null, + selectedDeployment: { longname: "SPADE_D2_2019" }, + }) + ).toBeFalsy() + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: ["ER-2"], + selectedDeployment: { longname: "SPADE_D1_2018" }, + }) + ).toBeFalsy() + expect( + isPlatformVisible({ + platformProperties, + selectedPlatforms: ["ER-2"], + selectedDeployment: null, + }) + ).toBeFalsy() + }) +}) diff --git a/src/utils/platform-colors.js b/src/utils/platform-colors.js index 62bbbc20..9244bcb0 100644 --- a/src/utils/platform-colors.js +++ b/src/utils/platform-colors.js @@ -119,3 +119,16 @@ export const getIconColors = () => { export const getPlatformIcon = platformName => STATIC_PLATFORMS.find(i => i.name === platformName).mapIcon + +export const isPlatformVisible = ({ + platformProperties, + selectedPlatforms, + selectedDeployment, +}) => { + return ( + (selectedDeployment === null || + selectedDeployment.longname === platformProperties.deployment) && + (selectedPlatforms === null || + selectedPlatforms.includes(platformProperties.platform_name)) + ) +}