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",