diff --git a/app/components/Map/convertToGeoJSON.js b/app/components/Map/convertToGeoJSON.js
new file mode 100644
index 0000000..d768493
--- /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, 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/getClosestSite.js b/app/components/Map/getClosestSite.js
new file mode 100644
index 0000000..0d874c0
--- /dev/null
+++ b/app/components/Map/getClosestSite.js
@@ -0,0 +1,20 @@
+// 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 getClosestSite = (features, pointerCoords) => {
+ const [pointerLng, pointerLat] = pointerCoords;
+ const closest = {};
+ features.forEach(({ properties }) => {
+ const currOffset =
+ Math.abs(pointerLng - properties.longitude) +
+ Math.abs(pointerLat - properties.latitude);
+ if (!closest.offset || currOffset < closest.offset) {
+ closest.offset = currOffset;
+ closest.properties = properties;
+ }
+ });
+ return closest.properties;
+};
+
+export default getClosestSite;
diff --git a/app/components/Map/index.js b/app/components/Map/index.js
index 1d6104a..57f3879 100644
--- a/app/components/Map/index.js
+++ b/app/components/Map/index.js
@@ -4,15 +4,27 @@
*
*/
-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';
+
import MapSelect from './mapSelect';
import { townArray } from './townConstants';
import CityInfo from './cityInfo';
import Wrapper from './Wrapper';
+import getClosestSite from './getClosestSite';
+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;
/* eslint-disable react/prefer-stateless-function */
class Map extends React.PureComponent {
@@ -30,14 +42,17 @@ class Map extends React.PureComponent {
selectedTown: 'Pittsburgh',
popupInfo: null,
};
-
+ this.mapRef = createRef();
this.handleClick = this.handleClick.bind(this);
}
- componentDidMount() {
- axios
- .get('https://dev.stevesaylor.io/api/location/')
- .then(res => this.setState({ geoJSON: this.convertToGeoJSON(res) }));
+ async componentDidMount() {
+ const res = await axios.get('https://dev.stevesaylor.io/api/location/');
+ 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) {
@@ -55,44 +70,13 @@ 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
- // 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],
- },
- properties: site,
- });
- }
- return result;
- }, []),
- };
- }
-
handleClick(event) {
- // Filter out features that we didn't provide
- const features = event.features
- ? event.features.filter(feature => feature.layer.id === 'data')
- : [];
- if (!features.length) 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 });
+ if (!event.features.length) {
+ this.setState({ popupInfo: null });
+ return;
+ }
+ const closestSite = getClosestSite(event.features, event.lngLat);
+ this.setState({ popupInfo: closestSite });
}
renderPopup() {
@@ -126,7 +110,11 @@ class Map extends React.PureComponent {
{...this.state.viewport}
onViewportChange={viewport => this.setState({ viewport })}
onClick={this.handleClick}
- clickRadius={10}
+ ref={this.mapRef}
+ clickRadius={clickRadius}
+ minZoom={9}
+ maxZoom={18}
+ interactiveLayerIds={['data']}
mapboxApiAccessToken="pk.eyJ1IjoiaHlwZXJmbHVpZCIsImEiOiJjaWpra3Q0MnIwMzRhdGZtNXAwMzRmNXhvIn0.tZzUmF9nGk2h28zx6PM13w"
>
{!!this.state.geoJSON && (
@@ -135,8 +123,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..5226a4f 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"
}
@@ -2147,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",
@@ -5929,9 +5939,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 +6521,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 +11924,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 +11938,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 +12443,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 +13640,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 +14237,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,16 +14598,16 @@
"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.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": "~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",
- "viewport-mercator-project": "^6.1.0"
+ "viewport-mercator-project": "^6.2.3 || ^7.0.1"
}
},
"react-reconciler": {
@@ -15331,9 +15335,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 +16859,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,12 +18127,11 @@
}
},
"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": "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 ac0d58e..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": "^4.1.2",
+ "react-map-gl": "^5.2.1",
"react-redux": "7.0.2",
"react-router-dom": "5.0.0",
"redux": "4.0.1",