UNPKG

4.65 kBTypeScriptView Raw
1import * as React from 'react';
2import { Map } from 'mapbox-gl';
3import { AnchorLimits } from './util/types';
4import { withMap } from './context';
5
6const triggerEvents = ['moveend', 'touchend', 'zoomend'];
7
8const scales = [
9 0.01,
10 0.02,
11 0.05,
12 0.1,
13 0.2,
14 0.5,
15 1,
16 2,
17 5,
18 10,
19 20,
20 50,
21 100,
22 200,
23 500,
24 1000,
25 2 * 1000,
26 5 * 1000,
27 10 * 1000
28];
29
30const positions = {
31 'top-right': { top: 10, right: 10, bottom: 'auto', left: 'auto' },
32 'top-left': { top: 10, left: 10, bottom: 'auto', right: 'auto' },
33 'bottom-right': { bottom: 10, right: 10, top: 'auto', left: 'auto' },
34 'bottom-left': { bottom: 10, left: 10, top: 'auto', right: 'auto' }
35};
36
37const containerStyle: React.CSSProperties = {
38 position: 'absolute',
39 zIndex: 10,
40 boxShadow: '0px 1px 4px rgba(0, 0, 0, .3)',
41 border: '1px solid rgba(0, 0, 0, 0.1)',
42 right: 50,
43 backgroundColor: '#fff',
44 opacity: 0.85,
45 display: 'flex',
46 flexDirection: 'row',
47 alignItems: 'baseline',
48 padding: '3px 7px'
49};
50
51const scaleStyle = {
52 border: '2px solid #7e8490',
53 boxShadow: '0px 1px 4px rgba(0, 0, 0, .3)',
54 borderTop: 'none',
55 height: 7,
56 borderBottomLeftRadius: 1,
57 borderBottomRightRadius: 1
58};
59
60const POSITIONS = Object.keys(positions);
61const MEASUREMENTS = ['km', 'mi'] as Measurement[];
62
63const MILE_IN_KILOMETERS = 1.60934;
64const MILE_IN_FEET = 5280;
65const KILOMETER_IN_METERS = 1000;
66
67const MIN_WIDTH_SCALE = 60;
68
69export type Measurement = 'km' | 'mi';
70
71export interface Props {
72 measurement?: Measurement;
73 position?: AnchorLimits;
74 style?: React.CSSProperties;
75 className?: string;
76 tabIndex?: number;
77 map: Map;
78}
79
80export interface State {
81 chosenScale: number;
82 scaleWidth: number;
83}
84
85export class ScaleControl extends React.Component<Props, State> {
86 public static defaultProps = {
87 measurement: MEASUREMENTS[0],
88 position: POSITIONS[2]
89 };
90
91 public state = {
92 chosenScale: 0,
93 scaleWidth: MIN_WIDTH_SCALE
94 };
95
96 public componentDidMount() {
97 this.setScale();
98
99 triggerEvents.forEach(event => {
100 this.props.map.on(event, this.setScale);
101 });
102 }
103
104 public componentWillUnmount() {
105 if (this.props.map) {
106 triggerEvents.forEach(event => {
107 this.props.map.off(event, this.setScale);
108 });
109 }
110 }
111
112 private setScale = () => {
113 const { measurement, map } = this.props;
114
115 // tslint:disable-next-line:no-any
116 const clientWidth = (map as any)._canvas.clientWidth;
117 // tslint:disable-next-line:no-any
118 const { _ne, _sw } = map.getBounds() as any;
119
120 const totalWidth = this._getDistanceTwoPoints(
121 [_sw.lng, _ne.lat],
122 [_ne.lng, _ne.lat],
123 measurement
124 );
125
126 const relativeWidth = totalWidth / clientWidth * MIN_WIDTH_SCALE;
127
128 const chosenScale = scales.reduce((acc, curr) => {
129 if (!acc && curr > relativeWidth) {
130 return curr;
131 }
132
133 return acc;
134 }, 0);
135
136 // tslint:disable-next-line:no-any
137 const scaleWidth = chosenScale / totalWidth * clientWidth;
138
139 this.setState({
140 chosenScale,
141 scaleWidth
142 });
143 };
144
145 private _getDistanceTwoPoints(x: number[], y: number[], measurement = 'km') {
146 const [lng1, lat1] = x;
147 const [lng2, lat2] = y;
148
149 // Radius of the earth in km or miles
150 const R = measurement === 'km' ? 6371 : 6371 / MILE_IN_KILOMETERS;
151 const dLat = this._deg2rad(lat2 - lat1);
152 const dLng = this._deg2rad(lng2 - lng1);
153 const a =
154 Math.sin(dLat / 2) * Math.sin(dLat / 2) +
155 Math.cos(this._deg2rad(lat1)) *
156 Math.cos(this._deg2rad(lat2)) *
157 Math.sin(dLng / 2) *
158 Math.sin(dLng / 2);
159 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
160 const d = R * c;
161
162 return d;
163 }
164
165 private _deg2rad(deg: number) {
166 return deg * (Math.PI / 180);
167 }
168
169 private _displayMeasurement(measurement: Measurement, chosenScale: number) {
170 if (chosenScale >= 1) {
171 return `${chosenScale} ${measurement}`;
172 }
173
174 if (measurement === 'mi') {
175 return `${Math.floor(chosenScale * MILE_IN_FEET)} ft`;
176 }
177
178 return `${Math.floor(chosenScale * KILOMETER_IN_METERS)} m`;
179 }
180
181 public render() {
182 const { measurement, style, position, className, tabIndex } = this.props;
183 const { chosenScale, scaleWidth } = this.state;
184
185 return (
186 <div
187 tabIndex={tabIndex}
188 style={{ ...containerStyle, ...positions[position!], ...style }}
189 className={className}
190 >
191 <div style={{ ...scaleStyle, width: scaleWidth }} />
192 <div style={{ paddingLeft: 10 }}>
193 {this._displayMeasurement(measurement!, chosenScale)}
194 </div>
195 </div>
196 );
197 }
198}
199
200export default withMap(ScaleControl);