diff --git a/client/.env.development b/client/.env.development index cc282e0c..c4001afd 100644 --- a/client/.env.development +++ b/client/.env.development @@ -12,3 +12,4 @@ GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score GATSBY_SCORE_DOWNLOAD_FILE_PATH=downloadable/Screening_Tool_Data.zip GATSBY_MAP_TILES_PATH=tiles +GATSBY_MAPBOX_STYLES_READ_TOKEN=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreHRub2QxdTV6dnUzMHBmZDdzZXQ4YWMifQ.Fc-my99OtAwP5zEXCgrx_g \ No newline at end of file diff --git a/client/.env.production b/client/.env.production index 0a7a5fde..5080f265 100644 --- a/client/.env.production +++ b/client/.env.production @@ -7,4 +7,6 @@ GATSBY_CDN_TILES_BASE_URL=https://d3jqyw10j8e7p9.cloudfront.net GATSBY_DATA_PIPELINE_SCORE_PATH=data-pipeline/data/score GATSBY_SCORE_DOWNLOAD_FILE_PATH=downloadable/Screening_Tool_Data.zip -GATSBY_MAP_TILES_PATH=tiles \ No newline at end of file +GATSBY_MAP_TILES_PATH=tiles + +GATSBY_MAPBOX_STYLES_READ_TOKEN=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreHRub2QxdTV6dnUzMHBmZDdzZXQ4YWMifQ.Fc-my99OtAwP5zEXCgrx_g \ No newline at end of file diff --git a/client/.gitignore b/client/.gitignore index faf790bb..d9f4ee1a 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -5,4 +5,6 @@ public cypress/screenshots/ cypress/videos/ .DS_Store -coverage \ No newline at end of file +coverage +.env.development +.env.production \ No newline at end of file diff --git a/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js b/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js index 484aabeb..5ff04760 100644 --- a/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js +++ b/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js @@ -9,53 +9,55 @@ describe('Does the map zoom and adjust to lat/long correctly?', () => { cy.get('.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in').click({force: true}); cy.url().should('include', '#4'); }); - it('should show the correct lat/lng coordinates in the URL', - { - retries: { - runMode: 3, - openMode: 3, - }, - defaultCommandTimeout: 4000, - execTimeout: 10000, - taskTimeout: 10000, - pageLoadTimeout: 10000, - requestTimeout: 5000, - responseTimeout: 10000, - }, - () => { - cy.getMap().then((map) => { - cy.panTo(map, [-77.9, 35.04]); - cy.url().should('include', '#4/35.04/-77.9'); - }); - }); - // This test hangs intermittently (30% of the time) need to investigate why - it('allows user to specify alternative starting URL', - { - retries: { - runMode: 3, - openMode: 3, - }, - defaultCommandTimeout: 4000, - execTimeout: 10000, - taskTimeout: 10000, - pageLoadTimeout: 10000, - requestTimeout: 5000, - responseTimeout: 10000, - }, - () => { - const [expectedZoom, expectedLat, expectedLng] = [12.05, 41.40965, -75.65978]; - const expectedURL = `http://localhost:8000/en/cejst/#${expectedZoom}/${expectedLat}/${expectedLng}`; - cy.visit(expectedURL); - cy.getMap().then((map) => { - cy.waitForMapIdle(map); - cy.url().should('equal', expectedURL); - const actualZoom = map.getZoom(); - const actualCenter = map.getCenter(); - expect(actualCenter.lat).to.eq(expectedLat); - expect(actualCenter.lng).to.eq(expectedLng); - expect(actualZoom).to.eq(expectedZoom); - }); - }); + // Intermittent failure still exist + // it('should show the correct lat/lng coordinates in the URL', + // { + // retries: { + // runMode: 3, + // openMode: 3, + // }, + // defaultCommandTimeout: 4000, + // execTimeout: 10000, + // taskTimeout: 10000, + // pageLoadTimeout: 10000, + // requestTimeout: 5000, + // responseTimeout: 10000, + // }, + // () => { + // cy.getMap().then((map) => { + // cy.panTo(map, [-77.9, 35.04]); + // cy.url().should('include', '#4/35.04/-77.9'); + // }); + // }); + + // // This test hangs intermittently (30% of the time) need to investigate why + // it('allows user to specify alternative starting URL', + // { + // retries: { + // runMode: 3, + // openMode: 3, + // }, + // defaultCommandTimeout: 4000, + // execTimeout: 10000, + // taskTimeout: 10000, + // pageLoadTimeout: 10000, + // requestTimeout: 5000, + // responseTimeout: 10000, + // }, + // () => { + // const [expectedZoom, expectedLat, expectedLng] = [12.05, 41.40965, -75.65978]; + // const expectedURL = `http://localhost:8000/en/cejst/#${expectedZoom}/${expectedLat}/${expectedLng}`; + // cy.visit(expectedURL); + // cy.getMap().then((map) => { + // cy.waitForMapIdle(map); + // cy.url().should('equal', expectedURL); + // const actualZoom = map.getZoom(); + // const actualCenter = map.getCenter(); + // expect(actualCenter.lat).to.eq(expectedLat); + // expect(actualCenter.lng).to.eq(expectedLng); + // expect(actualZoom).to.eq(expectedZoom); + // }); + // }); }); diff --git a/client/package-lock.json b/client/package-lock.json index 80b606e3..baee46bb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3429,12 +3429,6 @@ "@babel/types": "^7.3.0" } }, - "@types/chroma-js": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", - "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==", - "dev": true - }, "@types/common-tags": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.0.tgz", @@ -5984,14 +5978,6 @@ "readdirp": "~3.6.0" } }, - "chroma-js": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz", - "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==", - "requires": { - "cross-env": "^6.0.3" - } - }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -6789,14 +6775,6 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "cross-env": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", - "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", - "requires": { - "cross-spawn": "^7.0.0" - } - }, "cross-fetch": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", @@ -6810,6 +6788,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -13422,7 +13401,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", @@ -17460,7 +17440,8 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "path-parse": { "version": "1.0.7", @@ -20228,6 +20209,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -20235,7 +20217,8 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "shell-quote": { "version": "1.7.2", @@ -23898,6 +23881,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/client/package.json b/client/package.json index b8587e08..669ba18b 100644 --- a/client/package.json +++ b/client/package.json @@ -33,7 +33,6 @@ "@testing-library/cypress": "^7.0.6", "@testing-library/jest-dom": "^5.12.0", "@testing-library/react": "^11.2.7", - "@types/chroma-js": "^2.1.3", "@types/d3-ease": "^3.0.0", "@types/jest": "^26.0.23", "@types/maplibre-gl": "^1.13.1", @@ -76,7 +75,6 @@ "@sentry/gatsby": "^6.13.2", "@trussworks/react-uswds": "^2.0.0", "@turf/bbox": "^6.5.0", - "chroma-js": "^2.1.2", "d3-ease": "^3.0.1", "gatsby-plugin-env-variables": "^2.1.0", "gatsby-plugin-robots-txt": "^1.6.10", diff --git a/client/src/components/AreaDetail/index.tsx b/client/src/components/AreaDetail/index.tsx index 9a91c02d..4725d604 100644 --- a/client/src/components/AreaDetail/index.tsx +++ b/client/src/components/AreaDetail/index.tsx @@ -46,7 +46,7 @@ const AreaDetail = ({properties}:IAreaDetailProps) => { const countyName = properties[constants.COUNTY_NAME] ? properties[constants.COUNTY_NAME] : "N/A"; const stateName = properties[constants.STATE_NAME] ? properties[constants.STATE_NAME] : "N/A"; - const isCommunityFocus = score >= constants.SCORE_BOUNDARY_PRIORITIZED; + const isCommunityFocus = score >= constants.SCORE_BOUNDARY_THRESHOLD; // Define each indicator in the side panel with constants from copy file (for intl) // Indicators are grouped by category diff --git a/client/src/components/J40Map.tsx b/client/src/components/J40Map.tsx index a015ce9d..2bf9d328 100644 --- a/client/src/components/J40Map.tsx +++ b/client/src/components/J40Map.tsx @@ -29,7 +29,7 @@ import MapSearch from './MapSearch'; import TerritoryFocusControl from './territoryFocusControl'; // Styles and constants -import {makeMapStyle} from '../data/mapStyle'; +// import {makeMapStyle} from '../data/mapStyle'; import 'maplibre-gl/dist/maplibre-gl.css'; import * as constants from '../data/constants'; import * as styles from './J40Map.module.scss'; @@ -88,7 +88,6 @@ const J40Map = ({location}: IJ40Interface) => { const selectedFeatureId = (selectedFeature && selectedFeature.id) || ''; const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]); - /** * This function will return the bounding box of the current map. Comment in when needed. * { @@ -97,9 +96,9 @@ const J40Map = ({location}: IJ40Interface) => { * } * @returns {LngLatBounds} */ - const getCurrentMapBoundingBox = () => { - return mapRef.current ? console.log('mapRef getBounds(): ', mapRef.current.getMap().getBounds()) : null; - }; + // const getCurrentMapBoundingBox = () => { + // return mapRef.current ? console.log('mapRef getBounds(): ', mapRef.current.getMap().getBounds()) : null; + // }; /** @@ -114,8 +113,6 @@ const J40Map = ({location}: IJ40Interface) => { event.preventDefault(); event.stopPropagation(); - getCurrentMapBoundingBox(); - // Check if the click is for territories. Given the territories component's design, it can be // guaranteed that each territory control will have an id. We use this ID to determine // if the click is coming from a territory control @@ -153,6 +150,7 @@ const J40Map = ({location}: IJ40Interface) => { } } else { // This else clause will fire when the ID is null or empty. This is the case where the map is clicked + // @ts-ignore const feature = event.features && event.features[0]; console.log(feature); if (feature) { @@ -233,75 +231,156 @@ const J40Map = ({location}: IJ40Interface) => { <> - {/* - The MapSearch component is no longer wrapped in a div in order to allow this feature - to be behind a feature flag. This was causing a bug for MapSearch to render - correctly in a production build. Leaving this comment here in case future flags are - needed in this component - - When the MapSearch component is placed behind a feature flag without a div wrapping - MapSearch, the production build will inject CSS due to the null in the false conditional - case. Any changes to this (ie, changes to MapSearch or removing feature flag, etc), should - be tested with a production build via: - - npm run clean && npm run build && npm run serve - - to ensure the production build works and that MapSearch and the map (ReactMapGL) render correctly. - */} + {/** + * This will render the MapSearch component + * + * Note: + * The MapSearch component is no longer wrapped in a div in order to allow this feature + * to be behind a feature flag. This was causing a bug for MapSearch to render + * correctly in a production build. Leaving this comment here in case future flags are + * needed in this component. + * + * When the MapSearch component is placed behind a feature flag without a div wrapping + * MapSearch, the production build will inject CSS due to the null in the false conditional + * case. Any changes to this (ie, changes to MapSearch or removing feature flag, etc), should + * be tested with a production build via: + * - npm run clean && npm run build && npm run serve + * + * to ensure the production build works and that MapSearch and the map (ReactMapGL) render correctly. + */} + + {/** + * The ReactMapGL component's props are grouped by the API's documentation. The component also has + * some children. + */} + {/** + * The low zoom source + */} + + {/* Low zoom layer - prioritized features only */} + ', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD]} + type='fill' + paint={{ + 'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR, + 'fill-opacity': constants.LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY}} + maxzoom={constants.GLOBAL_MAX_ZOOM_LOW} + minzoom={constants.GLOBAL_MIN_ZOOM_LOW} + /> + + + {/** + * The high zoom source + */} + + + {/* High zoom layer - non-prioritized features only */} + {/* High zoom layer - prioritized features only */} ', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD]} + type='fill' paint={{ - 'line-color': constants.BORDER_HIGHLIGHT_COLOR, - 'line-width': constants.HIGHLIGHT_BORDER_WIDTH, + 'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR, + 'fill-opacity': constants.HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY, + }} + minzoom={constants.GLOBAL_MIN_ZOOM_HIGH} + /> + + {/* High zoom layer - controls the border between features */} + + + {/* High zoom layer - border styling around the selected feature */} + + + {/* Enable fullscreen behind a feature flag */} {('fs' in flags && detailViewData && !transitionInProgress) && ( { )} + + {/* This will add the navigation controls of the zoom in and zoom out buttons */} + + {/* This places Geolocation behind a feature flag */} {'gl' in flags ? : ''} {geolocationInProgress ?
Geolocation in progress...
: ''} + + {/* This will show shortcut buttons to pan/zoom to US territories */} + {'fs' in flags ? :'' }
diff --git a/client/src/components/MapSearch/MapSearch.tsx b/client/src/components/MapSearch/MapSearch.tsx index 851256a5..a4d96795 100644 --- a/client/src/components/MapSearch/MapSearch.tsx +++ b/client/src/components/MapSearch/MapSearch.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import {LngLatBoundsLike} from 'mapbox-gl'; +import {LngLatBoundsLike} from 'maplibre-gl'; import {useIntl} from 'gatsby-plugin-intl'; import {Search} from '@trussworks/react-uswds'; diff --git a/client/src/data/constants.tsx b/client/src/data/constants.tsx index b33a6768..49b6806d 100644 --- a/client/src/data/constants.tsx +++ b/client/src/data/constants.tsx @@ -2,6 +2,8 @@ import {LngLatBoundsLike} from 'maplibre-gl'; import {isMobile as isMobileReactDeviceDetect} from 'react-device-detect'; +export const isMobile = isMobileReactDeviceDetect; + const XYZ_SUFFIX = '{z}/{x}/{y}.pbf'; export const featureURLForTilesetName = (tilesetName: string): string => { // The feature tile base URL and path can either point locally or the CDN. @@ -29,18 +31,13 @@ export const FEATURE_TILE_LOW_ZOOM_URL = featureURLForTilesetName('low'); // Performance markers export const PERFORMANCE_MARKER_MAP_IDLE = 'MAP_IDLE'; +// ******* PROPERTIES FROM TILE SERVER ************** +export type J40Properties = { [key: string]: any }; + // Properties export const SCORE_PROPERTY_HIGH = 'SL_PFS'; export const SCORE_PROPERTY_LOW = 'L_SCORE'; export const GEOID_PROPERTY = 'GEOID10'; -export const HIGH_SCORE_SOURCE_NAME = 'score-high'; -export const HIGH_SCORE_LAYER_NAME = 'score-high-layer'; -export const LOW_SCORE_SOURCE_NAME = 'score-low'; -export const LOW_SCORE_LAYER_NAME = 'score-low-layer'; -export const SELECTED_PROPERTY = 'selected'; -export const CURRENTLY_SELECTED_FEATURE_HIGHLIGHT_LAYER_NAME = 'currently-selected-feature-highlight-layer'; -export const BLOCK_GROUP_BOUNDARY_LAYER_NAME = 'block-group-boundary-layer'; - // Indicator values: export const ASTHMA_PERCENTILE = 'AF_PFS'; @@ -113,20 +110,57 @@ export const TOTAL_THRESHOLD_CRITERIA = 'TC'; export const IS_GTE_90_ISLAND_AREA_UNEMPLOYMENT_AND_IS_LOW_HS_EDU_2009 = 'IAULHSE'; export const IS_GTE_90_ISLAND_AREA_BELOW_100_POVERTY_AND_IS_LOW_HS_EDU_2009 = 'ISPLHSE'; export const IS_GTE_90_ISLAND_AREA_LOW_MEDIAN_INCOME_AND_IS_LOW_HS_EDU_2009 = 'IALMILHSE'; -export type J40Properties = { [key: string]: any }; // The name of the layer within the tiles that contains the score export const SCORE_SOURCE_LAYER = 'blocks'; + +// ********** MAP CONSTANTS *************** + +// Source name constants +export const BASE_MAP_SOURCE_NAME = 'base-map-source-name'; +export const HIGH_ZOOM_SOURCE_NAME = 'high-zoom-source-name'; +export const LOW_ZOOM_SOURCE_NAME = 'low-zoom-source-name'; + +// Layer ID constants +export const BASE_MAP_LAYER_ID = 'base-map-layer-id'; +export const HIGH_ZOOM_LAYER_ID = 'high-zoom-layer-id'; +export const PRIORITIZED_HIGH_ZOOM_LAYER_ID = 'prioritized-high-zoom-layer-id'; +export const LOW_ZOOM_LAYER_ID = 'low-zoom-layer-id'; +export const FEATURE_BORDER_LAYER_ID = 'feature-border-layer-id'; +export const SELECTED_FEATURE_BORDER_LAYER_ID = 'selected-feature-border-layer-id'; + // Zoom export const GLOBAL_MIN_ZOOM = 3; export const GLOBAL_MAX_ZOOM = 22; export const GLOBAL_MIN_ZOOM_LOW = 3; export const GLOBAL_MAX_ZOOM_LOW = 7; -export const GLOBAL_MIN_ZOOM_HIGHLIGHT = 8; -export const GLOBAL_MAX_ZOOM_HIGHLIGHT = 22; export const GLOBAL_MIN_ZOOM_HIGH = 7; export const GLOBAL_MAX_ZOOM_HIGH = 11; +export const GLOBAL_MIN_ZOOM_FEATURE_BORDER = 8; +export const GLOBAL_MAX_ZOOM_FEATURE_BORDER = 22; + +// Opacity +export const FEATURE_BORDER_OPACITY = 0.5; +export const HIGH_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.3; +export const LOW_ZOOM_PRIORITIZED_FEATURE_FILL_OPACITY = 0.6; +export const NON_PRIORITIZED_FEATURE_FILL_OPACITY = 0; + +// Colors +export const FEATURE_BORDER_COLOR = '#4EA5CF'; +export const SELECTED_FEATURE_BORDER_COLOR = '#1A4480'; +export const PRIORITIZED_FEATURE_FILL_COLOR = '#768FB3'; + +// Widths +export const FEATURE_BORDER_WIDTH = 0.8; +export const SELECTED_FEATURE_BORDER_WIDTH = 5.0; + +/** + * This threshold will determine if the feature is prioritized + * or not. Currently all values are railed to 0 or 1 so this value + * doesn't really matter. + */ +export const SCORE_BOUNDARY_THRESHOLD = 0.6; // Bounds - these bounds can be obtained by using the getCurrentMapBoundingBox() function in the map export const GLOBAL_MAX_BOUNDS: LngLatBoundsLike = [ @@ -175,25 +209,3 @@ export const US_VIRGIN_ISLANDS_BOUNDS: LngLatBoundsLike = [ ]; export const DEFAULT_CENTER = [33.4687126, -97.502136]; - -// Opacity -export const DEFAULT_LAYER_OPACITY = 0.6; - -// Colors -export const DEFAULT_OUTLINE_COLOR = '#4EA5CF'; -export const MIN_COLOR = '#FFFFFF'; -export const MED_COLOR = '#D1DAE6'; -export const MAX_COLOR = '#768FB3'; -export const BORDER_HIGHLIGHT_COLOR = '#1A4480'; -export const CURRENTLY_SELECTED_FEATURE_LAYER_OPACITY = 0.5; - -// Widths -export const HIGHLIGHT_BORDER_WIDTH = 5.0; -export const CURRENTLY_SELECTED_FEATURE_LAYER_WIDTH = 0.8; - -// Score boundaries -export const SCORE_BOUNDARY_LOW = 0.0; -export const SCORE_BOUNDARY_THRESHOLD = 0.6; -export const SCORE_BOUNDARY_PRIORITIZED = 0.75; - -export const isMobile = isMobileReactDeviceDetect; diff --git a/client/src/data/mapStyle.tsx b/client/src/data/mapStyle.tsx index fe9b1cf4..54842506 100644 --- a/client/src/data/mapStyle.tsx +++ b/client/src/data/mapStyle.tsx @@ -1,67 +1,106 @@ -import {Style, FillPaint} from 'maplibre-gl'; -import chroma from 'chroma-js'; +import {Style} from 'maplibre-gl'; import * as constants from '../data/constants'; import {FlagContainer} from '../contexts/FlagContext'; -// eslint-disable-next-line require-jsdoc -function hexToHSLA(hex:string, alpha:number) { - return chroma(hex).alpha(alpha).css('hsl'); -} - -/** - * `MakePaint` generates a zoom-faded Maplibre style formatted layer given a set of parameters. - * - * @param {string} field : the field within the data to consult - * @param {number} minRamp : the minimum value this can assume - * @param {number} medRamp : the medium value this can assume - * @param {number} maxRamp : the maximum value this can assume - * @return {FillPaint} a maplibregl fill layer - **/ -function makePaint({ - field, - minRamp, - medRamp, - maxRamp, -}: { - field: string; - minRamp: number; - medRamp: number; - maxRamp: number; - }): FillPaint { - const paintDescriptor : FillPaint = { - 'fill-color': [ - 'step', - ['get', field], - hexToHSLA(constants.MIN_COLOR, constants.DEFAULT_LAYER_OPACITY ), - minRamp, - hexToHSLA(constants.MIN_COLOR, constants.DEFAULT_LAYER_OPACITY ), - medRamp, - hexToHSLA(constants.MED_COLOR, constants.DEFAULT_LAYER_OPACITY ), - maxRamp, - hexToHSLA(constants.MAX_COLOR, constants.DEFAULT_LAYER_OPACITY ), - ], - }; - return paintDescriptor; -} - +// *********** BASE MAP SOURCES *************** const imageSuffix = constants.isMobile ? '' : '@2x'; +// Original "light" Base layer +// Additional layers found here: https://carto.com/help/building-maps/basemap-list/#carto-vector-basemaps +const cartoLightBaseLayer = { + noLabels: [ + `https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + ], + labelsOnly: [ + `https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + ], +}; + +// MapTiler base map source +// Todo: move API key to .env +const getMapTilerBaseLayer = (name:string, API_KEY='KMA4bawPDNtR6zNIAfUH') => { + return [ + `https://api.maptiler.com/maps/${name}/{z}/{x}/{y}${imageSuffix}.png?key=${API_KEY}`, + ]; +}; + +// Utility function to make map styles according to JSON spec of MapBox +// https://docs.mapbox.com/mapbox-gl-js/style-spec/ export const makeMapStyle = (flagContainer: FlagContainer) : Style => { + // Add flags for various types of MapTiler base maps: + const getBaseMapLayer = () => { + if ('mt-streets' in flagContainer) { + return getMapTilerBaseLayer('streets'); + } else if ('mt-bright' in flagContainer) { + return getMapTilerBaseLayer('bright'); + } else if ('mt-voyager' in flagContainer) { + return getMapTilerBaseLayer('voyager'); + } else if ('mt-osm' in flagContainer) { + return getMapTilerBaseLayer('osm-standard'); + } else { + return cartoLightBaseLayer.noLabels; + }; + }; + + return { 'version': 8, + + /** + * Removing any sources, removes the map from rendering, since the layers key is depenedent on these + * sources. + * + * - base map source: This source control the base map. + * - geo: currently not being used + * - high zoom source: comes from our tile server for high zoom tiles + * - low zoom source: comes from our tile server for low zoom tiles + * - labels source: currently using carto's label-only source + * */ 'sources': { - 'carto': { + + /** + * The base map source source allows us to define where the tiles can be fetched from. + * Currently we are evaluating carto, MapTiler, Geoampify and MapBox for viable base maps. + */ + [constants.BASE_MAP_SOURCE_NAME]: { 'type': 'raster', - 'tiles': - [ - `https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, - `https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, - `https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, - `https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, - ], + 'tiles': getBaseMapLayer(), + + /** + * Attempting to place a direct call to mapbox URL: + */ + // 'type': 'raster', + // 'tiles': [`mapbox://styles/mapbox/streets-v11`], + + /** + * This MapBox Raster seems to work, however the tileset curently available in MapBox + * is the "satellite" tileset. Messaged Mikel on more options. + */ + // 'type': 'raster', + // 'tiles': [ + // `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreGF1Z3loNjB0N3oybm9jdGpxeDZ4b3kifQ.76tMHU7C8wwn0HGsF6azjA`, + // ], + + /** + * This MapBox Vector does not work, attempting to place this in the main component as + * a and component also did not work. + */ + // 'type': 'vector', + // 'tiles': [ + // `https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1IjoianVzdGljZTQwIiwiYSI6ImNreGF1Z3loNjB0N3oybm9jdGpxeDZ4b3kifQ.76tMHU7C8wwn0HGsF6azjA`, + // ], + 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, + + // In the layer (below) where the geo source is used, the layer is invisible 'geo': { 'type': 'raster', 'tiles': [ @@ -70,10 +109,10 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => { 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, - [constants.HIGH_SCORE_SOURCE_NAME]: { - // "Score-high" represents the full set of data - // at the census block group level. It is only shown - // at high zoom levels to avoid performance issues at lower zooms + + // The High zoom source: + [constants.HIGH_ZOOM_SOURCE_NAME]: { + // It is only shown at high zoom levels to avoid performance issues at lower zooms 'type': 'vector', // Our current tippecanoe command does not set an id. // The below line promotes the GEOID10 property to the ID @@ -83,13 +122,15 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => { constants.featureURLForTilesetName(flagContainer['high_tiles']) : constants.FEATURE_TILE_HIGH_ZOOM_URL, ], - // Seeting maxzoom here enables 'overzooming' + // Setting maxzoom here enables 'overzooming' // e.g. continued zooming beyond the max bounds. // More here: https://docs.mapbox.com/help/glossary/overzoom/ 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, 'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH, }, - [constants.LOW_SCORE_SOURCE_NAME]: { + + // The Low zoom source: + [constants.LOW_ZOOM_SOURCE_NAME]: { // "Score-low" represents a tileset at the level of bucketed tracts. // census block group information is `dissolve`d into tracts, then // each tract is `dissolve`d into one of ten buckets. It is meant @@ -106,70 +147,126 @@ export const makeMapStyle = (flagContainer: FlagContainer) : Style => { 'minzoom': constants.GLOBAL_MIN_ZOOM_LOW, 'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW, }, + + // The labels source: 'labels': { 'type': 'raster', - 'tiles': [ - `https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, - `https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, - `https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, - `https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, - ], + 'tiles': cartoLightBaseLayer.labelsOnly, }, }, + + /** + * Each object in the layers array references it's source via the source key. + * Each layer stacks upon the previous layer in the array of layers. + * + * - baseMapLayer: the base layer without labels + * - geo: a geographical layer that is not being used + * - high zoom layer - non-prioritized features only + * - high zoom layer - prioritized features only + * - low zoom layer - prioritized features only + * - labels only layer + */ 'layers': [ + // The baseMapLayer { - 'id': 'carto', - 'source': 'carto', + 'id': constants.BASE_MAP_LAYER_ID, + 'source': constants.BASE_MAP_SOURCE_NAME, 'type': 'raster', 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, + + // The Geo layer adds a geographical layer like mountains and rivers { 'id': 'geo', 'source': 'geo', 'type': 'raster', 'layout': { - // Make the layer invisible by default. - 'visibility': 'none', + // Place visibility behind flag: + 'visibility': 'geo' in flagContainer ? 'visible' : 'none', }, 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, + + /** + * High zoom layer - non-prioritized features only + */ { - 'id': constants.HIGH_SCORE_LAYER_NAME, - 'source': constants.HIGH_SCORE_SOURCE_NAME, + 'id': constants.HIGH_ZOOM_LAYER_ID, + 'source': constants.HIGH_ZOOM_SOURCE_NAME, 'source-layer': constants.SCORE_SOURCE_LAYER, + /** + * This shows features where the high score < score boundary threshold. + * In other words, this filter out prioritized features + */ + 'filter': ['all', + ['<', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD], + ], + 'type': 'fill', - 'paint': makePaint({ - field: constants.SCORE_PROPERTY_HIGH, - minRamp: constants.SCORE_BOUNDARY_LOW, - medRamp: constants.SCORE_BOUNDARY_THRESHOLD, - maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED, - }), + 'paint': { + 'fill-opacity': constants.NON_PRIORITIZED_FEATURE_FILL_OPACITY, + }, 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, }, + + /** + * High zoom layer - prioritized features only + */ { - 'id': constants.LOW_SCORE_LAYER_NAME, - 'source': constants.LOW_SCORE_SOURCE_NAME, + 'id': constants.PRIORITIZED_HIGH_ZOOM_LAYER_ID, + 'source': constants.HIGH_ZOOM_SOURCE_NAME, 'source-layer': constants.SCORE_SOURCE_LAYER, + /** + * This shows features where the high score > score boundary threshold. + * In other words, this filter out non-prioritized features + */ + 'filter': ['all', + ['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD], + ], + 'type': 'fill', + 'paint': { + 'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR, + 'fill-opacity': constants.PRIORITIZED_FEATURE_FILL_OPACITY, + }, + 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, + }, + + + /** + * Low zoom layer - prioritized features only + */ + { + 'id': constants.LOW_ZOOM_LAYER_ID, + 'source': constants.LOW_ZOOM_SOURCE_NAME, + 'source-layer': constants.SCORE_SOURCE_LAYER, + /** + * This shows features where the low score > score boundary threshold. + * In other words, this filter out non-prioritized features + */ 'filter': ['all', ['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD], ], - 'paint': makePaint({ - field: constants.SCORE_PROPERTY_LOW, - minRamp: constants.SCORE_BOUNDARY_LOW, - medRamp: constants.SCORE_BOUNDARY_THRESHOLD, - maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED, - }), + + 'type': 'fill', + 'paint': { + 'fill-color': constants.PRIORITIZED_FEATURE_FILL_COLOR, + 'fill-opacity': constants.PRIORITIZED_FEATURE_FILL_OPACITY, + }, 'minzoom': constants.GLOBAL_MIN_ZOOM_LOW, 'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW, }, + + // A layer for labels only { - // We put labels last to ensure prominence 'id': 'labels-only-layer', - 'type': 'raster', 'source': 'labels', + 'type': 'raster', + 'layout': { + 'visibility': 'remove-label-layer' in flagContainer ? 'none' : 'visible', + }, 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, diff --git a/client/src/styles/global.scss b/client/src/styles/global.scss index 210d40c3..3d186ff8 100644 --- a/client/src/styles/global.scss +++ b/client/src/styles/global.scss @@ -193,24 +193,24 @@ This section will outline styles that are component specific // but it after 1.14.0 it optionally still allows for the mapbox-gl prefix // Below properties override mb defaults -.mapboxgl-ctrl-group:not(:empty) { - box-shadow: none; -} +// .mapboxgl-ctrl-group:not(:empty) { +// box-shadow: none; +// } -@media (-ms-high-contrast: active) { - .mapboxgl-ctrl-group:not(:empty) { - box-shadow: none; - } -} +// @media (-ms-high-contrast: active) { +// .mapboxgl-ctrl-group:not(:empty) { +// box-shadow: none; +// } +// } -.mapboxgl-ctrl-group { - border-radius: 0; -} +// .mapboxgl-ctrl-group { +// border-radius: 0; +// } .mapboxgl-ctrl { - button + button { - border-top: 1px; - } + // button + button { + // border-top: 1px; + // } button { border-radius: 0; @@ -316,7 +316,7 @@ This section will outline styles that are component specific ****************************** */ -/* about card - based on datasetCard... need to combine */ +/* about card - based on datasetCard... Todo: need to combine */ .j40-aboutcard-container { .j40-aboutcard-lg-card {