mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #683 from sharetribe/coordinate-obfuscation
Optionally obfuscate coordinates and show a circle on the map
This commit is contained in:
commit
4ad89cfde6
9 changed files with 201 additions and 26 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import Map from './Map';
|
||||
import { types as sdkTypes } from '../../util/sdkLoader';
|
||||
import config from '../../config';
|
||||
|
||||
const { LatLng } = sdkTypes;
|
||||
|
||||
export const Empty = {
|
||||
export const WithMarker = {
|
||||
component: props => (
|
||||
<div style={{ height: 400 }}>
|
||||
<Map {...props} />
|
||||
|
|
@ -12,7 +13,26 @@ export const Empty = {
|
|||
),
|
||||
props: {
|
||||
center: new LatLng(60.16502999999999, 24.940064399999983),
|
||||
obfuscatedCenter: new LatLng(60.16502999999999, 24.940064399999983),
|
||||
address: 'Sharetribe',
|
||||
zoom: 22,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithObfuscatedLocation = {
|
||||
component: props => (
|
||||
<div style={{ height: 400 }}>
|
||||
<Map {...props} />
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
center: new LatLng(60.16502999999999, 24.940064399999983),
|
||||
obfuscatedCenter: new LatLng(60.16502999999999, 24.940064399999983),
|
||||
address: 'Sharetribe',
|
||||
zoom: 14,
|
||||
coordinatesConfig: {
|
||||
...config.coordinates,
|
||||
fuzzy: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import React, { Component } from 'react';
|
||||
import { string, number } from 'prop-types';
|
||||
import { withGoogleMap, GoogleMap, Marker } from 'react-google-maps';
|
||||
import { string, number, object } from 'prop-types';
|
||||
import { withGoogleMap, GoogleMap, Marker, Circle } from 'react-google-maps';
|
||||
import classNames from 'classnames';
|
||||
import { propTypes } from '../../util/types';
|
||||
import config from '../../config';
|
||||
|
||||
import CustomMarker from './images/marker-32x32.png';
|
||||
import css from './Map.css';
|
||||
|
||||
|
|
@ -11,9 +13,9 @@ import css from './Map.css';
|
|||
* It handles some of the google map initialization states.
|
||||
*/
|
||||
const MapWithGoogleMap = withGoogleMap(props => {
|
||||
const { center, zoom, address } = props;
|
||||
const { center, zoom, address, coordinatesConfig } = props;
|
||||
|
||||
const customMarker = {
|
||||
const markerIcon = {
|
||||
url: CustomMarker,
|
||||
// This marker is 32 pixels wide by 32 pixels high.
|
||||
size: new window.google.maps.Size(32, 32),
|
||||
|
|
@ -23,6 +25,16 @@ const MapWithGoogleMap = withGoogleMap(props => {
|
|||
anchor: new window.google.maps.Point(16, 32),
|
||||
};
|
||||
|
||||
const marker = <Marker position={center} icon={markerIcon} title={address} />;
|
||||
|
||||
const circleProps = {
|
||||
options: coordinatesConfig.circleOptions,
|
||||
radius: coordinatesConfig.circleRadius,
|
||||
center,
|
||||
};
|
||||
|
||||
const circle = <Circle {...circleProps} />;
|
||||
|
||||
return (
|
||||
<GoogleMap
|
||||
defaultZoom={zoom}
|
||||
|
|
@ -37,7 +49,7 @@ const MapWithGoogleMap = withGoogleMap(props => {
|
|||
fullscreenControl: true,
|
||||
}}
|
||||
>
|
||||
<Marker position={center} icon={customMarker} title={address} />
|
||||
{coordinatesConfig.fuzzy ? circle : marker}
|
||||
</GoogleMap>
|
||||
);
|
||||
});
|
||||
|
|
@ -51,10 +63,30 @@ export class Map extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className, rootClassName, mapRootClassName, address, center, zoom } = this.props;
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
mapRootClassName,
|
||||
address,
|
||||
center,
|
||||
obfuscatedCenter,
|
||||
zoom,
|
||||
coordinatesConfig,
|
||||
} = this.props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const mapClasses = mapRootClassName || css.mapRoot;
|
||||
const centerLocationForGoogleMap = { lat: center.lat, lng: center.lng };
|
||||
|
||||
if (coordinatesConfig.fuzzy && !obfuscatedCenter) {
|
||||
throw new Error(
|
||||
'Map: obfuscatedCenter prop is required when config.coordinates.fuzzy === true'
|
||||
);
|
||||
}
|
||||
if (!coordinatesConfig.fuzzy && !center) {
|
||||
throw new Error('Map: center prop is required when config.coordinates.fuzzy === false');
|
||||
}
|
||||
|
||||
const location = coordinatesConfig.fuzzy ? obfuscatedCenter : center;
|
||||
const centerLocationForGoogleMap = { lat: location.lat, lng: location.lng };
|
||||
|
||||
return (
|
||||
<MapWithGoogleMap
|
||||
|
|
@ -63,25 +95,30 @@ export class Map extends Component {
|
|||
center={centerLocationForGoogleMap}
|
||||
zoom={zoom}
|
||||
address={address}
|
||||
coordinatesConfig={coordinatesConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map.defaultProps = {
|
||||
className: '',
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
mapRootClassName: null,
|
||||
zoom: 11,
|
||||
address: '',
|
||||
zoom: config.coordinates.fuzzy ? config.coordinates.fuzzyDefaultZoomLevel : 11,
|
||||
coordinatesConfig: config.coordinates,
|
||||
};
|
||||
|
||||
Map.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
mapRootClassName: string,
|
||||
address: string.isRequired,
|
||||
center: propTypes.latlng.isRequired,
|
||||
address: string,
|
||||
center: propTypes.latlng,
|
||||
obfuscatedCenter: propTypes.latlng,
|
||||
zoom: number,
|
||||
coordinatesConfig: object,
|
||||
};
|
||||
|
||||
export default Map;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { arrayOf, bool, func, number, string, object, shape } from 'prop-types';
|
||||
import { withGoogleMap, GoogleMap } from 'react-google-maps';
|
||||
import classNames from 'classnames';
|
||||
import { groupBy, isEqual, reduce } from 'lodash';
|
||||
import { types as sdkTypes } from '../../util/sdkLoader';
|
||||
import { propTypes } from '../../util/types';
|
||||
import { obfuscatedCoordinates } from '../../util/maps';
|
||||
import { googleBoundsToSDKBounds } from '../../util/googleMaps';
|
||||
import { SearchMapInfoCard, SearchMapPriceLabel, SearchMapGroupLabel } from '../../components';
|
||||
import config from '../../config';
|
||||
|
||||
import css from './SearchMap.css';
|
||||
|
||||
|
|
@ -67,6 +69,21 @@ const reducedToArray = mapListings => {
|
|||
return reduce(mapListings, (acc, listing) => acc.concat([listing]), []);
|
||||
};
|
||||
|
||||
const withCoordinatesObfuscated = listings => {
|
||||
return listings.map(listing => {
|
||||
const { id, attributes, ...rest } = listing;
|
||||
const geolocation = obfuscatedCoordinates(attributes.geolocation, id ? id.uuid : null);
|
||||
return {
|
||||
id,
|
||||
...rest,
|
||||
attributes: {
|
||||
...attributes,
|
||||
geolocation,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* MapWithGoogleMap uses withGoogleMap HOC.
|
||||
* It handles some of the google map initialization states.
|
||||
|
|
@ -214,15 +231,20 @@ export class SearchMapComponent extends Component {
|
|||
rootClassName,
|
||||
center,
|
||||
isOpenOnModal,
|
||||
listings,
|
||||
listings: originalListings,
|
||||
mapRootClassName,
|
||||
onCloseAsModal,
|
||||
onIdle,
|
||||
zoom,
|
||||
coordinatesConfig,
|
||||
} = this.props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const mapClasses = mapRootClassName || css.mapRoot;
|
||||
|
||||
const listings = coordinatesConfig.fuzzy
|
||||
? withCoordinatesObfuscated(originalListings)
|
||||
: originalListings;
|
||||
|
||||
// container element listens clicks so that opened SearchMapInfoCard can be closed
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
return (
|
||||
|
|
@ -261,10 +283,9 @@ SearchMapComponent.defaultProps = {
|
|||
rootClassName: null,
|
||||
useLocationSearchBounds: true,
|
||||
zoom: 11,
|
||||
coordinatesConfig: config.coordinates,
|
||||
};
|
||||
|
||||
const { arrayOf, bool, func, number, string } = PropTypes;
|
||||
|
||||
SearchMapComponent.propTypes = {
|
||||
bounds: propTypes.latlngBounds,
|
||||
center: propTypes.latlng,
|
||||
|
|
@ -277,6 +298,12 @@ SearchMapComponent.propTypes = {
|
|||
rootClassName: string,
|
||||
useLocationSearchBounds: bool, // eslint-disable-line react/no-unused-prop-types
|
||||
zoom: number,
|
||||
coordinatesConfig: shape({
|
||||
fuzzy: bool.isRequired,
|
||||
fuzzyDefaultZoomLevel: number.isRequired,
|
||||
circleRadius: number.isRequired,
|
||||
circleOptions: object.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
const SearchMap = SearchMapComponent;
|
||||
|
|
|
|||
|
|
@ -238,6 +238,24 @@ const siteFacebookPage = 'https://www.facebook.com/Sharetribe/';
|
|||
// You should create one to track social sharing in Facebook
|
||||
const facebookAppId = null;
|
||||
|
||||
const coordinates = {
|
||||
// If true, obfuscate the coordinates of the listings that are shown
|
||||
// on a map.
|
||||
fuzzy: process.env.REACT_APP_FUZZY_COORDINATES || false,
|
||||
|
||||
fuzzyDefaultZoomLevel: 14,
|
||||
|
||||
// When fuzzy === true, a circle is shown on the map, the radius and
|
||||
// the style can be configured here.
|
||||
circleRadius: 500,
|
||||
circleOptions: {
|
||||
fillColor: '#c0392b',
|
||||
fillOpacity: 0.2,
|
||||
strokeColor: '#c0392b',
|
||||
strokeWeight: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
// NOTE: only expose configuration that should be visible in the
|
||||
// client side, don't add any server secrets in this file.
|
||||
const config = {
|
||||
|
|
@ -264,6 +282,7 @@ const config = {
|
|||
facebookAppId,
|
||||
sentryDsn,
|
||||
usingSSL,
|
||||
coordinates,
|
||||
custom,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -575,7 +575,11 @@ export class ListingPageComponent extends Component {
|
|||
</div>
|
||||
|
||||
<SectionRulesMaybe publicData={publicData} />
|
||||
<SectionMapMaybe geolocation={geolocation} publicData={publicData} />
|
||||
<SectionMapMaybe
|
||||
geolocation={geolocation}
|
||||
publicData={publicData}
|
||||
listingId={currentListing.id}
|
||||
/>
|
||||
|
||||
<div className={css.reviewsContainer}>
|
||||
<h2 className={css.reviewsHeading}>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,52 @@
|
|||
import React from 'react';
|
||||
import { object, string } from 'prop-types';
|
||||
import { string } from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { propTypes } from '../../util/types';
|
||||
import { obfuscatedCoordinates } from '../../util/maps';
|
||||
import { Map } from '../../components';
|
||||
import config from '../../config';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionMapMaybe = props => {
|
||||
const { className, rootClassName, geolocation, publicData } = props;
|
||||
const { className, rootClassName, geolocation, publicData, listingId } = props;
|
||||
|
||||
if (!geolocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const address = publicData.location ? publicData.location.address : '';
|
||||
const classes = classNames(rootClassName || css.locationContainer, className);
|
||||
|
||||
return geolocation ? (
|
||||
const mapProps = config.coordinates.fuzzy
|
||||
? { obfuscatedCenter: obfuscatedCoordinates(geolocation, listingId ? listingId.uuid : null) }
|
||||
: { address, center: geolocation };
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<h2 className={css.locationTitle}>
|
||||
<FormattedMessage id="ListingPage.locationTitle" />
|
||||
</h2>
|
||||
<div className={css.map}>
|
||||
<Map center={geolocation} address={address} />
|
||||
<Map {...mapProps} />
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
SectionMapMaybe.defaultProps = { className: null, rootClassName: null };
|
||||
SectionMapMaybe.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
geolocation: null,
|
||||
listingId: null,
|
||||
};
|
||||
|
||||
SectionMapMaybe.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
geolocation: propTypes.latlng.isRequired,
|
||||
publicData: object.isRequired,
|
||||
className: string,
|
||||
geolocation: propTypes.latlng,
|
||||
listingId: propTypes.uuid,
|
||||
};
|
||||
|
||||
export default SectionMapMaybe;
|
||||
|
|
|
|||
|
|
@ -334,6 +334,11 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
"lng": 60,
|
||||
}
|
||||
}
|
||||
listingId={
|
||||
UUID {
|
||||
"uuid": "listing1",
|
||||
}
|
||||
}
|
||||
publicData={Object {}}
|
||||
rootClassName={null}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,19 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
className={null}
|
||||
coordinatesConfig={
|
||||
Object {
|
||||
"circleOptions": Object {
|
||||
"fillColor": "#c0392b",
|
||||
"fillOpacity": 0.2,
|
||||
"strokeColor": "#c0392b",
|
||||
"strokeWeight": 0.5,
|
||||
},
|
||||
"circleRadius": 500,
|
||||
"fuzzy": false,
|
||||
"fuzzyDefaultZoomLevel": 14,
|
||||
}
|
||||
}
|
||||
isOpenOnModal={false}
|
||||
listings={Array []}
|
||||
mapRootClassName={null}
|
||||
|
|
|
|||
34
src/util/maps.js
Normal file
34
src/util/maps.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { random, memoize, round } from 'lodash';
|
||||
import { types as sdkTypes } from './sdkLoader';
|
||||
|
||||
const { LatLng } = sdkTypes;
|
||||
|
||||
const obfuscatedCoordinatesImpl = latlng => {
|
||||
const { lat, lng } = latlng;
|
||||
const threshold = 0.01;
|
||||
const newLat = round(lat + random(-1 * threshold, threshold), 5);
|
||||
const newLng = round(lng + random(-1 * threshold, threshold), 5);
|
||||
return new LatLng(newLat, newLng);
|
||||
};
|
||||
|
||||
const obfuscationKeyGetter = (latlng, cacheKey) => cacheKey;
|
||||
|
||||
const memoizedObfuscatedCoordinatesImpl = memoize(obfuscatedCoordinatesImpl, obfuscationKeyGetter);
|
||||
|
||||
/**
|
||||
* Make the given coordinates randomly a little bit different.
|
||||
*
|
||||
* @param {LatLng} latlng coordinates
|
||||
* @param {String?} cacheKey if given, the results are memoized and
|
||||
* the same coordinates are returned for the same key as long as the
|
||||
* cache isn't cleared (e.g. with page refresh). This results in
|
||||
* e.g. same listings always getting the same obfuscated coordinates
|
||||
* if the listing id is used as the cache key.
|
||||
*
|
||||
* @return {LatLng} obfuscated coordinates
|
||||
*/
|
||||
export const obfuscatedCoordinates = (latlng, cacheKey = null) => {
|
||||
return cacheKey
|
||||
? memoizedObfuscatedCoordinatesImpl(latlng, cacheKey)
|
||||
: obfuscatedCoordinatesImpl(latlng);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue