Skip to content

Commit

Permalink
re-implement locate.js template as react component
Browse files Browse the repository at this point in the history
(see #126)
  • Loading branch information
sheppard committed Nov 9, 2020
1 parent 0fcea4d commit 7276476
Show file tree
Hide file tree
Showing 16 changed files with 377 additions and 35 deletions.
279 changes: 269 additions & 10 deletions packages/map/src/components/inputs/Geo.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,290 @@
import React, { useMemo } from 'react';
import { useComponents } from '@wq/react';
import { useOverlayComponents } from '../../hooks';
import { useField } from 'formik';
import React, { useState, useEffect, useMemo } from 'react';
import { useComponents, useInputComponents, usePlugin } from '@wq/react';
import { useOverlayComponents } from '@wq/map';
import { useField, useFormikContext } from 'formik';
import PropTypes from 'prop-types';

export const TYPE_MAP = {
geopoint: 'point',
geotrace: 'line_string',
geoshape: 'polygon'
};

const LOOKUP_METHODS = [
{ name: 'gps', label: 'Current' },
{ name: 'geocode', label: 'Address' },
{ name: 'manual', label: 'Lat/Lng' }
];

export default function Geo({ name, type, label }) {
const { Fieldset, AutoMap } = useComponents(),
const {
Fieldset,
AutoMap,
View,
Button,
IconButton,
Typography
} = useComponents(),
{ Input, Toggle } = useInputComponents(),
{ Draw } = useOverlayComponents(),
[, meta, helpers] = useField(name),
{ value } = meta,
{ setValue } = helpers;
[, { value }, { setValue }] = useField(name),
[, { value: method }] = useField(name + '_method'),
[
,
{ value: address },
{ setValue: setAddress, setError: setAddressError }
] = useField(name + '_address'),
[, { value: latitude }, { setValue: setLatitude }] = useField(
name + '_latitude'
),
[, { value: longitude }, { setValue: setLongitude }] = useField(
name + '_longitude'
),
[, { setValue: setAccuracy }] = useField(name + '_accuracy'),
{ setBounds, config } = usePlugin('map'),
[gpsStatus, setGpsStatus] = useState(''),
[gpsWatch, setGpsWatch] = useState(''),
[geocodeStatus, setGeocodeStatus] = useState(null),
maxGeometries = 1; // FIXME;

const { values } = useFormikContext();

const drawType = TYPE_MAP[type] || 'all',
geojson = useFeatureCollection(value);

async function geocode() {
setAddressError(null);
setGeocodeStatus('Looking up location...');
try {
const result = await config.geocoder(address);
const geometry = flatten(result.geometry);
if (type === 'geopoint') {
setValue(geometry);
}
recenterMap(geometry.coordinates[1], geometry.coordinates[0]);
setGeocodeStatus(result.label || 'Location found!');
} catch (e) {
setAddressError(e.message || '' + e);
setGeocodeStatus(null);
}
}

function handleChange(geojson) {
setValue(flatten(geojson));
geojson = flatten(geojson);
if (
geojson.type === 'GeometryCollection' &&
geojson.geometries.length > maxGeometries
) {
geojson = geojson.geometries[geojson.geometries.length - 1];
}
if (
method === 'manual' &&
type === 'geopoint' &&
geojson.type === 'Point'
) {
setLongitude(+geojson.coordinates[0].toFixed(6));
setLatitude(+geojson.coordinates[1].toFixed(6));
}
setValue(geojson);
}

useEffect(() => {
if (gpsWatch && method !== 'gps') {
stopGps();
}
}, [gpsWatch, method]);

useEffect(() => {
if (
address === undefined &&
method === 'geocode' &&
config.geocoderAddress
) {
setDefaultAddress();
}
async function setDefaultAddress() {
setAddress(await config.geocoderAddress(values));
}
}, [address, method, values]);

function saveLatLong() {
if (
!latitude ||
!longitude ||
Math.abs(latitude) > 90 ||
Math.abs(longitude) > 180
) {
return;
}
if (type === 'geopoint') {
setValue({
type: 'Point',
coordinates: [longitude, latitude]
});
}
recenterMap(latitude, longitude);
}

function recenterMap(lat, lng) {
setBounds([
[lat - 0.01, lng - 0.01],
[lat + 0.01, lng + 0.01]
]);
}

function startGps() {
if (gpsWatch) {
return;
}
if (!('geolocation' in navigator)) {
setGpsStatus('Geolocation not supported');
return;
}
const watchId = navigator.geolocation.watchPosition(
onPosition,
onError,
{
enableHighAccuracy: true,
timeout: 60 * 1000
}
);

setGpsWatch(watchId);
setGpsStatus('Determining location...');
}

const methods = config.geocoder
? LOOKUP_METHODS
: LOOKUP_METHODS.filter(method => method.name !== 'geocode');

function onPosition(evt) {
const lat = +evt.coords.latitude.toFixed(6),
lng = +evt.coords.longitude.toFixed(6),
acc = +evt.coords.accuracy.toFixed(3);
setAccuracy(acc);
if (type === 'geopoint') {
setValue({
type: 'Point',
coordinates: [lng, lat]
});
}
const latFmt = lat > 0 ? lat + '°N' : -lat + '°S',
lngFmt = lng > 0 ? lng + '°E' : -lng + '°W',
accFmt =
acc > 1000
? '~' + Math.round(acc / 1000) + 'km'
: acc > 1
? '~' + Math.round(acc) + 'm'
: acc + 'm';
setGpsStatus(`${latFmt} ${lngFmt} (${accFmt})`);
recenterMap(lat, lng);
}

function onError(error) {
setGpsStatus(error.message);
stopGps();
}

function stopGps() {
if (gpsWatch) {
navigator.geolocation.clearWatch(gpsWatch);
}
setGpsWatch(null);
}

const gpsActive = !!gpsWatch;

return (
<Fieldset label={label}>
<AutoMap>
<View
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<View style={{ marginRight: 8 }}>
<Toggle name={name + '_method'} choices={methods} />
</View>
{method === 'gps' && (
<>
<Typography
style={{
marginRight: 8,
flex: 1,
textAlign: 'center'
}}
color="textSecondary"
>
{gpsStatus}
</Typography>
<Button
icon={gpsActive ? 'gps-stop' : 'gps-start'}
style={{ minWidth: 140 }}
variant={gpsActive ? 'contained' : 'outlined'}
color="secondary"
onClick={gpsActive ? stopGps : startGps}
>
{gpsActive ? 'Stop GPS' : 'Start GPS'}
</Button>
</>
)}
{method === 'geocode' && (
<>
<Input
name={name + '_address'}
label="Address"
helperText={
geocodeStatus || 'Enter address or city name'
}
/>
<IconButton
onClick={geocode}
icon="search"
color="secondary"
/>
</>
)}
{method === 'manual' && (
<>
<Input
name={name + '_latitude'}
label="Latitude"
type="decimal"
inputProps={{
step: 0.000001,
min: -90,
max: 90
}}
InputLabelProps={{
shrink: true
}}
style={{ marginRight: 4 }}
/>
<Input
name={name + '_longitude'}
label="Longitude"
type="decimal"
inputProps={{
step: 0.000001,
min: -180,
max: 180
}}
InputLabelProps={{
shrink: true
}}
style={{ marginLeft: 4 }}
/>
<IconButton
onClick={saveLatLong}
icon="search"
variant="filled"
color="secondary"
/>
</>
)}
</View>
<AutoMap containerStyle={{ minHeight: 400 }}>
<Draw type={drawType} data={geojson} setData={handleChange} />
</AutoMap>
</Fieldset>
Expand Down
16 changes: 15 additions & 1 deletion packages/map/src/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import reducer, {
MAP_HIDE_OVERLAY,
MAP_SET_BASEMAP,
MAP_SET_HIGHLIGHT,
MAP_CLEAR_HIGHLIGHT
MAP_CLEAR_HIGHLIGHT,
MAP_SET_BOUNDS
} from './reducer';
import reactRenderer from '@wq/react';

Expand Down Expand Up @@ -59,6 +60,12 @@ const map = {
return {
type: MAP_CLEAR_HIGHLIGHT
};
},
setBounds(bounds) {
return {
type: MAP_SET_BOUNDS,
payload: bounds
};
}
},
components: {
Expand Down Expand Up @@ -130,6 +137,13 @@ map.init = function (config) {
if (plugin.overlays) {
Object.assign(this.config.overlays, plugin.overlays);
}
if (plugin.geocoder) {
this.config.geocoder = address => plugin.geocoder(address);
}
if (plugin.geocoderAddress) {
this.config.geocoderAddress = values =>
plugin.geocoderAddress(values);
}
});

// FIXME: loadDraw();
Expand Down
8 changes: 7 additions & 1 deletion packages/map/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const MAP_READY = 'MAP_READY',
MAP_HIDE_OVERLAY = 'MAP_HIDE_OVERLAY',
MAP_SET_BASEMAP = 'MAP_SET_BASEMAP',
MAP_SET_HIGHLIGHT = 'MAP_SET_HIGHLIGHT',
MAP_CLEAR_HIGHLIGHT = 'MAP_CLEAR_HIGHLIGHT';
MAP_CLEAR_HIGHLIGHT = 'MAP_CLEAR_HIGHLIGHT',
MAP_SET_BOUNDS = 'MAP_SET_BOUNDS';

var _lastRouteInfo = null;

Expand Down Expand Up @@ -97,6 +98,11 @@ export default function reducer(state = {}, action, config) {
highlight: undefined
};
}
case MAP_SET_BOUNDS:
return {
...state,
bounds: action.payload
};
default:
return state;
}
Expand Down
Loading

0 comments on commit 7276476

Please sign in to comment.