From 01be5762c1234fe82932775b1d7dcebde8a647b8 Mon Sep 17 00:00:00 2001 From: cdr Date: Wed, 29 Jan 2020 02:37:07 -0500 Subject: [PATCH 01/13] Change cursor type on point hover - Throttled the hover event so that it doesn't blow up slower computers - Changed click radius for computers. Once hovering was implemented it was clear that the click radius was way too wide for precise pointers. User agent sniffing might not be a guarantee but it is probably the best option, short of waiting for a touch event and changing the click radius. --- app/components/Map/index.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 1d6104a..09d2c65 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -14,6 +14,8 @@ import { townArray } from './townConstants'; import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; +const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; + /* eslint-disable react/prefer-stateless-function */ class Map extends React.PureComponent { constructor(props) { @@ -95,6 +97,17 @@ class Map extends React.PureComponent { this.setState({ popupInfo: features[index].properties }); } + handleHover(event) { + const isHoveringFeature = event.features.some( + feature => feature.layer.id === 'data', + ); + if (isHoveringFeature) { + event.target.style.cursor = 'pointer'; // eslint-disable-line + } else { + event.target.style.cursor = 'grab'; // eslint-disable-line + } + } + renderPopup() { const { popupInfo } = this.state; @@ -126,7 +139,8 @@ class Map extends React.PureComponent { {...this.state.viewport} onViewportChange={viewport => this.setState({ viewport })} onClick={this.handleClick} - clickRadius={10} + onHover={_.throttle(this.handleHover, 100)} + clickRadius={clickRadius} mapboxApiAccessToken="pk.eyJ1IjoiaHlwZXJmbHVpZCIsImEiOiJjaWpra3Q0MnIwMzRhdGZtNXAwMzRmNXhvIn0.tZzUmF9nGk2h28zx6PM13w" > {!!this.state.geoJSON && ( From ceba4ab6ee59372090414970b84314e3bf66c798 Mon Sep 17 00:00:00 2001 From: cdr Date: Wed, 29 Jan 2020 02:49:58 -0500 Subject: [PATCH 02/13] Remove coordinate correction hack Wanted to remove this now so that it doesn't get forgotten about. Roughly half of the points will appear in Antarctica until the coordinates are corrected in the database. --- app/components/Map/index.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 09d2c65..356c0a0 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -70,12 +70,7 @@ class Map extends React.PureComponent { geometry: { type: 'Point', // GeoJSON takes lat/lon in reverse order - // Even more confusing: at least half of the lat/lon values provided are reversed?? - // Not sure how this wasn't a problem with previous config - coordinates: - longitude < latitude - ? [longitude, latitude] - : [latitude, longitude], + coordinates: [longitude, latitude], }, properties: site, }); From acfb7a9c6951a3bc1a92e18e61f1603cd50ce987 Mon Sep 17 00:00:00 2001 From: cdr Date: Wed, 29 Jan 2020 03:24:53 -0500 Subject: [PATCH 03/13] Close popup when clicking outside of feature --- app/components/Map/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 356c0a0..a282c96 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -85,7 +85,10 @@ class Map extends React.PureComponent { const features = event.features ? event.features.filter(feature => feature.layer.id === 'data') : []; - if (!features.length) return; + if (!features.length) { + this.setState({ popupInfo: null }); + return; + } // If there are still several features, pick one at random // Future: might be better to calculate which is closest to cursor const index = Math.floor(Math.random() * features.length); @@ -93,6 +96,7 @@ class Map extends React.PureComponent { } handleHover(event) { + if (!event.features) return; const isHoveringFeature = event.features.some( feature => feature.layer.id === 'data', ); From d6a4d6878956bf592b5a6fa8b472b14ef4a7b95d Mon Sep 17 00:00:00 2001 From: cdr Date: Wed, 29 Jan 2020 15:55:02 -0500 Subject: [PATCH 04/13] Calculate closest feature to click --- app/components/Map/getClosestFeature.js | 26 +++++++++++++++++++++++++ app/components/Map/index.js | 9 ++++----- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 app/components/Map/getClosestFeature.js diff --git a/app/components/Map/getClosestFeature.js b/app/components/Map/getClosestFeature.js new file mode 100644 index 0000000..2a67429 --- /dev/null +++ b/app/components/Map/getClosestFeature.js @@ -0,0 +1,26 @@ +// It's good to have a large click radius for mobile to be forgiving with touch precision +// However it is worthwhile to calculate the closest feature in case there are several +// features within the click radius and you are trying to be precise. + +const getClosestFeature = event => { + const [pointerLng, pointerLat] = event.lngLat; + const first = event.features.shift(); + const closest = { + offset: + Math.abs(pointerLng - first.longitude) + + Math.abs(pointerLat - first.latitude), + properties: first.properties, + }; + event.features.forEach(({ properties }) => { + const currOffset = + Math.abs(pointerLng - properties.longitude) + + Math.abs(pointerLat - properties.latitude); + if (currOffset < closest.offset) { + closest.offset = currOffset; + closest.properties = properties; + } + }); + return closest.properties; +}; + +export default getClosestFeature; diff --git a/app/components/Map/index.js b/app/components/Map/index.js index a282c96..85f468e 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -6,13 +6,14 @@ import React, { memo } from 'react'; import _ from 'lodash'; - import ReactMapGL, { Source, Layer, Popup } from 'react-map-gl'; import axios from 'axios'; + import MapSelect from './mapSelect'; import { townArray } from './townConstants'; import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; +import getClosestFeature from './getClosestFeature'; const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; @@ -89,10 +90,8 @@ class Map extends React.PureComponent { this.setState({ popupInfo: null }); return; } - // If there are still several features, pick one at random - // Future: might be better to calculate which is closest to cursor - const index = Math.floor(Math.random() * features.length); - this.setState({ popupInfo: features[index].properties }); + const closestFeature = getClosestFeature(event); + this.setState({ popupInfo: closestFeature }); } handleHover(event) { From b0b39b1bbcd62f91030615f76deac22c8e6db00d Mon Sep 17 00:00:00 2001 From: cdr Date: Wed, 29 Jan 2020 16:02:11 -0500 Subject: [PATCH 05/13] Move convertToGeoJSON into own file --- app/components/Map/convertToGeoJSON.js | 23 +++++++++++++++++++++++ app/components/Map/index.js | 26 ++------------------------ 2 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 app/components/Map/convertToGeoJSON.js diff --git a/app/components/Map/convertToGeoJSON.js b/app/components/Map/convertToGeoJSON.js new file mode 100644 index 0000000..c2b3968 --- /dev/null +++ b/app/components/Map/convertToGeoJSON.js @@ -0,0 +1,23 @@ +// Converts api data to geoJSON points +// Might be better if this arrived from the server in this format + +const convertToGeoJSON = ({ data }) => ({ + type: 'FeatureCollection', + features: data.reduce((result, site) => { + const { latitude, longitude } = site; + if (longitude) { + result.push({ + type: 'Feature', + geometry: { + type: 'Point', + // GeoJSON takes lat/lon in reverse order + coordinates: [longitude, latitude], + }, + properties: site, + }); + } + return result; + }, []), +}); + +export default convertToGeoJSON; diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 85f468e..2e119ae 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -14,6 +14,7 @@ import { townArray } from './townConstants'; import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; import getClosestFeature from './getClosestFeature'; +import convertToGeoJSON from './convertToGeoJSON'; const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; @@ -40,7 +41,7 @@ class Map extends React.PureComponent { componentDidMount() { axios .get('https://dev.stevesaylor.io/api/location/') - .then(res => this.setState({ geoJSON: this.convertToGeoJSON(res) })); + .then(res => this.setState({ geoJSON: convertToGeoJSON(res) })); } handleSelection(event) { @@ -58,29 +59,6 @@ class Map extends React.PureComponent { }); } - convertToGeoJSON({ data }) { - // Converts api data to geoJSON points - // Might be better if this arrived from the server in this format - return { - type: 'FeatureCollection', - features: data.reduce((result, site) => { - const { latitude, longitude } = site; - if (longitude) { - result.push({ - type: 'Feature', - geometry: { - type: 'Point', - // GeoJSON takes lat/lon in reverse order - coordinates: [longitude, latitude], - }, - properties: site, - }); - } - return result; - }, []), - }; - } - handleClick(event) { // Filter out features that we didn't provide const features = event.features From 4a4c5abf7f26f384a766d0aac255ff1baf116d1a Mon Sep 17 00:00:00 2001 From: cdr Date: Thu, 30 Jan 2020 00:33:15 -0500 Subject: [PATCH 06/13] Use images instead of built-in Mapbox icons Obviously the images need to be changed and expanded upon. I grabbed these (free, no-attribution) icons from iconfinder.com just to use them as an example. Relevant issue #36 here for icon status, and issue #19 at food-access-map-data for info on location type attributes --- app/components/Map/index.js | 27 +++++++++++-- app/images/icon-carrot.svg | 1 + app/images/icon-cart.svg | 1 + package-lock.json | 81 +++++++++++++++++-------------------- package.json | 2 +- 5 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 app/images/icon-carrot.svg create mode 100644 app/images/icon-cart.svg diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 2e119ae..43ebc8d 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -4,7 +4,7 @@ * */ -import React, { memo } from 'react'; +import React, { memo, createRef } from 'react'; import _ from 'lodash'; import ReactMapGL, { Source, Layer, Popup } from 'react-map-gl'; import axios from 'axios'; @@ -15,6 +15,14 @@ import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; import getClosestFeature from './getClosestFeature'; import convertToGeoJSON from './convertToGeoJSON'; +import iconCartSrc from '../../images/icon-cart.svg'; +import iconCarrotSrc from '../../images/icon-carrot.svg'; + +// Mapbox addImage takes an HTMLImageElement, ImageData, or ImageBitmap +const iconCartElement = new Image(24, 24); +iconCartElement.src = iconCartSrc; +const iconCarrotElement = new Image(30, 30); +iconCarrotElement.src = iconCarrotSrc; const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; @@ -34,7 +42,7 @@ class Map extends React.PureComponent { selectedTown: 'Pittsburgh', popupInfo: null, }; - + this.mapRef = createRef(); this.handleClick = this.handleClick.bind(this); } @@ -42,6 +50,10 @@ class Map extends React.PureComponent { axios .get('https://dev.stevesaylor.io/api/location/') .then(res => this.setState({ geoJSON: convertToGeoJSON(res) })); + // Add the icons to use in the Layer layout below + const mapInstance = this.mapRef.current.getMap(); + mapInstance.addImage('icon-cart', iconCartElement); + mapInstance.addImage('icon-carrot', iconCarrotElement); } handleSelection(event) { @@ -116,6 +128,7 @@ class Map extends React.PureComponent { onViewportChange={viewport => this.setState({ viewport })} onClick={this.handleClick} onHover={_.throttle(this.handleHover, 100)} + ref={this.mapRef} clickRadius={clickRadius} mapboxApiAccessToken="pk.eyJ1IjoiaHlwZXJmbHVpZCIsImEiOiJjaWpra3Q0MnIwMzRhdGZtNXAwMzRmNXhvIn0.tZzUmF9nGk2h28zx6PM13w" > @@ -125,8 +138,14 @@ class Map extends React.PureComponent { type="symbol" id="data" layout={{ - 'icon-image': 'marker-15', - 'icon-allow-overlap': true, + 'icon-image': [ + 'match', + ['get', 'type'], + 'Supermarket', + 'icon-carrot', + 'icon-cart', + ], + 'icon-ignore-placement': true, }} /> diff --git a/app/images/icon-carrot.svg b/app/images/icon-carrot.svg new file mode 100644 index 0000000..a4724ca --- /dev/null +++ b/app/images/icon-carrot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/icon-cart.svg b/app/images/icon-cart.svg new file mode 100644 index 0000000..abc99bc --- /dev/null +++ b/app/images/icon-cart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8957efa..77a89d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2006,11 +2006,12 @@ } }, "@material-ui/pickers": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.7.tgz", - "integrity": "sha512-dDi8G8TOXssXZQsGCRM4zoDnWMY4O/vvqVCH4ViIHflvS4ek4v30IlFcSONI5jGzL0dmJhNKso2UEn7qS3iZ3g==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", + "integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==", "requires": { "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", "@types/styled-jsx": "^2.2.8", "clsx": "^1.0.2", "react-transition-group": "^4.0.0", @@ -2018,9 +2019,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", - "integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", "requires": { "regenerator-runtime": "^0.13.2" } @@ -5929,9 +5930,9 @@ } }, "earcut": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.5.tgz", - "integrity": "sha512-QFWC7ywTVLtvRAJTVp8ugsuuGQ5mVqNmJ1cRYeLrSHgP3nycr2RHTJob9OtM0v8ujuoKN0NY1a93J/omeTL1PA==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz", + "integrity": "sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==" }, "ecc-jsbn": { "version": "0.1.2", @@ -6511,11 +6512,6 @@ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", "dev": true }, - "esm": { - "version": "3.0.84", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.0.84.tgz", - "integrity": "sha512-SzSGoZc17S7P+12R9cg21Bdb7eybX25RnIeRZ80xZs+VZ3kdQKzqTp2k4hZJjR7p9l0186TTXSgrxzlMDBktlw==" - }, "espree": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", @@ -11919,9 +11915,9 @@ } }, "mapbox-gl": { - "version": "0.54.1", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.54.1.tgz", - "integrity": "sha512-HtY+HobYTHTsFOJ3buTHtNvZv/Tjfp0vararhEWCjI7wQq8XxK16sEpsXucokrAhuu94js4KJylo13bKJx6l0Q==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.7.0.tgz", + "integrity": "sha512-iVZQUdhZzeVCE8VlELo24GfGqhAzjouiJl1K4rcfk9mtyJLCbWHlzGT6H5Bs61A/3NQXsSx54GdJXAWvebtFFg==", "requires": { "@mapbox/geojson-rewind": "^0.4.0", "@mapbox/geojson-types": "^1.0.2", @@ -11933,18 +11929,17 @@ "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "csscolorparser": "~1.0.2", - "earcut": "^2.1.5", - "esm": "~3.0.84", + "earcut": "^2.2.2", "geojson-vt": "^3.2.1", "gl-matrix": "^3.0.0", "grid-index": "^1.1.0", "minimist": "0.0.8", "murmurhash-js": "^1.0.0", - "pbf": "^3.0.5", + "pbf": "^3.2.1", "potpack": "^1.0.1", "quickselect": "^2.0.0", "rw": "^1.3.3", - "supercluster": "^6.0.1", + "supercluster": "^7.0.0", "tinyqueue": "^2.0.0", "vt-pbf": "^3.1.1" }, @@ -12439,9 +12434,9 @@ } }, "mjolnir.js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-2.2.1.tgz", - "integrity": "sha512-bUTP/NbwOfdrN4TKMjUcarfGmWU5yN6aHFR1ek7BNuFOwHk4PslUZjKzdOp1jwx2m0uCoRa5lG+x82l8Vii7Ng==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-2.4.0.tgz", + "integrity": "sha512-0WyWlc5EzdZ7eD0Fjy1DarzJpknesJaMJ6P0c6gDlbotfj3GRzv0odTXfTVVMm9WxEQSUzxosdnPqnd0SDxIyA==", "requires": { "@babel/runtime": "^7.0.0", "hammerjs": "^2.0.8" @@ -13636,9 +13631,9 @@ } }, "pbf": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.0.tgz", - "integrity": "sha512-98Eh7rsJNJF/Im6XYMLaOW3cLnNyedlOd6hu3iWMD5I7FZGgpw8yN3vQBrmLbLodu7G784Irb9Qsv2yFrxSAGw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", "requires": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -14233,9 +14228,9 @@ "dev": true }, "protocol-buffers-schema": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz", - "integrity": "sha512-Xdayp8sB/mU+sUV4G7ws8xtYMGdQnxbeIfLjyO9TZZRJdztBGhlmbI5x1qcY4TG5hBkIKGnc28i7nXxaugu88w==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.4.0.tgz", + "integrity": "sha512-G/2kcamPF2S49W5yaMGdIpkG6+5wZF0fzBteLKgEHjbNzqjZQ85aAs1iJGto31EJaSTkNvHs5IXuHSaTLWBAiA==" }, "proxy-addr": { "version": "2.0.4", @@ -14594,12 +14589,12 @@ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "react-map-gl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-4.1.13.tgz", - "integrity": "sha512-IczjaRVL2CuR+lBLsTOa0UIuYEcWg8vFy4a3SzmtHl8G0OVcTl00IlvTU7r6Ol+k3p23dxq8eAdFZh8VTBdOwg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-5.0.12.tgz", + "integrity": "sha512-b2+OU/U8T8kAGoSP3GXBkSWQKkBHq8IyZYMUCBEFZ9gH7dyRtCwjy9Vanzek7PmZs668q2+896fYQ3Z9eO6X0A==", "requires": { "@babel/runtime": "^7.0.0", - "mapbox-gl": "~0.54.0", + "mapbox-gl": "^1.0.0", "mjolnir.js": "^2.2.0", "prop-types": "^15.7.2", "react-virtualized-auto-sizer": "^1.0.2", @@ -15331,9 +15326,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz", - "integrity": "sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", "requires": { "regenerator-runtime": "^0.13.2" } @@ -16855,9 +16850,9 @@ } }, "supercluster": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-6.0.2.tgz", - "integrity": "sha512-aa0v2HURjBTOpbcknilcfxGDuArM8khklKSmZ/T8ZXL0BuRwb5aRw95lz+2bmWpFvCXDX/+FzqHxmg0TIaJErw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.0.0.tgz", + "integrity": "sha512-8VuHI8ynylYQj7Qf6PBMWy1PdgsnBiIxujOgc9Z83QvJ8ualIYWNx2iMKyKeC4DZI5ntD9tz/CIwwZvIelixsA==", "requires": { "kdbush": "^3.0.0" } @@ -18123,9 +18118,9 @@ } }, "viewport-mercator-project": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-6.2.1.tgz", - "integrity": "sha512-Ns0KExngwGkX/QZCAzYbqh3TTI8LKeO8pOphZN4mZmp/+wO/HqDacbztwXMdrTLy57ToolT4XrAH2NhuK7Nyfw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-6.2.3.tgz", + "integrity": "sha512-QQb0/qCLlP4DdfbHHSWVYXpghB2wkLIiiZQnoelOB59mXKQSyZVxjreq1S+gaBJFpcGkWEcyVtre0+2y2DTl/Q==", "requires": { "@babel/runtime": "^7.0.0", "gl-matrix": "^3.0.0" diff --git a/package.json b/package.json index ac0d58e..9585560 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "react-dom": "16.8.6", "react-helmet": "6.0.0-beta", "react-intl": "2.8.0", - "react-map-gl": "^4.1.2", + "react-map-gl": "^5.0.12", "react-redux": "7.0.2", "react-router-dom": "5.0.0", "redux": "4.0.1", From 002fa4e7c58506de1e4076cc8f96569fc488ade8 Mon Sep 17 00:00:00 2001 From: cdr Date: Thu, 30 Jan 2020 00:57:32 -0500 Subject: [PATCH 07/13] Improve map performance by trimming geoJSON & using map min/max zoom --- app/components/Map/convertToGeoJSON.js | 46 +++++++++++-------- ...tClosestFeature.js => getClosestSiteId.js} | 6 +-- app/components/Map/index.js | 19 +++++--- 3 files changed, 41 insertions(+), 30 deletions(-) rename app/components/Map/{getClosestFeature.js => getClosestSiteId.js} (88%) diff --git a/app/components/Map/convertToGeoJSON.js b/app/components/Map/convertToGeoJSON.js index c2b3968..aa52c35 100644 --- a/app/components/Map/convertToGeoJSON.js +++ b/app/components/Map/convertToGeoJSON.js @@ -1,23 +1,29 @@ -// Converts api data to geoJSON points -// Might be better if this arrived from the server in this format +// Converts api data to geoJSON points. Might be better if this arrived from the server in this format +// siteLookup is used to keep geoJSON slim for better map performance +// https://docs.mapbox.com/help/troubleshooting/working-with-large-geojson-data/#cleaning-up-your-data -const convertToGeoJSON = ({ data }) => ({ - type: 'FeatureCollection', - features: data.reduce((result, site) => { - const { latitude, longitude } = site; - if (longitude) { - result.push({ - type: 'Feature', - geometry: { - type: 'Point', - // GeoJSON takes lat/lon in reverse order - coordinates: [longitude, latitude], - }, - properties: site, - }); - } - return result; - }, []), -}); +const convertToGeoJSON = ({ data }) => { + const siteLookup = {}; + const geoJSON = { + type: 'FeatureCollection', + features: data.reduce((result, site) => { + const { latitude, longitude, name, type, id } = site; + if (longitude) { + siteLookup[id] = site; + result.push({ + type: 'Feature', + geometry: { + type: 'Point', + // GeoJSON takes lat/lon in reverse order + coordinates: [longitude, latitude], + }, + properties: { name, type, id }, + }); + } + return result; + }, []), + }; + return { siteLookup, geoJSON }; +}; export default convertToGeoJSON; diff --git a/app/components/Map/getClosestFeature.js b/app/components/Map/getClosestSiteId.js similarity index 88% rename from app/components/Map/getClosestFeature.js rename to app/components/Map/getClosestSiteId.js index 2a67429..9b74b9f 100644 --- a/app/components/Map/getClosestFeature.js +++ b/app/components/Map/getClosestSiteId.js @@ -2,7 +2,7 @@ // However it is worthwhile to calculate the closest feature in case there are several // features within the click radius and you are trying to be precise. -const getClosestFeature = event => { +const getClosestSiteId = event => { const [pointerLng, pointerLat] = event.lngLat; const first = event.features.shift(); const closest = { @@ -20,7 +20,7 @@ const getClosestFeature = event => { closest.properties = properties; } }); - return closest.properties; + return closest.properties.id; }; -export default getClosestFeature; +export default getClosestSiteId; diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 43ebc8d..e37a656 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -13,7 +13,7 @@ import MapSelect from './mapSelect'; import { townArray } from './townConstants'; import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; -import getClosestFeature from './getClosestFeature'; +import getClosestSiteId from './getClosestSiteId'; import convertToGeoJSON from './convertToGeoJSON'; import iconCartSrc from '../../images/icon-cart.svg'; import iconCarrotSrc from '../../images/icon-carrot.svg'; @@ -25,6 +25,7 @@ const iconCarrotElement = new Image(30, 30); iconCarrotElement.src = iconCarrotSrc; const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; +let lookupSite; // Seems unnecessary to keep this in component state /* eslint-disable react/prefer-stateless-function */ class Map extends React.PureComponent { @@ -46,10 +47,11 @@ class Map extends React.PureComponent { this.handleClick = this.handleClick.bind(this); } - componentDidMount() { - axios - .get('https://dev.stevesaylor.io/api/location/') - .then(res => this.setState({ geoJSON: convertToGeoJSON(res) })); + async componentDidMount() { + const res = await axios.get('https://dev.stevesaylor.io/api/location/'); + const { siteLookup, geoJSON } = convertToGeoJSON(res); + this.setState({ geoJSON }); + lookupSite = siteLookup; // Add the icons to use in the Layer layout below const mapInstance = this.mapRef.current.getMap(); mapInstance.addImage('icon-cart', iconCartElement); @@ -80,8 +82,9 @@ class Map extends React.PureComponent { this.setState({ popupInfo: null }); return; } - const closestFeature = getClosestFeature(event); - this.setState({ popupInfo: closestFeature }); + const closestSiteId = getClosestSiteId(event); + const closestSite = lookupSite[closestSiteId]; + this.setState({ popupInfo: closestSite }); } handleHover(event) { @@ -130,6 +133,8 @@ class Map extends React.PureComponent { onHover={_.throttle(this.handleHover, 100)} ref={this.mapRef} clickRadius={clickRadius} + minZoom={9} + maxZoom={18} mapboxApiAccessToken="pk.eyJ1IjoiaHlwZXJmbHVpZCIsImEiOiJjaWpra3Q0MnIwMzRhdGZtNXAwMzRmNXhvIn0.tZzUmF9nGk2h28zx6PM13w" > {!!this.state.geoJSON && ( From 8c830f652dfe23ccd49e57c206bc8159241b1b6a Mon Sep 17 00:00:00 2001 From: cdr Date: Thu, 30 Jan 2020 01:22:24 -0500 Subject: [PATCH 08/13] Remove siteLookup I had previously assumed that the other site data was passed into foodSiteDetail, however after looking it is clear that it is intended to fetch that from the server. No point in processing it into an object then. --- app/components/Map/convertToGeoJSON.js | 46 ++++++++----------- ...{getClosestSiteId.js => getClosestSite.js} | 6 +-- app/components/Map/index.js | 10 ++-- 3 files changed, 26 insertions(+), 36 deletions(-) rename app/components/Map/{getClosestSiteId.js => getClosestSite.js} (88%) diff --git a/app/components/Map/convertToGeoJSON.js b/app/components/Map/convertToGeoJSON.js index aa52c35..d768493 100644 --- a/app/components/Map/convertToGeoJSON.js +++ b/app/components/Map/convertToGeoJSON.js @@ -1,29 +1,23 @@ -// Converts api data to geoJSON points. Might be better if this arrived from the server in this format -// siteLookup is used to keep geoJSON slim for better map performance -// https://docs.mapbox.com/help/troubleshooting/working-with-large-geojson-data/#cleaning-up-your-data +// Converts api data to geoJSON points +// Might be better if this arrived from the server in this format -const convertToGeoJSON = ({ data }) => { - const siteLookup = {}; - const geoJSON = { - type: 'FeatureCollection', - features: data.reduce((result, site) => { - const { latitude, longitude, name, type, id } = site; - if (longitude) { - siteLookup[id] = site; - result.push({ - type: 'Feature', - geometry: { - type: 'Point', - // GeoJSON takes lat/lon in reverse order - coordinates: [longitude, latitude], - }, - properties: { name, type, id }, - }); - } - return result; - }, []), - }; - return { siteLookup, geoJSON }; -}; +const convertToGeoJSON = ({ data }) => ({ + type: 'FeatureCollection', + features: data.reduce((result, site) => { + const { latitude, longitude, name, type, id } = site; + if (longitude) { + result.push({ + type: 'Feature', + geometry: { + type: 'Point', + // GeoJSON takes lat/lon in reverse order + coordinates: [longitude, latitude], + }, + properties: { latitude, longitude, name, type, id }, + }); + } + return result; + }, []), +}); export default convertToGeoJSON; diff --git a/app/components/Map/getClosestSiteId.js b/app/components/Map/getClosestSite.js similarity index 88% rename from app/components/Map/getClosestSiteId.js rename to app/components/Map/getClosestSite.js index 9b74b9f..387c2e6 100644 --- a/app/components/Map/getClosestSiteId.js +++ b/app/components/Map/getClosestSite.js @@ -2,7 +2,7 @@ // However it is worthwhile to calculate the closest feature in case there are several // features within the click radius and you are trying to be precise. -const getClosestSiteId = event => { +const getClosestSite = event => { const [pointerLng, pointerLat] = event.lngLat; const first = event.features.shift(); const closest = { @@ -20,7 +20,7 @@ const getClosestSiteId = event => { closest.properties = properties; } }); - return closest.properties.id; + return closest.properties; }; -export default getClosestSiteId; +export default getClosestSite; diff --git a/app/components/Map/index.js b/app/components/Map/index.js index e37a656..5a08ab0 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -13,7 +13,7 @@ import MapSelect from './mapSelect'; import { townArray } from './townConstants'; import CityInfo from './cityInfo'; import Wrapper from './Wrapper'; -import getClosestSiteId from './getClosestSiteId'; +import getClosestSite from './getClosestSite'; import convertToGeoJSON from './convertToGeoJSON'; import iconCartSrc from '../../images/icon-cart.svg'; import iconCarrotSrc from '../../images/icon-carrot.svg'; @@ -25,7 +25,6 @@ const iconCarrotElement = new Image(30, 30); iconCarrotElement.src = iconCarrotSrc; const clickRadius = navigator.userAgent.includes('Mobi') ? 10 : 0; -let lookupSite; // Seems unnecessary to keep this in component state /* eslint-disable react/prefer-stateless-function */ class Map extends React.PureComponent { @@ -49,9 +48,7 @@ class Map extends React.PureComponent { async componentDidMount() { const res = await axios.get('https://dev.stevesaylor.io/api/location/'); - const { siteLookup, geoJSON } = convertToGeoJSON(res); - this.setState({ geoJSON }); - lookupSite = siteLookup; + this.setState({ geoJSON: convertToGeoJSON(res) }); // Add the icons to use in the Layer layout below const mapInstance = this.mapRef.current.getMap(); mapInstance.addImage('icon-cart', iconCartElement); @@ -82,8 +79,7 @@ class Map extends React.PureComponent { this.setState({ popupInfo: null }); return; } - const closestSiteId = getClosestSiteId(event); - const closestSite = lookupSite[closestSiteId]; + const closestSite = getClosestSite(event); this.setState({ popupInfo: closestSite }); } From dafa883064b8b8aa9d8572b80f93ce1c1344669c Mon Sep 17 00:00:00 2001 From: cdr Date: Thu, 30 Jan 2020 01:32:59 -0500 Subject: [PATCH 09/13] Update react-map-gl This fixes mapbox error 'Error removing layer of undefined' when navigating away from the map component I thought I had updated react-map-gl to the newest already but apparently not! --- package-lock.json | 26 +++++++++++++++++--------- package.json | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77a89d7..5226a4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2148,6 +2148,15 @@ } } }, + "@math.gl/web-mercator": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-3.1.3.tgz", + "integrity": "sha512-aU+3upxkdA9yObA7EudfEKfN/QH4o5impyc7s/a34J2hZKPsihgREphxnKzl2RknXD/KoL7MnQv589LToAJGMQ==", + "requires": { + "@babel/runtime": "^7.0.0", + "gl-matrix": "^3.0.0" + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -14589,16 +14598,16 @@ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "react-map-gl": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-5.0.12.tgz", - "integrity": "sha512-b2+OU/U8T8kAGoSP3GXBkSWQKkBHq8IyZYMUCBEFZ9gH7dyRtCwjy9Vanzek7PmZs668q2+896fYQ3Z9eO6X0A==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-5.2.1.tgz", + "integrity": "sha512-8o8WhCIhOS90FfEEnXJDcylnq0qZPdsMaQVW0qBVY9GYbnRJQlCw6ZTusg0rgmCYmbXKWA1ApuKnA7PrpXRxlQ==", "requires": { "@babel/runtime": "^7.0.0", "mapbox-gl": "^1.0.0", "mjolnir.js": "^2.2.0", "prop-types": "^15.7.2", "react-virtualized-auto-sizer": "^1.0.2", - "viewport-mercator-project": "^6.1.0" + "viewport-mercator-project": "^6.2.3 || ^7.0.1" } }, "react-reconciler": { @@ -18118,12 +18127,11 @@ } }, "viewport-mercator-project": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-6.2.3.tgz", - "integrity": "sha512-QQb0/qCLlP4DdfbHHSWVYXpghB2wkLIiiZQnoelOB59mXKQSyZVxjreq1S+gaBJFpcGkWEcyVtre0+2y2DTl/Q==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-7.0.1.tgz", + "integrity": "sha512-WKTuTL7o6WKdPQ+gmZhlXL7UpSdCdPUjxkDTBd/3AayBdAFSQGHxsqdbmPBvmoGwvo9KWo/30HTkNo/Z7ORJpw==", "requires": { - "@babel/runtime": "^7.0.0", - "gl-matrix": "^3.0.0" + "@math.gl/web-mercator": "^3.1.3" } }, "vm-browserify": { diff --git a/package.json b/package.json index 9585560..e99de74 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "react-dom": "16.8.6", "react-helmet": "6.0.0-beta", "react-intl": "2.8.0", - "react-map-gl": "^5.0.12", + "react-map-gl": "^5.2.1", "react-redux": "7.0.2", "react-router-dom": "5.0.0", "redux": "4.0.1", From d3d482130a27dac1559b011e07a0bb19535add0e Mon Sep 17 00:00:00 2001 From: cdr Date: Fri, 31 Jan 2020 00:37:57 -0500 Subject: [PATCH 10/13] Use already filtered features, remove unnecessary defensive ternary --- app/components/Map/getClosestSite.js | 8 ++++---- app/components/Map/index.js | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/components/Map/getClosestSite.js b/app/components/Map/getClosestSite.js index 387c2e6..3f5a427 100644 --- a/app/components/Map/getClosestSite.js +++ b/app/components/Map/getClosestSite.js @@ -2,16 +2,16 @@ // However it is worthwhile to calculate the closest feature in case there are several // features within the click radius and you are trying to be precise. -const getClosestSite = event => { - const [pointerLng, pointerLat] = event.lngLat; - const first = event.features.shift(); +const getClosestSite = (features, pointerCoords) => { + const [pointerLng, pointerLat] = pointerCoords; + const first = features.shift(); const closest = { offset: Math.abs(pointerLng - first.longitude) + Math.abs(pointerLat - first.latitude), properties: first.properties, }; - event.features.forEach(({ properties }) => { + features.forEach(({ properties }) => { const currOffset = Math.abs(pointerLng - properties.longitude) + Math.abs(pointerLat - properties.latitude); diff --git a/app/components/Map/index.js b/app/components/Map/index.js index 5a08ab0..aa5c8d4 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -72,14 +72,12 @@ class Map extends React.PureComponent { handleClick(event) { // Filter out features that we didn't provide - const features = event.features - ? event.features.filter(feature => feature.layer.id === 'data') - : []; + const features = event.features.filter(({ layer }) => layer.id === 'data'); if (!features.length) { this.setState({ popupInfo: null }); return; } - const closestSite = getClosestSite(event); + const closestSite = getClosestSite(features, event.lngLat); this.setState({ popupInfo: closestSite }); } From 333ec22d675c92d824a03a3813a65733bf8e5dc0 Mon Sep 17 00:00:00 2001 From: cdr Date: Sat, 1 Feb 2020 03:41:55 -0500 Subject: [PATCH 11/13] Specify interactive layer, remove hover handler --- app/components/Map/index.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/app/components/Map/index.js b/app/components/Map/index.js index aa5c8d4..57f3879 100644 --- a/app/components/Map/index.js +++ b/app/components/Map/index.js @@ -71,28 +71,14 @@ class Map extends React.PureComponent { } handleClick(event) { - // Filter out features that we didn't provide - const features = event.features.filter(({ layer }) => layer.id === 'data'); - if (!features.length) { + if (!event.features.length) { this.setState({ popupInfo: null }); return; } - const closestSite = getClosestSite(features, event.lngLat); + const closestSite = getClosestSite(event.features, event.lngLat); this.setState({ popupInfo: closestSite }); } - handleHover(event) { - if (!event.features) return; - const isHoveringFeature = event.features.some( - feature => feature.layer.id === 'data', - ); - if (isHoveringFeature) { - event.target.style.cursor = 'pointer'; // eslint-disable-line - } else { - event.target.style.cursor = 'grab'; // eslint-disable-line - } - } - renderPopup() { const { popupInfo } = this.state; @@ -124,11 +110,11 @@ class Map extends React.PureComponent { {...this.state.viewport} onViewportChange={viewport => this.setState({ viewport })} onClick={this.handleClick} - onHover={_.throttle(this.handleHover, 100)} ref={this.mapRef} clickRadius={clickRadius} minZoom={9} maxZoom={18} + interactiveLayerIds={['data']} mapboxApiAccessToken="pk.eyJ1IjoiaHlwZXJmbHVpZCIsImEiOiJjaWpra3Q0MnIwMzRhdGZtNXAwMzRmNXhvIn0.tZzUmF9nGk2h28zx6PM13w" > {!!this.state.geoJSON && ( From f0946ff39dc922b1daea657832b982207d3a9797 Mon Sep 17 00:00:00 2001 From: cdr Date: Thu, 6 Feb 2020 13:41:22 -0500 Subject: [PATCH 12/13] Fix bug with closest site calculation --- app/components/Map/getClosestSite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Map/getClosestSite.js b/app/components/Map/getClosestSite.js index 3f5a427..94d2f5e 100644 --- a/app/components/Map/getClosestSite.js +++ b/app/components/Map/getClosestSite.js @@ -7,8 +7,8 @@ const getClosestSite = (features, pointerCoords) => { const first = features.shift(); const closest = { offset: - Math.abs(pointerLng - first.longitude) + - Math.abs(pointerLat - first.latitude), + Math.abs(pointerLng - first.properties.longitude) + + Math.abs(pointerLat - first.properties.latitude), properties: first.properties, }; features.forEach(({ properties }) => { From a054aaad09bd23ecc3c8a53d72f045667e6bc603 Mon Sep 17 00:00:00 2001 From: cdr Date: Fri, 7 Feb 2020 09:43:43 -0500 Subject: [PATCH 13/13] Clean up getClosestSite --- app/components/Map/getClosestSite.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/components/Map/getClosestSite.js b/app/components/Map/getClosestSite.js index 94d2f5e..0d874c0 100644 --- a/app/components/Map/getClosestSite.js +++ b/app/components/Map/getClosestSite.js @@ -4,18 +4,12 @@ const getClosestSite = (features, pointerCoords) => { const [pointerLng, pointerLat] = pointerCoords; - const first = features.shift(); - const closest = { - offset: - Math.abs(pointerLng - first.properties.longitude) + - Math.abs(pointerLat - first.properties.latitude), - properties: first.properties, - }; + const closest = {}; features.forEach(({ properties }) => { const currOffset = Math.abs(pointerLng - properties.longitude) + Math.abs(pointerLat - properties.latitude); - if (currOffset < closest.offset) { + if (!closest.offset || currOffset < closest.offset) { closest.offset = currOffset; closest.properties = properties; }