Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

geoJSON layer: handle hover, improve click handling, use icons #42

Open
wants to merge 13 commits into
base: fix-map
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/components/Map/convertToGeoJSON.js
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 20 additions & 0 deletions app/components/Map/getClosestSite.js
Original file line number Diff line number Diff line change
@@ -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;
88 changes: 41 additions & 47 deletions app/components/Map/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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 && (
Expand All @@ -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,
}}
/>
</Source>
Expand Down
1 change: 1 addition & 0 deletions app/images/icon-carrot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/images/icon-cart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 49 additions & 46 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading