From 17e87a8501330af06564c0952b4c13a0a87f0ef3 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 18 Jul 2018 17:00:31 +0300 Subject: [PATCH] Add StaticMap sub component for Map --- .../static/icons/map-marker-32x32.png | Bin src/components/Map/StaticMap.js | 177 ++++++++++++++++++ src/config.js | 23 ++- 3 files changed, 199 insertions(+), 1 deletion(-) rename src/components/Map/images/marker-32x32.png => public/static/icons/map-marker-32x32.png (100%) create mode 100644 src/components/Map/StaticMap.js diff --git a/src/components/Map/images/marker-32x32.png b/public/static/icons/map-marker-32x32.png similarity index 100% rename from src/components/Map/images/marker-32x32.png rename to public/static/icons/map-marker-32x32.png diff --git a/src/components/Map/StaticMap.js b/src/components/Map/StaticMap.js new file mode 100644 index 00000000..694e5109 --- /dev/null +++ b/src/components/Map/StaticMap.js @@ -0,0 +1,177 @@ +import React, { Component } from 'react'; +import { number, object, shape, string } from 'prop-types'; +import pick from 'lodash/pick'; +import isEqual from 'lodash/isEqual'; +import polyline from '@mapbox/polyline'; +import { encodeLatLng, stringify } from '../../util/urlHelpers'; +import { lazyLoadWithDimensions } from '../../util/contextHelpers'; +import config from '../../config'; + +const DEFAULT_COLOR = 'FF0000'; +const DEFAULT_STROKE_OPACITY = 0.3; +const DEFAULT_FILL_OPACITY = 0.2; + +// Return polyline encoded list of points forming a circle +// This algorithm is based on +// https://stackoverflow.com/questions/7316963/drawing-a-circle-google-static-maps +const getEncodedGMapCirclePoints = (lat, lng, rad, detail = 8) => { + const R = 6371; + const pi = Math.PI; + + const _lat = lat * pi / 180; + const _lng = lng * pi / 180; + const d = rad / 1000 / R; + + let points = []; + for (let i = 0; i <= 360; i += detail) { + const brng = i * pi / 180; + + let pLat = Math.asin( + Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng) + ); + const pLng = + (_lng + + Math.atan2( + Math.sin(brng) * Math.sin(d) * Math.cos(_lat), + Math.cos(d) - Math.sin(_lat) * Math.sin(pLat) + )) * + 180 / + pi; + pLat = pLat * 180 / pi; + + points.push([pLat.toFixed(6), pLng.toFixed(6)]); + } + + return polyline.encode(points); +}; + +// Extract color from string. Given value should be either with '#' (e.g. #FFFFFF') or without it. +const formatColorFromString = color => { + if (typeof color === 'string' && /^#[0-9A-F]{6}$/i.test(color)) { + return color.substring(1).toUpperCase(); + } else if (typeof color === 'string' && /^[0-9A-F]{6}$/i) { + return color.toUpperCase(); + } else { + return DEFAULT_COLOR; + } +}; + +// Convert opacity from floating point value (0.0 -> 1.0) to a hexadecimal format +const convertOpacity = opacity => { + if (typeof opacity === 'number' && !isNaN(opacity) && opacity >= 0 && opacity <= 1) { + // 0.2 => 20 + return Math.floor(opacity * 255) + .toString(16) + .toUpperCase(); + } +}; + +// Draw a circle polyline for fuzzy location. +const drawFuzzyCircle = (coordinatesConfig, center) => { + if ( + !( + coordinatesConfig && + typeof coordinatesConfig === 'object' && + center && + typeof center === 'object' + ) + ) { + return ''; + } + + const { fillColor, fillOpacity, strokeColor, strokeWeight } = coordinatesConfig.circleOptions; + const { lat, lng } = center; + + const circleRadius = coordinatesConfig.coordinateOffset || 500; + const circleStrokeWeight = strokeWeight || 1; + const circleStrokeColor = formatColorFromString(strokeColor); + const circleStrokeOpacity = convertOpacity(DEFAULT_STROKE_OPACITY); + const circleFill = formatColorFromString(fillColor); + const circleFillOpacity = convertOpacity(fillOpacity || DEFAULT_FILL_OPACITY); + + //Encoded polyline string + const encodedPolyline = getEncodedGMapCirclePoints(lat, lng, circleRadius, 8); + + const polylineGraphicTokens = [ + `color:0x${circleStrokeColor}${circleStrokeOpacity}`, + `fillcolor:0x${circleFill}${circleFillOpacity}`, + `weight:${circleStrokeWeight}`, + `enc:${encodedPolyline}`, + ]; + + return polylineGraphicTokens.join('|'); +}; + +// Get custom marker data for static map URI +const customMarker = (options, lat, lng) => { + const { anchorX, anchorY, markerURI } = options; + return [`anchor:${anchorX},${anchorY}`, `icon:${markerURI}`, `${lat},${lng}`].join('|'); +}; + +class StaticMap extends Component { + shouldComponentUpdate(nextProps, prevState) { + // Do not draw the map unless center, zoom or dimensions change + // We want to prevent unnecessary calls to Google Maps APIs due + const currentData = pick(this.props, ['center', 'zoom', 'dimensions']); + const nextData = pick(nextProps, ['center', 'zoom', 'dimensions']); + return !isEqual(currentData, nextData); + } + + render() { + const { center, zoom, address, coordinatesConfig, dimensions } = this.props; + const { lat, lng } = center || {}; + const { width, height } = dimensions; + + // Extra graphics for the static map image + // 1. if fuzzy coordinates are used, return circle path + // 2. if customMarker is defined in config.js, use that + // 3. else return default marker + const targetMaybe = coordinatesConfig.fuzzy + ? { path: drawFuzzyCircle(coordinatesConfig, center) } + : coordinatesConfig.customMarker + ? { markers: customMarker(coordinatesConfig.customMarker, lat, lng) } + : { markers: `${lat},${lng}` }; + + const srcParams = stringify({ + center: encodeLatLng(center), + zoom, + size: `${width}x${height}`, + maptype: 'roadmap', + key: config.googleMapsAPIKey, + ...targetMaybe, + }); + + return ( + {address} + ); + } +} + +StaticMap.defaultProps = { + className: null, + rootClassName: null, + address: '', + center: null, + zoom: config.coordinates.fuzzy ? config.coordinates.fuzzyDefaultZoomLevel : 11, + coordinatesConfig: config.coordinates, +}; + +StaticMap.propTypes = { + className: string, + rootClassName: string, + address: string, + center: shape({ + lat: number.isRequired, + lng: number.isRequired, + }).isRequired, + zoom: number, + coordinatesConfig: object, + + // from withDimensions + dimensions: shape({ + width: number.isRequired, + height: number.isRequired, + }).isRequired, +}; + +export default lazyLoadWithDimensions(StaticMap, { maxWidth: '640px' }); diff --git a/src/config.js b/src/config.js index 240cfb32..f594a2b4 100644 --- a/src/config.js +++ b/src/config.js @@ -252,6 +252,9 @@ const siteFacebookPage = 'https://www.facebook.com/Sharetribe/'; // You should create one to track social sharing in Facebook const facebookAppId = null; +// Google Maps API key is needed for static map images. +const googleMapsAPIKey = process.env.REACT_APP_GOOGLE_MAPS_API_KEY; + const coordinates = { // If true, obfuscate the coordinates of the listings that are shown // on a map. @@ -280,9 +283,26 @@ const coordinates = { fillColor: '#c0392b', fillOpacity: 0.2, strokeColor: '#c0392b', - strokeWeight: 0.5, + strokeOpacity: 0.5, + strokeWeight: 1, clickable: false, }, + + // An option to use custom marker on static maps. Uncomment to enable it. + // https://developers.google.com/maps/documentation/maps-static/dev-guide#Markers + // + // Note 1: fuzzy coordinate circle overwrites these custom marker settings) + // Note 2: markerURI needs to be a public URI accessible by Google Maps API servers. + // The easiest place is /public/static/icons/ folder, but then the marker image is not available + // while developing through localhost. + + // customMarker: { + // markerURI: encodeURI(`${canonicalRootURL}/static/icons/map-marker-32x32.png`), + // anchorX: 16, + // anchorY: 32, + // width: 32, + // height: 32, + // }, }; // NOTE: only expose configuration that should be visible in the @@ -318,6 +338,7 @@ const config = { facebookAppId, sentryDsn, usingSSL, + googleMapsAPIKey, coordinates, custom, };