Skip to content

Commit

Permalink
feat: add zoom controls to tile map (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
meelrossi authored Mar 15, 2023
1 parent b868b2a commit 5bceca1
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 22 deletions.
5 changes: 4 additions & 1 deletion .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export const parameters = {
date: /Date$/,
},
},
}
options: {
storySort: (a, b) => Number.parseInt(a[1].name) - Number.parseInt(b[1].name)
}
}
49 changes: 49 additions & 0 deletions src/components/TileMap/TileMap.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,52 @@
.react-tile-map-popup.visible {
opacity: 1;
}

.react-tile-map .zoom-controls {
display: flex;
flex-direction: column;
position: absolute;
right: 20px;
bottom: 20px;
box-shadow: 0 1px 8px rgb(0 0 0 / 30%);
border-radius: 6px;
}

.react-tile-map .zoom-controls button {
color: rgb(214, 214, 214);
font-size: 20px;
background-color: #736e7d;
border: 0;
height: 48px;
width: 40px;
position: relative;
cursor: pointer;
}

.react-tile-map .zoom-controls button:hover:not(:disabled) {
color: white;
}

.react-tile-map .zoom-controls button:disabled {
cursor: default;
color: #969696
}

.react-tile-map .zoom-controls button:first-child {
border-radius: 6px 6px 0 0;
}

.react-tile-map .zoom-controls button:last-child {
border-radius: 0 0 6px 6px;
}

.react-tile-map .zoom-controls button:last-child::before {
content: '';
position: absolute;
left: 10%;
top: -2px;
height: 1px;
width: 80%;
border-bottom: 1px solid #161419;
opacity: 0.4;
}
82 changes: 61 additions & 21 deletions src/components/TileMap/TileMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export class TileMap extends React.PureComponent<Props, State> {
panY: 0,
padding: 4,
isDraggable: true,
withZoomControls: false,
layers: [],
renderMap: renderMap
renderMap: renderMap,
}

private oldState: State
Expand All @@ -61,11 +62,11 @@ export class TileMap extends React.PureComponent<Props, State> {
pan: { x: panX, y: panY },
center: {
x: x == null ? initialX : x,
y: y == null ? initialY : y
y: y == null ? initialY : y,
},
size: zoom * size,
zoom,
popup: null
popup: null,
}
this.state = this.generateState(props, initialState)
this.oldState = this.state
Expand All @@ -86,12 +87,12 @@ export class TileMap extends React.PureComponent<Props, State> {
...nextState,
center: {
x: nextProps.x,
y: nextProps.y
y: nextProps.y,
},
pan: {
x: 0,
y: 0
}
y: 0,
},
}
}

Expand Down Expand Up @@ -122,7 +123,7 @@ export class TileMap extends React.PureComponent<Props, State> {
if (newZoom !== this.props.zoom && newZoom !== this.state.zoom) {
this.setState({
zoom: newZoom,
size: this.props.size * newZoom
size: this.props.size * newZoom,
})
}
}
Expand Down Expand Up @@ -174,7 +175,7 @@ export class TileMap extends React.PureComponent<Props, State> {
center,
pan,
size,
padding
padding,
})
return { ...viewport, pan, zoom, center, size }
}
Expand Down Expand Up @@ -208,18 +209,18 @@ export class TileMap extends React.PureComponent<Props, State> {

const boundaries = {
nw: { x: minX - halfWidth, y: maxY + halfHeight },
se: { x: maxX + halfWidth, y: minY - halfHeight }
se: { x: maxX + halfWidth, y: minY - halfHeight },
}

const viewport = {
nw: {
x: this.state.center.x - halfWidth,
y: this.state.center.y + halfHeight
y: this.state.center.y + halfHeight,
},
se: {
x: this.state.center.x + halfWidth,
y: this.state.center.y - halfHeight
}
y: this.state.center.y - halfHeight,
},
}

if (viewport.nw.x + newPan.x / newSize < boundaries.nw.x) {
Expand All @@ -238,7 +239,7 @@ export class TileMap extends React.PureComponent<Props, State> {
this.setState({
pan: newPan,
zoom: newZoom,
size: newSize
size: newSize,
})
this.renderMap()
this.debouncedUpdateCenter()
Expand All @@ -252,7 +253,7 @@ export class TileMap extends React.PureComponent<Props, State> {

const viewportOffset = {
x: (width - padding - 0.5) / 2 - center.x,
y: (height - padding) / 2 + center.y
y: (height - padding) / 2 + center.y,
}

const coordX = Math.round(panOffset.x - viewportOffset.x)
Expand Down Expand Up @@ -326,7 +327,7 @@ export class TileMap extends React.PureComponent<Props, State> {
if (this.mounted) {
this.setState(
{
popup: { x, y, top, left, visible: true }
popup: { x, y, top, left, visible: true },
},
() => onPopup(this.state.popup!)
)
Expand All @@ -352,8 +353,8 @@ export class TileMap extends React.PureComponent<Props, State> {
{
popup: {
...this.state.popup,
visible: false
}
visible: false,
},
},
() => {
onPopup(this.state.popup!)
Expand All @@ -371,12 +372,12 @@ export class TileMap extends React.PureComponent<Props, State> {
const newPan = { x: panX, y: panY }
const newCenter = {
x: center.x + Math.floor((pan.x - panX) / size),
y: center.y - Math.floor((pan.y - panY) / size)
y: center.y - Math.floor((pan.y - panY) / size),
}

this.setState({
pan: newPan,
center: newCenter
center: newCenter,
})
}

Expand All @@ -398,7 +399,7 @@ export class TileMap extends React.PureComponent<Props, State> {
nw,
se,
center,
layers
layers,
})
}

Expand Down Expand Up @@ -434,8 +435,29 @@ export class TileMap extends React.PureComponent<Props, State> {
return classes
}

handleZoomChange(zoomModifier: number) {
const { size, maxSize, minSize } = this.props
const { zoom } = this.state

const maxZoom = maxSize / size
const minZoom = minSize / size

const newZoom = Math.max(
minZoom,
Math.min(maxZoom, zoom + zoomModifier * this.getDzZoomModifier())
)
const newSize = newZoom * size

this.setState({
zoom: newZoom,
size: newSize,
})
this.renderMap()
}

render() {
const { width, height, className } = this.props
const { width, height, className, minSize, maxSize, withZoomControls } = this.props
const { size } = this.state

const styles = { width, height }

Expand All @@ -449,6 +471,24 @@ export class TileMap extends React.PureComponent<Props, State> {
height={height}
ref={this.refCanvas}
/>
{withZoomControls && (
<div className="zoom-controls">
<button
onClick={this.handleZoomChange.bind(this, 10)}
aria-label="Add zoom"
disabled={size === maxSize}
>
+
</button>
<button
onClick={this.handleZoomChange.bind(this, -10)}
aria-label="Decrease zoom"
disabled={size === minSize}
>
-
</button>
</div>
)}
</div>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/TileMap/TileMap.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type Props = {
isDraggable: boolean
/** amount of padding tiles */
padding: number
/** whether the zoom controls should appear */
withZoomControls: boolean
/** callbacks */
onMouseDown?: (x: number, y: number) => void
onMouseUp?: (x: number, y: number) => void
Expand Down
80 changes: 80 additions & 0 deletions stories/10_Atlas_zoom_controls.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react'
import { storiesOf } from '@storybook/react'
import { number } from '@storybook/addon-knobs'

import { TileMap, Layer } from '../src'

type AtlasTile = {
x: number
y: number
type: number
estate_id: number
left: number
top: number
topLeft: number
}

let atlas: Record<string, AtlasTile> | null = null

async function loadTiles() {
const resp = await fetch('https://api.decentraland.org/v1/tiles')
const json = await resp.json()
atlas = json.data as Record<string, AtlasTile>
}

loadTiles().catch(console.error)

const stories = storiesOf('TileMap', module)

export const COLOR_BY_TYPE = Object.freeze({
0: '#ff9990', // my parcels
1: '#ff4053', // my parcels on sale
2: '#ff9990', // my estates
3: '#ff4053', // my estates on sale
4: '#ffbd33', // parcels/estates where I have permissions
5: '#5054D4', // districts
6: '#563db8', // contributions
7: '#716C7A', // roads
8: '#70AC76', // plazas
9: '#3D3A46', // owned parcel/estate
10: '#3D3A46', // parcels on sale (we show them as owned parcels)
11: '#09080A', // unowned pacel/estate
12: '#18141a', // background
13: '#110e13', // loading odd
14: '#0d0b0e' // loading even
})

const atlasLayer: Layer = (x, y) => {
const id = x + ',' + y
if (atlas !== null && id in atlas) {
const tile = atlas[id]
const color = COLOR_BY_TYPE[tile.type]

const top = !!tile.top
const left = !!tile.left
const topLeft = !!tile.topLeft

return {
color,
top,
left,
topLeft
}
} else {
return {
color: (x + y) % 2 === 0 ? COLOR_BY_TYPE[12] : COLOR_BY_TYPE[13]
}
}
}

stories.add('10. With zoom controls', () => {
return (
<TileMap
className="atlas"
layers={[atlasLayer]}
x={number('x', 0)}
y={number('y', 0)}
withZoomControls
/>
)
})

0 comments on commit 5bceca1

Please sign in to comment.