Skip to content
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
5 changes: 4 additions & 1 deletion demo/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Index = () => {
title={'@carbonplan/maps'}
/>
<Box sx={{ position: 'absolute', top: 0, bottom: 0, width: '100%' }}>
<Map zoom={2} center={[0, 0]} debug={debug}>
<Map zoom={2} center={[-10, 10]} debug={debug}>
<Fill
color={theme.rawColors.background}
source={bucket + 'basemaps/ocean'}
Expand All @@ -59,7 +59,10 @@ const Index = () => {
backgroundColor={theme.colors.background}
fontFamily={theme.fonts.mono}
fontSize={'14px'}
minRadius={300}
maxRadius={2000}
initialRadius={1000}
// mode={'rectangle'} // options: 'circle', 'rectangle', defaults to 'circle'
/>
)}
<Raster
Expand Down
23 changes: 11 additions & 12 deletions src/region/region-picker/circle-picker/circle-renderer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { select } from 'd3-selection'
import { FLOATING_HANDLE, SHOW_RADIUS_GUIDELINE } from '../constants'
import { getPathMaker, project } from './utils'
import {
FLOATING_HANDLE,
SHOW_RADIUS_GUIDELINE,
POLES,
UNITS_DICT,
} from '../constants'
import { getPathMaker, project } from '../utils'
import {
area,
convertArea,
Expand All @@ -12,13 +17,7 @@ import {
circle as turfCircle,
point,
} from '@turf/turf'
import CursorManager from './cursor-manager'

const POLES = [point([0, -90]), point([0, 90])]
const abbreviations = {
kilometers: 'km',
miles: 'mi',
}
import CursorManager from '../cursor-manager'

export default function CircleRenderer({
id,
Expand Down Expand Up @@ -132,7 +131,7 @@ export default function CircleRenderer({

const onMouseUp = () => {
onIdle(circle)
setCursor({ draggingCircle: false })
setCursor({ draggingRegion: false })
map.off('mousemove', onMouseMove)
map.off('touchmove', onMouseMove)
map.dragPan.enable()
Expand Down Expand Up @@ -160,7 +159,7 @@ export default function CircleRenderer({
lng: lngLat.lng - center.lng,
lat: lngLat.lat - center.lat,
}
setCursor({ draggingCircle: true })
setCursor({ draggingRegion: true })
svgCircle.style('pointer-events', 'none')
svgHandle.style('pointer-events', 'none')
}
Expand Down Expand Up @@ -311,7 +310,7 @@ export default function CircleRenderer({
const translateY = 4

svgRadiusText
.text(radius.toFixed(0) + abbreviations[units])
.text(radius.toFixed(0) + UNITS_DICT[units])
.attr(
'transform',
`rotate(${-1 * guidelineAngle + 90}) ` + `translate(0, ${translateY})`
Expand Down
8 changes: 7 additions & 1 deletion src/region/region-picker/circle-picker/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'
import { useMap } from '../../../map-provider'
import CircleRenderer from './circle-renderer'
import { HANDLE_RADIUS } from '../constants'

const CirclePicker = ({
id,
Expand Down Expand Up @@ -73,7 +74,12 @@ const CirclePicker = ({
fill={backgroundColor}
fillOpacity={0.8}
/>
<circle id={`handle-${id}`} r={8} fill={color} cursor='ew-resize' />
<circle
id={`handle-${id}`}
r={HANDLE_RADIUS}
fill={color}
cursor='ew-resize'
/>
<line
id={`radius-guideline-${id}`}
stroke={color}
Expand Down
13 changes: 11 additions & 2 deletions src/region/region-picker/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { point } from '@turf/turf'
/*
Set to true to deduplicate points when filtering. This addresses mapbox's
caveat about duplication in their docs on queryRenderedFeatures and
Expand All @@ -13,13 +14,21 @@ duplicate points, but queryRenderedFeatures does not.
*/
export const DEDUPE_ON_FILTER = true

//// CIRCLE FILTER ////

//// REGION PICKER FILTER ////
export const FLOATING_HANDLE = true
export const HANDLE_RADIUS = 8
export const SHOW_RADIUS_GUIDELINE = true

/*
Set to true to update the sidebar stats while dragging the circle.
Morphocode does that well, but it's really laggy here because
we need to refilter all of the points every time the mouse moves.
*/
export const UPDATE_STATS_ON_DRAG = false

export const POLES = [point([0, -90]), point([0, 90])]

export const UNITS_DICT = {
kilometers: 'km',
miles: 'mi',
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export default function CursorManager(map) {
let mouseState = {
onHandle: false,
draggingHandle: false,
onCircle: false,
draggingCircle: false,
onRegion: false,
draggingRegion: false,
}

return function setCursor(newState) {
Expand All @@ -17,7 +17,7 @@ export default function CursorManager(map) {

if (mouseState.onHandle || mouseState.draggingHandle)
canvas.style.cursor = 'ew-resize'
else if (mouseState.onCircle || mouseState.draggingCircle)
else if (mouseState.onRegion || mouseState.draggingRegion)
canvas.style.cursor = 'move'
else canvas.style.cursor = originalStyle
}
Expand Down
71 changes: 48 additions & 23 deletions src/region/region-picker/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'
import CirclePicker from './circle-picker'
import RectanglePicker from './rectangle-picker'
import { UPDATE_STATS_ON_DRAG } from './constants'
import { distance } from '@turf/turf'
import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -46,8 +47,7 @@ function getInitialCenter(map, center) {
}
}

// TODO:
// - accept mode (only accept mode="circle" to start)
// only accept mode="circle" or mode="rectangle" to start
function RegionPicker({
backgroundColor,
color,
Expand All @@ -58,6 +58,7 @@ function RegionPicker({
initialCenter: initialCenterProp,
minRadius,
maxRadius,
mode = 'circle',
}) {
const { map } = useMap()
const id = useRef(uuidv4())
Expand All @@ -78,34 +79,58 @@ function RegionPicker({
}
}, [])

const handleCircle = useCallback((circle) => {
if (!circle) return
setRegion(circle)
setCenter(circle.properties.center)
const handleShape = useCallback((shape) => {
if (!shape) return
setRegion(shape)
setCenter(shape.properties.center)
}, [])

// TODO: consider extending support for degrees and radians
if (!['kilometers', 'miles'].includes(units)) {
throw new Error('Units must be one of miles, kilometers')
}

return (
<CirclePicker
id={id.current}
map={map}
center={initialCenter.current}
radius={initialRadius.current}
onDrag={UPDATE_STATS_ON_DRAG ? handleCircle : undefined}
onIdle={handleCircle}
backgroundColor={backgroundColor}
color={color}
units={units}
fontFamily={fontFamily}
fontSize={fontSize}
maxRadius={maxRadius}
minRadius={minRadius}
/>
)
if (mode === 'circle') {
return (
<CirclePicker
key={`${mode}-picker`}
id={id.current}
center={initialCenter.current}
radius={initialRadius.current}
onDrag={UPDATE_STATS_ON_DRAG ? handleShape : undefined}
onIdle={handleShape}
backgroundColor={backgroundColor}
color={color}
units={units}
fontFamily={fontFamily}
fontSize={fontSize}
maxRadius={maxRadius}
minRadius={minRadius}
/>
)
} else if (mode === 'rectangle') {
return (
<RectanglePicker
key={`${mode}-picker`}
id={id.current}
center={initialCenter.current}
radius={initialRadius.current}
onDrag={UPDATE_STATS_ON_DRAG ? handleShape : undefined}
onIdle={handleShape}
backgroundColor={backgroundColor}
color={color}
units={units}
fontFamily={fontFamily}
fontSize={fontSize}
maxRadius={maxRadius}
minRadius={minRadius}
/>
)
} else {
throw new Error(
"RegionPicker `mode` must be one of ['circle', 'rectangle']"
)
}
}

export default RegionPicker
107 changes: 107 additions & 0 deletions src/region/region-picker/rectangle-picker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useState, useEffect } from 'react'
import { useMap } from '../../../map-provider'
import RectangleRenderer from './rectangle-renderer'
import { HANDLE_RADIUS } from '../constants'

const RectanglePicker = ({
id,
backgroundColor,
center,
color,
fontFamily,
fontSize,
radius,
onIdle,
onDrag,
units,
maxRadius,
minRadius,
}) => {
const { map } = useMap()
const [renderer, setRenderer] = useState(null)

useEffect(() => {
const renderer = RectangleRenderer({
id,
map,
onIdle,
onDrag,
initialCenter: center,
initialRadius: radius,
units,
maxRadius,
minRadius,
})

setRenderer(renderer)

return function cleanup() {
// need to check load state for fast-refresh purposes
if (map.loaded()) renderer.remove()
}
}, [])

return (
<svg
id={`rectangle-picker-${id}`}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
>
<defs>
<clipPath id={`rect-clip-${id}`}>
<path id={`rectangle-cutout-${id}`} />
</clipPath>
</defs>

<path
id={`rectangle-${id}`}
stroke={color}
strokeWidth={1}
fill='transparent'
cursor='move'
/>

<rect
x='0'
y='0'
width='100%'
height='100%'
clipPath={`url(#rect-clip-${id})`}
fill={backgroundColor}
fillOpacity={0.8}
/>

<circle
id={`handle-${id}`}
r={HANDLE_RADIUS}
fill={color}
cursor='ew-resize'
/>

<line
id={`radius-guideline-${id}`}
stroke={color}
strokeOpacity={0}
strokeWidth={1}
strokeDasharray='3,2'
/>

<g id={`radius-text-container-${id}`}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we change to show side length here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is probably much more intuitive.

Then a circular region picker could be created with:

<RegionPicker
  ...
  mode={'circle'}
  minRadius={300}
  maxRadius={2000}
  initialRadius={1000}
/>

And a rectangular region picker with:

<RegionPicker
  ...
  mode={'rectangle'}
  minSideLength={300}
  maxSideLength={2000}
  initialSideLength={1000}
/>

or something similar?

We could then check to make sure that radius props are only used when mode is set to 'circle' and side length props when mode is 'rectangle'.

<text
id={`radius-text-${id}`}
textAnchor='middle'
fontFamily={fontFamily}
fontSize={fontSize}
fill={color}
/>
</g>
</svg>
)
}

export default RectanglePicker
Loading