├── .gitignore
├── LICENSE
├── README.md
├── babelPresets.js
├── demo
└── demo.gif
├── dist
├── common
│ ├── constants
│ │ ├── ClusterOptions.js
│ │ └── GeoJSONTypes.js
│ ├── hoc
│ │ ├── connectWithSpiderifierPoint.js
│ │ ├── detectLocationHasOverlappedPoints.js
│ │ ├── doZoomingOnClick.js
│ │ ├── index.js
│ │ └── spiderifier.css
│ └── utils
│ │ ├── calc.js
│ │ ├── cluster.js
│ │ ├── event.js
│ │ ├── index.js
│ │ ├── props.js
│ │ └── react.js
├── components
│ ├── MappedComponent.js
│ ├── MarkerLayer
│ │ ├── Component.js
│ │ └── index.js
│ └── ReactMapboxGlCluster
│ │ ├── ClusterLayer.css
│ │ ├── ClusterLayer.js
│ │ ├── MapboxGlCluster.js
│ │ ├── index.js
│ │ └── package.json
├── index.js
└── types
│ ├── common
│ ├── constants
│ │ ├── ClusterOptions.d.ts
│ │ └── GeoJSONTypes.d.ts
│ ├── hoc
│ │ ├── connectWithSpiderifierPoint.d.ts
│ │ ├── detectLocationHasOverlappedPoints.d.ts
│ │ ├── doZoomingOnClick.d.ts
│ │ └── index.d.ts
│ └── utils
│ │ ├── calc.d.ts
│ │ ├── cluster.d.ts
│ │ ├── event.d.ts
│ │ ├── index.d.ts
│ │ ├── props.d.ts
│ │ └── react.d.ts
│ ├── components
│ ├── MappedComponent.d.ts
│ ├── MarkerLayer
│ │ ├── Component.d.ts
│ │ └── index.d.ts
│ └── ReactMapboxGlCluster
│ │ ├── ClusterLayer.d.ts
│ │ ├── MapboxGlCluster.d.ts
│ │ └── index.d.ts
│ └── index.d.ts
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── data.js
├── index.css
├── index.js
├── lib
│ ├── common
│ │ ├── constants
│ │ │ ├── ClusterOptions.js
│ │ │ └── GeoJSONTypes.js
│ │ ├── hoc
│ │ │ ├── connectWithSpiderifierPoint.jsx
│ │ │ ├── detectLocationHasOverlappedPoints.jsx
│ │ │ ├── doZoomingOnClick.jsx
│ │ │ ├── index.js
│ │ │ └── spiderifier.css
│ │ └── utils
│ │ │ ├── calc.js
│ │ │ ├── cluster.js
│ │ │ ├── event.js
│ │ │ ├── index.js
│ │ │ ├── props.js
│ │ │ └── react.js
│ ├── components
│ │ ├── MappedComponent.jsx
│ │ ├── MarkerLayer
│ │ │ ├── Component.jsx
│ │ │ └── index.jsx
│ │ └── ReactMapboxGlCluster
│ │ │ ├── ClusterLayer.css
│ │ │ ├── ClusterLayer.jsx
│ │ │ ├── MapboxGlCluster.jsx
│ │ │ ├── index.js
│ │ │ └── package.json
│ └── index.js
├── logo.svg
├── react-app-env.d.ts
└── serviceWorker.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn.lock
22 | yarn-debug.log*
23 | yarn-error.log*
24 | /.history
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Thuan Bui
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-mapbox-gl-cluster
2 |
3 | [](https://github.com/thuanmb/react-mapbox-gl-cluster/blob/master/LICENSE)
4 | [](https://www.npmjs.com/package/react-mapbox-gl-cluster)
5 |
6 | The `React` component for the cluster layer in the `mapbox-gl`.
7 |
8 | The cluster layer has some build-in actions:
9 |
10 | 1. Zoom in when clicking on a cluster.
11 | 2. Show the spiderifiers when clicking on a cluster which contains the points at same location.
12 |
13 | This layer must be rendered inside `react-mapbox-gl` component.
14 |
15 | ## Examples:
16 |
17 | - https://github.com/thuanmb/react-mapbox-gl-cluster/blob/master/src/App.js
18 |
19 | 
20 |
21 | Please note that the `ReactMapboxGlCluster` should be used together with the `React` wrapper of `mapbox-gl` e.g. `react-mapbox-gl`.
22 | https://github.com/alex3165/react-mapbox-gl
23 |
24 | ```js
25 | import React, {Component} from 'react';
26 | import ReactMapboxGl from 'react-mapbox-gl';
27 | import {ReactMapboxGlCluster} from 'react-mapbox-gl-cluster';
28 | import {data} from './data';
29 |
30 | const Map = ReactMapboxGl({
31 | accessToken: process.env.MAPBOX_GL_TOKEN,
32 | });
33 |
34 | const mapProps = {
35 | center: [-95.7129, 37.0902],
36 | zoom: [3],
37 | style: 'mapbox://styles/mapbox/streets-v8',
38 | };
39 |
40 | class App extends Component {
41 | getEventHandlers() {
42 | return {
43 | onClick: (properties, coords, offset) =>
44 | console.log(`Receive event onClick at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
45 | onMouseEnter: (properties, coords, offset) =>
46 | console.log(`Receive event onMouseEnter at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
47 | onMouseLeave: (properties, coords, offset) =>
48 | console.log(`Receive event onMouseLeave at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
49 | onClusterClick: (properties, coords, offset) =>
50 | console.log(`Receive event onClusterClick at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
51 | };
52 | }
53 |
54 | render() {
55 | return (
56 |
57 |
60 |
61 | );
62 | }
63 | }
64 | ```
65 |
66 | ## Documentations
67 |
68 | #### Properties
69 |
70 | - `data (object)`
71 | Data source for layer. It must to follow FeatureCollection geojson format
72 |
73 | - `radius (number)`
74 | [Optional] Cluster radius, in pixels.
75 |
76 | - `minZoom (number)`
77 | [Optional] Minimum zoom level at which clusters are generated.
78 |
79 | - `maxZoom (number)`
80 | [Optional] Maximum zoom level at which clusters are generated.
81 |
82 | - `extent (number)`
83 | [Optional](Tiles) Tile extent. Radius is calculated relative to this value.
84 |
85 | - `nodeSize (number)`
86 | [Optional] Size of the KD-tree leaf node. Affects performance.
87 |
88 | - `pointClassName (string)`
89 | [Optional] The class name of each point.
90 |
91 | - `pointStyles (object)`
92 | [Optional] The class name of each cluster.
93 |
94 | - `clusterClassName (string)`
95 | [Optional] The class name of each cluster.
96 |
97 | - `spiralComponent (element)`
98 | [Optional] The custom component for the spiral. Example usage:
99 | ```
100 | const CustomSpiralComponent = ({properties, ...restProps}) => {
101 | const onClick = e => {
102 | console.log(`Receive event onClick in spiral at properties: ${JSON.stringify(properties)}`);
103 | };
104 | return ;
105 | };
106 |
107 | ...
108 |
111 | ```
112 |
113 | - `markerComponent (element)`
114 | [Optional] The custom component for marker. Example usage:
115 | ```
116 | const CustomeMarkerComponent = ({properties, className, cssObject}) => {
117 | const onClick = e => {
118 | console.log(`Receive event onClick in marker at properties: ${JSON.stringify(properties)}`);
119 | };
120 | return ;
121 | };
122 | ...
123 |
126 | ```
127 |
128 | - clusterClickEnabled (bool)
129 | [Optional] Enable/disable zoom on cluster click
130 |
131 | #### Events
132 |
133 | - `onClick (function)`
134 | [Optional] Handler for when user on marker
135 |
136 | - `onMouseEnter (function)`
137 | [Optional] Handle when user move the mouse enter a point
138 |
139 | - `onMouseLeave (function)`
140 | [Optional] Handle when user move the mouse leave a point
141 |
142 | - `onClusterClick (function)`
143 | [Optional] Handle when user click on cluster
144 |
145 | - `onClusterMouseEnter (function)`
146 | [Optional] Handle when user move the mouse enter a cluster
147 |
148 | - `onClusterMouseLeave (function)`
149 | [Optional] Handle when user move the mouse leave a cluster
150 |
151 | ## ChangeLog:
152 |
153 | ### 1.20.0
154 |
155 | - Upgrading packages
156 |
157 | ### 1.19.0
158 |
159 | - Upgrading packages
160 |
161 | ### 1.18.0
162 |
163 | - Removing node-sass dependency
164 |
165 | ### 1.17.0
166 |
167 | - Fix TS compiling does not
168 |
169 | ### 1.15.0
170 |
171 | - Upgrading packages
172 |
173 | ### 1.14.0
174 |
175 | - Upgrading packages
176 |
177 | ### 1.12.0
178 |
179 | - Upgrading packages
180 |
181 | ### 1.11.0
182 |
183 | - Support `clusterClickEnabled` flag to enable/disable on cluster click event
184 | - Bumps depedencies version
185 |
186 | ### 1.10.0
187 |
188 | - Use caret version for react-mapbox-gl
189 |
190 | ### 1.7.0
191 |
192 | - Upgrading packages
193 |
194 | ### 1.6.0
195 |
196 | - Upgrading packages
197 |
198 | ### 1.5.0
199 |
200 | - Upgrading packages
201 |
202 | ### 1.4.0
203 |
204 | - Upgrading packages
205 |
206 | ### 1.3.0
207 |
208 | - Upgrading packages
209 |
210 | ### 1.2.1
211 |
212 | - Fix bundling issue.
213 |
214 | ### 1.2.0
215 |
216 | - Support custom marker.
217 |
218 | ```
219 | const CustomeMarkerComponent = ({properties, className, cssObject}) => {
220 | const onClick = e => {
221 | console.log(`Receive event onClick in marker at properties: ${JSON.stringify(properties)}`);
222 | };
223 | return ;
224 | };
225 | ...
226 |
229 | ```
230 |
231 | ### 1.1.0
232 |
233 | - Support custom spiral.
234 |
235 | ```
236 | const CustomSpiralComponent = ({properties, ...restProps}) => {
237 | const onClick = e => {
238 | console.log(`Receive event onClick in spiral at properties: ${JSON.stringify(properties)}`);
239 | };
240 | return ;
241 | };
242 |
243 | ...
244 |
247 | ```
248 |
249 | ### 1.0.0
250 |
251 | - Upgrading depedencies to latest version.
252 |
253 | ### 0.2.0 [BREAKING CHANGES]
254 |
255 | - Upgrade all packages to latest version. These packages include: `react`, `mapbox-gl`, `react-mapbox-gl`, `react-mapbox-gl-spiderifier`...
256 |
257 | ### 0.1.7
258 |
259 | - Fix JS error when click on a marker
260 |
261 | ## Development
262 |
263 | ### Starting the server in local
264 | - Adding the `.env` file
265 | - Adding the key `REACT_APP_MAPBOX_GL_TOKEN` into the `.env` file
266 | - Starting the server by: `yarn start`
267 |
268 | ## Upgrading dependencies checklist
269 | - Upgrading the dependencies
270 | ```
271 | yarn upgrade-interactive --latest
272 | ```
273 | - Pull the latest code
274 | - Create a new branch
275 | - Test if the app works after upgrading: `yarn start`
276 | - Build the package: `yarn build`
277 | - Increasing the package version in the `package.json`
278 | - Adding the release note in the `README`
279 | - Push the change into Github
280 | - Publish the package into npmjs: `npm publish`
281 |
--------------------------------------------------------------------------------
/babelPresets.js:
--------------------------------------------------------------------------------
1 | module.exports = () => ({
2 | presets: [[require("babel-preset-react-app"), { absoluteRuntime: false }]]
3 | });
4 |
--------------------------------------------------------------------------------
/demo/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thuanmb/react-mapbox-gl-cluster/c7ba16f3b3726aaf96e6583af57ff6f69a825dd2/demo/demo.gif
--------------------------------------------------------------------------------
/dist/common/constants/ClusterOptions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.ClusterOptions = void 0;
7 | const ClusterOptions = {
8 | NearestPointsRadius: 7,
9 | ZoomLevel: 17
10 | };
11 | exports.ClusterOptions = ClusterOptions;
--------------------------------------------------------------------------------
/dist/common/constants/GeoJSONTypes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.NormalTypes = exports.ListKeysByType = exports.GeoJSONTypes = exports.CollectionTypes = void 0;
7 | const GeoJSONTypes = {
8 | Point: "Point",
9 | MultiPoint: "MultiPoint",
10 | LineString: "LineString",
11 | MultiLineString: "MultiLineString",
12 | Polygon: "Polygon",
13 | MultiPolygon: "MultiPolygon",
14 | GeometryCollection: "GeometryCollection",
15 | FeatureCollection: "FeatureCollection"
16 | };
17 | exports.GeoJSONTypes = GeoJSONTypes;
18 | const NormalTypes = [GeoJSONTypes.Point, GeoJSONTypes.MultiPoint, GeoJSONTypes.LineString, GeoJSONTypes.MultiLineString, GeoJSONTypes.Polygon, GeoJSONTypes.MultiPolygon];
19 | exports.NormalTypes = NormalTypes;
20 | const CollectionTypes = [GeoJSONTypes.GeometryCollection, GeoJSONTypes.FeatureCollection];
21 | exports.CollectionTypes = CollectionTypes;
22 | const ListKeysByType = {
23 | [GeoJSONTypes.GeometryCollection]: "geometries",
24 | [GeoJSONTypes.FeatureCollection]: "features"
25 | };
26 | exports.ListKeysByType = ListKeysByType;
--------------------------------------------------------------------------------
/dist/common/hoc/connectWithSpiderifierPoint.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _propTypes = _interopRequireDefault(require("prop-types"));
13 |
14 | var _lodash = _interopRequireDefault(require("lodash"));
15 |
16 | var _utils = require("../utils");
17 |
18 | var _reactMapboxGlSpiderifier = require("react-mapbox-gl-spiderifier");
19 |
20 | var _invariant = require("@turf/invariant");
21 |
22 | var _ClusterOptions = require("../constants/ClusterOptions");
23 |
24 | var _MappedComponent = _interopRequireDefault(require("../../components/MappedComponent"));
25 |
26 | require("./spiderifier.css");
27 |
28 | const SPIDERIFIER_PROPS = ["coordinates", "circleSpiralSwitchover", "circleFootSeparation", "spiralFootSeparation", "spiralLengthStart", "spiralLengthFactor", "animate", "animationSpeed", "transformSpiderLeft", "transformSpiderTop", "showingLegs", "onClick", "onMouseDown", "onMouseEnter", "onMouseLeave", "onMouseMove", "onMouseOut", "onMouseOver", "onMouseUp"];
29 | const MARKER_PROPS = ["data", "radius", "minZoom", "maxZoom", "extent", "nodeSize", "pointClassName", "pointStyles", "clusterClassName", "clusterClassName", "markerComponent", "onMouseLeave", "onClick", "onClusterClick", "onClusterMouseEnter", "onClusterMouseLeave", "clusterClickEnabled"];
30 | /**
31 | * @type Class
32 | */
33 |
34 | const connectWithSpiderifierPoint = WrappedComponent => {
35 | class ConnectedWithSpiderifierComponent extends _MappedComponent.default {
36 | constructor(props) {
37 | super(props);
38 |
39 | this.onClickOverlappedPoints = (points, coordinates) => {
40 | this._updateSpiderifierProps([points], coordinates);
41 | };
42 |
43 | this.onMapChange = () => {
44 | const {
45 | onlySpiderifier
46 | } = this.props;
47 |
48 | if (!onlySpiderifier && _lodash.default.isArray(this._spiderifieredLocations)) {
49 | const {
50 | data,
51 | radius
52 | } = this.props;
53 | const map = this.getMapInstance();
54 |
55 | this._spiderifieredLocations.forEach(lngLat => {
56 | const points = (0, _utils.findPointsWithSameLocation)(data, lngLat, map, radius);
57 |
58 | if (!points) {
59 | this.onSpiderifierRemoved(lngLat);
60 | }
61 | });
62 | }
63 | };
64 |
65 | this.state = {
66 | overlappedPointsGroup: null
67 | };
68 | this.registeredEvents = false;
69 | }
70 |
71 | componentDidUpdate(prevProps) {
72 | this._checkAndUpdatePoints(prevProps);
73 |
74 | this.bindEvents();
75 | }
76 |
77 | shouldComponentUpdate(nextProps, nextState) {
78 | return (0, _utils.checkPropsChange)(this.props, nextProps, ["data", "showInitialSpiderifier", "onlySpiderifier", "circleFootSeparation", "transformSpiderLeft", "showingLegs"], _lodash.default.isEqual) || !_lodash.default.isEqual(this.state, nextState);
79 | }
80 |
81 | componentWillUnmount() {
82 | this.unbindEvents();
83 | }
84 |
85 | bindEvents() {
86 | const map = this.getMapInstance();
87 |
88 | if (map && !this.registeredEvents) {
89 | map.on("zoomend", this.onMapChange);
90 | this.registeredEvents = true;
91 | }
92 | }
93 |
94 | unbindEvents() {
95 | const map = this.getMapInstance();
96 |
97 | if (map) {
98 | map.off("zoomend", this.onMapChange);
99 | }
100 | }
101 |
102 | onSpiderifierRemoved(lngLat) {
103 | const {
104 | overlappedPointsGroup
105 | } = this.state;
106 |
107 | if (_lodash.default.isArray(overlappedPointsGroup)) {
108 | const removedIndex = overlappedPointsGroup.findIndex(_ref => {
109 | let {
110 | coordinates
111 | } = _ref;
112 | return _lodash.default.isEqual(coordinates, lngLat);
113 | });
114 |
115 | if (removedIndex > -1) {
116 | const newGroup = [...overlappedPointsGroup.slice(0, removedIndex), ...overlappedPointsGroup.slice(removedIndex + 1)];
117 | this.setState({
118 | overlappedPointsGroup: newGroup
119 | });
120 | }
121 | }
122 |
123 | const {
124 | onSpiderifierRemoved
125 | } = this.props;
126 |
127 | if (_lodash.default.isFunction(onSpiderifierRemoved)) {
128 | onSpiderifierRemoved(lngLat);
129 | }
130 | }
131 |
132 | _checkAndUpdatePoints(prevProps) {
133 | if ((0, _utils.checkPropsChange)(this.props, prevProps, ["data", "showInitialSpiderifier", "onlySpiderifier"], _lodash.default.isEqual)) {
134 | this._updatePoints();
135 | }
136 | }
137 |
138 | _getComponentProps(keys) {
139 | return _lodash.default.pick(this.props, keys);
140 | }
141 |
142 | _getWrappedComponentProps() {
143 | return this._getComponentProps(MARKER_PROPS);
144 | }
145 |
146 | _getSpiderifierComponentProps() {
147 | return this._getComponentProps(SPIDERIFIER_PROPS);
148 | }
149 |
150 | _groupNearestPoint(props) {
151 | const {
152 | data,
153 | showInitialSpiderifier,
154 | onlySpiderifier
155 | } = props;
156 | const map = this.getMapInstance();
157 | const groupedPoints = (0, _utils.groupNearestPointsByRadius)(data, map, _ClusterOptions.ClusterOptions.NearestPointsRadius);
158 |
159 | if (groupedPoints.length > 0) {
160 | if (onlySpiderifier && groupedPoints.length === 1) {
161 | this._updateSpiderifierProps(groupedPoints);
162 | } else if (showInitialSpiderifier) {
163 | let firstGroup = groupedPoints.find(group => group.length > 1);
164 |
165 | if (firstGroup == null) {
166 | firstGroup = groupedPoints[0];
167 | }
168 |
169 | this._updateSpiderifierProps([firstGroup]);
170 | }
171 | }
172 | }
173 |
174 | _processSpiderifyProperties(props) {
175 | const {
176 | spiderifyPropsProcessor
177 | } = this.props;
178 |
179 | if (_lodash.default.isFunction(spiderifyPropsProcessor)) {
180 | return spiderifyPropsProcessor(props);
181 | }
182 |
183 | return props;
184 | }
185 |
186 | _renderSpiderifierContent(key, properties) {
187 | const {
188 | spiralComponent: SpiralComponent
189 | } = this.props;
190 |
191 | if (SpiralComponent) {
192 | return /*#__PURE__*/_react.default.createElement(SpiralComponent, {
193 | key: key,
194 | properties: properties
195 | });
196 | }
197 |
198 | return /*#__PURE__*/_react.default.createElement("div", {
199 | className: "spiderifier-marker-content",
200 | key: key,
201 | properties: properties
202 | }, /*#__PURE__*/_react.default.createElement("div", null, properties.label));
203 | }
204 |
205 | _renderSpiderifier() {
206 | const {
207 | overlappedPointsGroup
208 | } = this.state;
209 |
210 | if (overlappedPointsGroup && overlappedPointsGroup.length > 0) {
211 | const spiderifierComponentProps = this._getSpiderifierComponentProps();
212 |
213 | return overlappedPointsGroup.map((overlappedPoints, index) => {
214 | const {
215 | coordinates,
216 | markers
217 | } = overlappedPoints;
218 | return /*#__PURE__*/_react.default.createElement(_reactMapboxGlSpiderifier.ReactMapboxGlSpiderifier, Object.assign({
219 | key: index
220 | }, spiderifierComponentProps, {
221 | coordinates: coordinates
222 | }), markers.map((marker, index) => this._renderSpiderifierContent(index, marker)));
223 | });
224 | }
225 |
226 | return null;
227 | }
228 |
229 | _shouldRenderClusterLayer() {
230 | const {
231 | onlySpiderifier,
232 | overlappedPointsGroup
233 | } = this.props;
234 | return !onlySpiderifier || !overlappedPointsGroup || overlappedPointsGroup.length > 1;
235 | }
236 |
237 | _updatePoints() {
238 | let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
239 | const {
240 | data,
241 | showInitialSpiderifier,
242 | onlySpiderifier
243 | } = props;
244 |
245 | if (data != null && (showInitialSpiderifier || onlySpiderifier)) {
246 | this._groupNearestPoint(props);
247 | }
248 | }
249 |
250 | _updateSpiderifierProps(group, coordinates) {
251 | this._spiderifieredLocations = [];
252 |
253 | if (group.length > 0) {
254 | const overlappedPointsGroup = group.map(points => {
255 | if (points.length > 0) {
256 | const properties = points.map(feature => feature.properties);
257 | let coords = coordinates;
258 |
259 | if (coords == null) {
260 | coords = (0, _invariant.getCoord)(points[0]);
261 | }
262 |
263 | return {
264 | markers: this._processSpiderifyProperties(properties),
265 | coordinates: coords
266 | };
267 | }
268 |
269 | return null;
270 | });
271 | const {
272 | onShowSpiderifier
273 | } = this.props;
274 | overlappedPointsGroup.forEach(group => {
275 | const {
276 | coordinates,
277 | markers
278 | } = group;
279 |
280 | this._spiderifieredLocations.push(coordinates);
281 |
282 | if (_lodash.default.isFunction(onShowSpiderifier)) {
283 | onShowSpiderifier(coordinates, markers);
284 | }
285 | });
286 | this.setState({
287 | overlappedPointsGroup
288 | });
289 | }
290 | }
291 |
292 | render() {
293 | const wrappedComponentProps = this._getWrappedComponentProps();
294 |
295 | return /*#__PURE__*/_react.default.createElement("div", null, this._shouldRenderClusterLayer() && /*#__PURE__*/_react.default.createElement(WrappedComponent, Object.assign({}, wrappedComponentProps, {
296 | onClickOverlappedPoints: this.onClickOverlappedPoints
297 | })), this._renderSpiderifier());
298 | }
299 |
300 | }
301 |
302 | ConnectedWithSpiderifierComponent.propTypes = {
303 | /**
304 | * Indicate if the spiderifier should be shown for the first overlapped point onload
305 | */
306 | showInitialSpiderifier: _propTypes.default.bool,
307 |
308 | /**
309 | * Indicate if the spiderifier should be shown without wrapped component
310 | */
311 | onlySpiderifier: _propTypes.default.bool,
312 |
313 | /**
314 | * Handler to transform the properties of each point
315 | */
316 | spiderifyPropsProcessor: _propTypes.default.func,
317 |
318 | /**
319 | * Callback when a spiderifier shown
320 | */
321 | onShowSpiderifier: _propTypes.default.func,
322 |
323 | /**
324 | * [Optional] Handle when user do zoom/move to change the map and made the points
325 | * on the map changed and don't have overlapped points anymore
326 | */
327 | onSpiderifierRemoved: _propTypes.default.func,
328 |
329 | /**
330 | * Allow to customize the spiral component
331 | */
332 | spiralComponent: _propTypes.default.oneOfType([_propTypes.default.element, _propTypes.default.func])
333 | };
334 | ConnectedWithSpiderifierComponent.defaultProps = { ...WrappedComponent.defaultProps,
335 | ..._reactMapboxGlSpiderifier.ReactMapboxGlSpiderifier.defaultProps
336 | };
337 | return ConnectedWithSpiderifierComponent;
338 | };
339 |
340 | var _default = connectWithSpiderifierPoint;
341 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/common/hoc/detectLocationHasOverlappedPoints.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _propTypes = _interopRequireDefault(require("prop-types"));
13 |
14 | var _lodash = _interopRequireDefault(require("lodash"));
15 |
16 | var _helpers = require("@turf/helpers");
17 |
18 | var _utils = require("../utils");
19 |
20 | var _ClusterOptions = require("../constants/ClusterOptions");
21 |
22 | var _MappedComponent = _interopRequireDefault(require("../../components/MappedComponent"));
23 |
24 | /**
25 | * @type Class
26 | */
27 | const detectLocationHasOverlappedPoints = WrappedComponent => {
28 | class LayerWithOverlappedPointComponent extends _MappedComponent.default {
29 | constructor() {
30 | super(...arguments);
31 |
32 | this.onClick = (properties, lngLat, event, meta) => {
33 | const {
34 | onClick
35 | } = this.props;
36 |
37 | this._handleClick(properties, lngLat, event, meta, onClick);
38 | };
39 |
40 | this.onClusterClick = (properties, lngLat, event, meta) => {
41 | const {
42 | onClusterClick
43 | } = this.props;
44 |
45 | this._handleClick(properties, lngLat, event, meta, onClusterClick);
46 | };
47 | }
48 |
49 | _handleClick(properties, lngLat, event, meta, callback) {
50 | if (!_lodash.default.isArray(properties)) {
51 | if (_lodash.default.isFunction(callback)) {
52 | callback(properties, lngLat, event, meta);
53 | }
54 |
55 | return true;
56 | }
57 |
58 | const {
59 | onClickOverlappedPoints
60 | } = this.props;
61 | const map = this.getMapInstance();
62 | const features = properties.map(prop => (0, _helpers.point)(prop.coordinates, prop));
63 | const data = (0, _helpers.featureCollection)(features);
64 | const points = (0, _utils.findPointsWithSameLocation)(data, lngLat, map, _ClusterOptions.ClusterOptions.NearestPointsRadius, _ClusterOptions.ClusterOptions.ZoomLevel);
65 |
66 | if (points) {
67 | if (_lodash.default.isFunction(onClickOverlappedPoints)) {
68 | onClickOverlappedPoints(features, lngLat, event, meta);
69 | return false;
70 | }
71 | } else if (_lodash.default.isFunction(callback)) {
72 | callback(properties, lngLat, event, meta);
73 | }
74 |
75 | return true;
76 | }
77 |
78 | render() {
79 | const props = { ...this.props,
80 | onClick: this.onClick,
81 | onClusterClick: this.onClusterClick
82 | };
83 | return /*#__PURE__*/_react.default.createElement(WrappedComponent, props);
84 | }
85 |
86 | }
87 |
88 | LayerWithOverlappedPointComponent.propTypes = {
89 | /**
90 | * [Optional] Handle when user click on a location which has overlapped points
91 | */
92 | onClickOverlappedPoints: _propTypes.default.func
93 | };
94 | LayerWithOverlappedPointComponent.defaultProps = { ...WrappedComponent.defaultProps
95 | };
96 | return LayerWithOverlappedPointComponent;
97 | };
98 |
99 | var _default = detectLocationHasOverlappedPoints;
100 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/common/hoc/doZoomingOnClick.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _propTypes = _interopRequireDefault(require("prop-types"));
13 |
14 | var _lodash = _interopRequireDefault(require("lodash"));
15 |
16 | var _MappedComponent = _interopRequireDefault(require("../../components/MappedComponent"));
17 |
18 | var _utils = require("../utils");
19 |
20 | /**
21 | * @type Class
22 | */
23 | const doZoomingOnClick = WrappedComponent => {
24 | class ZoomableComponent extends _MappedComponent.default {
25 | constructor() {
26 | super(...arguments);
27 |
28 | this.onClusterClick = (properties, lngLat, event, meta) => {
29 | const {
30 | onClusterClick,
31 | clusterClickEnabled
32 | } = this.props;
33 |
34 | if (!clusterClickEnabled) {
35 | return;
36 | }
37 |
38 | const map = this.getMapInstance();
39 | const currentZoom = map.getZoom();
40 | const maxZoom = map.getMaxZoom();
41 | const zoom = (0, _utils.calculateNextZoomLevel)(currentZoom, maxZoom);
42 | map.flyTo({
43 | center: lngLat,
44 | zoom
45 | });
46 |
47 | this._handleClick(properties, lngLat, event, meta, onClusterClick);
48 | };
49 | }
50 |
51 | _handleClick(properties, lngLat, event, meta, callback) {
52 | if (_lodash.default.isFunction(callback)) {
53 | callback(properties, lngLat, event, meta);
54 | }
55 | }
56 |
57 | render() {
58 | const props = { ...this.props,
59 | onClusterClick: this.onClusterClick
60 | };
61 | return /*#__PURE__*/_react.default.createElement(WrappedComponent, props);
62 | }
63 |
64 | }
65 |
66 | ZoomableComponent.propTypes = {
67 | clusterClickEnabled: _propTypes.default.bool
68 | };
69 | ZoomableComponent.defaultProps = { ...WrappedComponent.defaultProps,
70 | clusterClickEnabled: true
71 | };
72 | return ZoomableComponent;
73 | };
74 |
75 | var _default = doZoomingOnClick;
76 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/common/hoc/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | Object.defineProperty(exports, "connectWithSpiderifierPoint", {
9 | enumerable: true,
10 | get: function () {
11 | return _connectWithSpiderifierPoint.default;
12 | }
13 | });
14 | Object.defineProperty(exports, "detectLocationHasOverlappedPoints", {
15 | enumerable: true,
16 | get: function () {
17 | return _detectLocationHasOverlappedPoints.default;
18 | }
19 | });
20 | Object.defineProperty(exports, "doZoomingOnClick", {
21 | enumerable: true,
22 | get: function () {
23 | return _doZoomingOnClick.default;
24 | }
25 | });
26 |
27 | var _connectWithSpiderifierPoint = _interopRequireDefault(require("./connectWithSpiderifierPoint"));
28 |
29 | var _detectLocationHasOverlappedPoints = _interopRequireDefault(require("./detectLocationHasOverlappedPoints"));
30 |
31 | var _doZoomingOnClick = _interopRequireDefault(require("./doZoomingOnClick"));
--------------------------------------------------------------------------------
/dist/common/hoc/spiderifier.css:
--------------------------------------------------------------------------------
1 | .spiderifier-marker-content {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 28px;
6 | height: 28px;
7 | color: rgb(255, 255, 255);
8 | background-color: rgb(195, 38, 94);
9 | border-color: rgb(255, 255, 255);
10 | border-width: 1px;
11 | border-radius: 50%;
12 | margin-left: -14px;
13 | margin-top: -14px;
14 | box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.29);
15 | font-size: 8px;
16 | font-weight: bold;
17 | text-align: center;
18 | border: 1px solid;
19 | }
20 |
--------------------------------------------------------------------------------
/dist/common/utils/calc.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.calculateNextZoomLevel = void 0;
7 |
8 | /**
9 | * Calculate the next zoom level base on the current zoom
10 | * @param {number} currentZoom the current zoom level
11 | * @param {number} maxZoom the max zoom level of the map
12 | * @param {number} extraZoomLevels how many extra level more for each zoom
13 | */
14 | const calculateNextZoomLevel = function (currentZoom) {
15 | let maxZoom = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 20;
16 | let extraZoomLevels = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 2;
17 |
18 | if (currentZoom >= 14 && currentZoom < maxZoom - 2) {
19 | return maxZoom - 2;
20 | }
21 |
22 | if (currentZoom >= maxZoom - 2) {
23 | return maxZoom;
24 | }
25 |
26 | const delta = maxZoom - currentZoom;
27 | const percentage = delta / maxZoom;
28 | const zoom = currentZoom + extraZoomLevels * percentage + extraZoomLevels * Math.pow(percentage, 2) + extraZoomLevels * Math.pow(percentage, 3);
29 | return zoom;
30 | };
31 |
32 | exports.calculateNextZoomLevel = calculateNextZoomLevel;
--------------------------------------------------------------------------------
/dist/common/utils/cluster.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.groupNearestPointsByRadius = exports.findPointsWithSameLocation = exports.createClusters = void 0;
9 |
10 | var _lodash = _interopRequireDefault(require("lodash"));
11 |
12 | var _supercluster = _interopRequireDefault(require("supercluster"));
13 |
14 | var _invariant = require("@turf/invariant");
15 |
16 | var _mapboxGl = require("mapbox-gl");
17 |
18 | var _GeoJSONTypes = require("../constants/GeoJSONTypes");
19 |
20 | const RADIUS_TO_EXTENDS = 200;
21 |
22 | const checkCollectionGeoJSON = data => _GeoJSONTypes.CollectionTypes.indexOf(data.type) !== -1;
23 |
24 | const createBoundsFromCoordinates = (coordinates, bounds) => {
25 | if (bounds == null) {
26 | return new _mapboxGl.LngLatBounds(coordinates, coordinates);
27 | }
28 |
29 | return bounds.extend(coordinates);
30 | };
31 |
32 | const extendBounds = function (boundary) {
33 | let radius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
34 | const boundObj = new _mapboxGl.LngLatBounds(boundary);
35 | const ne = boundObj.getNorthEast();
36 | const neBound = ne.toBounds(radius / 2);
37 | const sw = boundObj.getSouthWest();
38 | const swBound = sw.toBounds(radius / 2);
39 | return _lodash.default.flatten([swBound.getSouthWest().toArray(), neBound.getNorthEast().toArray()]);
40 | };
41 |
42 | const flattenCoordinates = (coordinates, positionType) => {
43 | let depth;
44 |
45 | switch (positionType) {
46 | case _GeoJSONTypes.GeoJSONTypes.MultiPoint:
47 | case _GeoJSONTypes.GeoJSONTypes.LineString:
48 | depth = 0;
49 | break;
50 |
51 | case _GeoJSONTypes.GeoJSONTypes.Polygon:
52 | case _GeoJSONTypes.GeoJSONTypes.MultiLineString:
53 | depth = 1;
54 | break;
55 |
56 | case _GeoJSONTypes.GeoJSONTypes.MultiPolygon:
57 | depth = 2;
58 | break;
59 |
60 | case _GeoJSONTypes.GeoJSONTypes.Point:
61 | default:
62 | depth = -1;
63 | }
64 |
65 | if (depth === -1) {
66 | return [coordinates];
67 | }
68 |
69 | return _lodash.default.flattenDepth(coordinates, depth);
70 | };
71 |
72 | const getCoordinateForPosition = function (position) {
73 | let geoJSONType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _GeoJSONTypes.GeoJSONTypes.FeatureCollection;
74 |
75 | if (geoJSONType === _GeoJSONTypes.GeoJSONTypes.FeatureCollection) {
76 | return position.geometry.coordinates;
77 | }
78 |
79 | return position.coordinates;
80 | };
81 |
82 | const getFeatureList = geoJSON => {
83 | const {
84 | type
85 | } = geoJSON;
86 | const key = _GeoJSONTypes.ListKeysByType[type];
87 | return geoJSON[key];
88 | };
89 |
90 | const getTypeForPosition = (position, geoJSONType) => {
91 | if (geoJSONType === _GeoJSONTypes.GeoJSONTypes.FeatureCollection) {
92 | return position.geometry.type;
93 | }
94 |
95 | return position.type;
96 | };
97 |
98 | const roundCoords = coords => [_lodash.default.round(coords[0], 4), _lodash.default.round(coords[1], 4)];
99 | /**
100 | * Calculate the boundary of a geojson
101 | * @param {object} data a geojson in any format
102 | * @param? {*} totalBounds [Optional] if given, the boundary will be calculated base on the current "totalBounds"
103 | * @return {LngLatBounds} the total boundary
104 | */
105 |
106 |
107 | const calculateBoundary = function (data) {
108 | let totalBounds = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
109 | const {
110 | type
111 | } = data;
112 |
113 | if (checkCollectionGeoJSON(data)) {
114 | const features = getFeatureList(data);
115 | features.forEach(feature => {
116 | let coordinates = getCoordinateForPosition(feature, type);
117 | let featureType = getTypeForPosition(feature, type);
118 | coordinates = flattenCoordinates(coordinates, featureType);
119 |
120 | if (!_lodash.default.isArray(coordinates)) {
121 | return totalBounds;
122 | }
123 |
124 | if (!totalBounds) {
125 | totalBounds = new _mapboxGl.LngLatBounds(coordinates[0], coordinates[0]);
126 | }
127 |
128 | totalBounds = coordinates.reduce(function (bounds, coord) {
129 | return bounds.extend(coord);
130 | }, totalBounds);
131 | });
132 | return totalBounds;
133 | }
134 |
135 | const coordinates = (0, _invariant.getCoord)(data);
136 | return createBoundsFromCoordinates(coordinates, totalBounds);
137 | };
138 | /**
139 | * Find the list of point that inside a specific radius
140 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
141 | * @param {MapBox} mapBox Required. The mapbox instance
142 | * @param {number} zoom The zoom level, at which the points is clustered
143 | * @return {Array} The list of feature
144 | */
145 |
146 |
147 | const createClusters = function (data, mapBox) {
148 | let radius = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 60;
149 | let zoom = arguments.length > 3 ? arguments[3] : undefined;
150 |
151 | if (!data || !data.features || !_lodash.default.isArray(data.features)) {
152 | throw new Error("Data cannot be empty");
153 | }
154 |
155 | if (!mapBox) {
156 | throw new Error("Mapbox instance must be provided");
157 | }
158 |
159 | const superC = new _supercluster.default({
160 | radius,
161 | maxZoom: mapBox.getMaxZoom()
162 | });
163 | const featureList = getFeatureList(data);
164 | superC.load(featureList);
165 |
166 | if (!zoom) {
167 | zoom = mapBox.getZoom();
168 | }
169 |
170 | let boundary = _lodash.default.isEmpty(featureList) ? [0, 0, 0, 0] : _lodash.default.flatten(calculateBoundary(data).toArray()); // in case of all points at the same location,
171 | // extends its coords by 200 meters radius to make superC work.
172 |
173 | boundary = extendBounds(boundary, RADIUS_TO_EXTENDS);
174 | const clusters = featureList.length > 1 ? superC.getClusters(boundary, Math.round(zoom)) : featureList;
175 | return {
176 | superC,
177 | clusters
178 | };
179 | };
180 | /**
181 | * Find the list of point that have a similar location (lngLat)
182 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
183 | * @param {Coordinate} lngLat Required. The coordinate follow format [longitude, latitude]
184 | * @param {MapBox} mapBox Required. The mapbox instance
185 | * @param {number} radius The radius of the cluster
186 | * @param {number} zoom The zoom level, at which the points is clustered
187 | * @return {Array} The list of point at the same location. Null if cannot find the
188 | * similar points
189 | */
190 |
191 |
192 | exports.createClusters = createClusters;
193 |
194 | const findPointsWithSameLocation = function (data, lngLat, mapBox) {
195 | let radius = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 5;
196 | let zoom = arguments.length > 4 ? arguments[4] : undefined;
197 |
198 | if (!data || !data.features || !_lodash.default.isArray(data.features)) {
199 | throw new Error("Data cannot be empty");
200 | }
201 |
202 | if (!lngLat || !_lodash.default.isArray(lngLat)) {
203 | throw new Error("Specific location cannot be empty");
204 | }
205 |
206 | if (!mapBox) {
207 | throw new Error("Mapbox instance must be provided");
208 | }
209 |
210 | const {
211 | clusters,
212 | superC
213 | } = createClusters(data, mapBox, radius, zoom);
214 | const clusterAtLngLat = clusters.find(cluster => _lodash.default.isEqual(roundCoords(cluster.geometry.coordinates), roundCoords(lngLat)));
215 |
216 | if (clusterAtLngLat) {
217 | const {
218 | cluster,
219 | cluster_id,
220 | point_count
221 | } = clusterAtLngLat.properties;
222 |
223 | if (cluster && point_count > 1) {
224 | try {
225 | return superC.getLeaves(cluster_id, Infinity);
226 | } catch (e) {
227 | return null;
228 | }
229 | }
230 | }
231 |
232 | return null;
233 | };
234 | /**
235 | * Group the list of point that inside a specific radius
236 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
237 | * @param {MapBox} mapBox Required. The mapbox instance
238 | * @param {number} radius Optional. The radius of the cluster
239 | * @return {Array>} The list of grouped feature
240 | */
241 |
242 |
243 | exports.findPointsWithSameLocation = findPointsWithSameLocation;
244 |
245 | const groupNearestPointsByRadius = function (data, mapBox) {
246 | let radius = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 60;
247 |
248 | if (!data || !data.features || !_lodash.default.isArray(data.features)) {
249 | throw new Error("Data cannot be empty");
250 | }
251 |
252 | if (!mapBox) {
253 | throw new Error("Mapbox instance must be provided");
254 | }
255 |
256 | const zoom = mapBox.getMaxZoom() - 2;
257 | let {
258 | clusters,
259 | superC
260 | } = createClusters(data, mapBox, radius, zoom);
261 | clusters = clusters.map(cluster => {
262 | const {
263 | cluster: isCluster,
264 | cluster_id
265 | } = cluster.properties;
266 |
267 | if (isCluster) {
268 | try {
269 | return superC.getLeaves(cluster_id, Infinity);
270 | } catch (e) {
271 | return null;
272 | }
273 | }
274 |
275 | return [cluster];
276 | });
277 | return _lodash.default.filter(clusters);
278 | };
279 |
280 | exports.groupNearestPointsByRadius = groupNearestPointsByRadius;
--------------------------------------------------------------------------------
/dist/common/utils/event.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.getExactEventHandlerName = exports.extractEventHandlers = void 0;
9 |
10 | var _lodash = _interopRequireDefault(require("lodash"));
11 |
12 | const EVENT_PREFIX = /^on(.+)$/i;
13 |
14 | const extractEventHandlers = function (props) {
15 | let eventPrefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : EVENT_PREFIX;
16 | return _lodash.default.reduce(Object.keys(props), (res, prop) => {
17 | const cb = props[prop];
18 |
19 | if (eventPrefix.test(prop) && _lodash.default.isFunction(cb)) {
20 | const key = prop.replace(eventPrefix, (match, p) => `on${p}`);
21 | res[key] = cb;
22 | }
23 |
24 | return res;
25 | }, {});
26 | };
27 |
28 | exports.extractEventHandlers = extractEventHandlers;
29 |
30 | const getExactEventHandlerName = event => {
31 | if (!_lodash.default.isString(event)) {
32 | return event;
33 | }
34 |
35 | return event.replace("on", "").toLowerCase();
36 | };
37 |
38 | exports.getExactEventHandlerName = getExactEventHandlerName;
--------------------------------------------------------------------------------
/dist/common/utils/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | Object.defineProperty(exports, "calculateNextZoomLevel", {
7 | enumerable: true,
8 | get: function () {
9 | return _calc.calculateNextZoomLevel;
10 | }
11 | });
12 | Object.defineProperty(exports, "checkPropsChange", {
13 | enumerable: true,
14 | get: function () {
15 | return _props.checkPropsChange;
16 | }
17 | });
18 | Object.defineProperty(exports, "createClusters", {
19 | enumerable: true,
20 | get: function () {
21 | return _cluster.createClusters;
22 | }
23 | });
24 | Object.defineProperty(exports, "extractEventHandlers", {
25 | enumerable: true,
26 | get: function () {
27 | return _event.extractEventHandlers;
28 | }
29 | });
30 | Object.defineProperty(exports, "findPointsWithSameLocation", {
31 | enumerable: true,
32 | get: function () {
33 | return _cluster.findPointsWithSameLocation;
34 | }
35 | });
36 | Object.defineProperty(exports, "getExactEventHandlerName", {
37 | enumerable: true,
38 | get: function () {
39 | return _event.getExactEventHandlerName;
40 | }
41 | });
42 | Object.defineProperty(exports, "groupNearestPointsByRadius", {
43 | enumerable: true,
44 | get: function () {
45 | return _cluster.groupNearestPointsByRadius;
46 | }
47 | });
48 | Object.defineProperty(exports, "isReactComponent", {
49 | enumerable: true,
50 | get: function () {
51 | return _react.isReactComponent;
52 | }
53 | });
54 |
55 | var _calc = require("./calc");
56 |
57 | var _cluster = require("./cluster");
58 |
59 | var _event = require("./event");
60 |
61 | var _props = require("./props");
62 |
63 | var _react = require("./react");
--------------------------------------------------------------------------------
/dist/common/utils/props.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.checkPropsChange = void 0;
9 |
10 | var _lodash = _interopRequireDefault(require("lodash"));
11 |
12 | const checkPropsChange = function (props, nextProps, keys) {
13 | let equalityChecker = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _lodash.default.isEqual;
14 |
15 | const propsToCheck = _lodash.default.pick(props, keys);
16 |
17 | const nextPropsToCheck = _lodash.default.pick(nextProps, keys);
18 |
19 | if (_lodash.default.isFunction(equalityChecker)) {
20 | return equalityChecker(propsToCheck, nextPropsToCheck);
21 | }
22 |
23 | return propsToCheck === nextPropsToCheck;
24 | };
25 |
26 | exports.checkPropsChange = checkPropsChange;
--------------------------------------------------------------------------------
/dist/common/utils/react.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.isReactComponent = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _lodash = _interopRequireDefault(require("lodash"));
13 |
14 | const isFunctionComponent = component => component && _lodash.default.isFunction(component.type) && String(component.type).includes("createElement");
15 | /**
16 | * Check if a component is a custom class React component or native DOM elements (e.g. div, span)
17 | * @param {*} component
18 | * @return {bool} True if the input component is React component
19 | */
20 |
21 |
22 | const isReactComponent = component => {
23 | const isReactComponent = _lodash.default.get(component, "type.prototype.isReactComponent");
24 |
25 | const isPureReactComponent = _lodash.default.get(component, "type.prototype.isPureReactComponent");
26 |
27 | const isFunctionalComponent = isFunctionComponent(component);
28 | const isFragmentComponent = _lodash.default.toString(_lodash.default.get(component, "type")) === "Symbol(react.fragment)";
29 | const isReactMemoComponent = _lodash.default.toString(_lodash.default.get(component, "$$typeof")) === "Symbol(react.memo)";
30 | return isReactMemoComponent || /*#__PURE__*/_react.default.isValidElement(component) && (isReactComponent || isPureReactComponent || isFunctionalComponent || isFragmentComponent);
31 | };
32 |
33 | exports.isReactComponent = isReactComponent;
--------------------------------------------------------------------------------
/dist/components/MappedComponent.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.default = void 0;
7 |
8 | var _react = require("react");
9 |
10 | var _reactMapboxGl = require("react-mapbox-gl");
11 |
12 | class MappedComponent extends _react.Component {
13 | getMapInstance() {
14 | return this.context;
15 | }
16 |
17 | }
18 |
19 | MappedComponent.contextType = _reactMapboxGl.MapContext;
20 | var _default = MappedComponent;
21 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/components/MarkerLayer/Component.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _reactDom = _interopRequireDefault(require("react-dom"));
13 |
14 | var _propTypes = _interopRequireDefault(require("prop-types"));
15 |
16 | var _lodash = _interopRequireDefault(require("lodash"));
17 |
18 | var _mapboxGl = _interopRequireDefault(require("mapbox-gl"));
19 |
20 | var _utils = require("../../common/utils");
21 |
22 | var _MappedComponent = _interopRequireDefault(require("../MappedComponent"));
23 |
24 | class MarkerLayer extends _MappedComponent.default {
25 | constructor() {
26 | super(...arguments);
27 |
28 | this._disableMapDragPan = () => {
29 | const map = this.getMapInstance();
30 |
31 | if (map) {
32 | map.dragPan.disable();
33 | }
34 | };
35 |
36 | this._enableMapDragPan = () => {
37 | const map = this.getMapInstance();
38 |
39 | if (map) {
40 | map.dragPan.enable();
41 | }
42 | };
43 |
44 | this._generateEventHander = eventName => e => {
45 | const handler = this.props[eventName];
46 |
47 | if (_lodash.default.isFunction(handler)) {
48 | const {
49 | coordinates
50 | } = this.props;
51 | const properties = this.getProperties();
52 | handler(properties, coordinates, this.getOffset(), e);
53 | }
54 | };
55 | }
56 |
57 | componentDidMount() {
58 | const node = this.attachChildren(this.props);
59 | this.layer = new _mapboxGl.default.Marker(node).setLngLat(this.props.coordinates).addTo(this.getMapInstance());
60 | }
61 |
62 | componentDidUpdate(prevProps, prevState) {
63 | if (prevProps.coordinates !== this.props.coordinates) {
64 | this.layer.setLngLat(prevProps.coordinates);
65 | }
66 |
67 | if (prevProps.children !== this.props.children || (0, _utils.checkPropsChange)(this.props, prevProps, ["style", "className"])) {
68 | this.attachChildren(prevProps);
69 | }
70 | }
71 |
72 | componentWillUnmount() {
73 | if (!this.layer) {
74 | return;
75 | }
76 |
77 | this.layer.remove();
78 | delete this.layer;
79 | }
80 |
81 | attachChildren() {
82 | let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
83 | const {
84 | children
85 | } = props;
86 |
87 | if (children) {
88 | if (!this.element) {
89 | this.element = document.createElement("div");
90 | } else {
91 | this._unbindEvents();
92 | }
93 |
94 | const style = this.getStyle(this.props);
95 | this.element.className = this.getContainerClassName(props);
96 | Object.keys(style).forEach(s => {
97 | this.element.style[s] = style[s];
98 | });
99 |
100 | this._bindEvents();
101 |
102 | const content = this.getContent(props);
103 |
104 | _reactDom.default.render(content, this.element);
105 | }
106 |
107 | return this.element;
108 | }
109 |
110 | getContainerClassName(props) {
111 | return `mapboxgl-marker ${props.className}`;
112 | }
113 |
114 | getContent(props) {
115 | const {
116 | children
117 | } = props;
118 | return /*#__PURE__*/_react.default.createElement("div", {
119 | className: "nio-marker-content f-width f-height"
120 | }, children);
121 | }
122 |
123 | getProperties() {
124 | return this.props.properties;
125 | }
126 |
127 | getOffset() {
128 | return [0, 0];
129 | }
130 |
131 | getStyle(props) {
132 | return _lodash.default.clone(props.style) || {};
133 | }
134 |
135 | _bindEvents() {
136 | const events = (0, _utils.extractEventHandlers)(this.props);
137 | this.realHandlers = {};
138 |
139 | _lodash.default.forEach(events, (handler, name) => {
140 | const realHandler = this._generateEventHander(name);
141 |
142 | this.element.addEventListener((0, _utils.getExactEventHandlerName)(name), realHandler);
143 | this.realHandlers[name] = realHandler;
144 | });
145 |
146 | this.element.addEventListener("mousedown", this._disableMapDragPan);
147 | this.element.addEventListener("mouseup", this._enableMapDragPan);
148 | }
149 |
150 | _unbindEvents() {
151 | const events = (0, _utils.extractEventHandlers)(this.props);
152 | this.element.removeEventListener("mousedown", this._disableMapDragPan);
153 | this.element.removeEventListener("mouseup", this._enableMapDragPan);
154 |
155 | _lodash.default.forEach(events, (handler, name) => {
156 | const realHandler = this.realHandlers[name];
157 | this.element.removeEventListener((0, _utils.getExactEventHandlerName)(name), realHandler);
158 | });
159 |
160 | delete this.realHandlers;
161 | }
162 |
163 | render() {
164 | return null;
165 | }
166 |
167 | }
168 |
169 | MarkerLayer.displayName = "MarkerLayer";
170 | MarkerLayer.propTypes = {
171 | /**
172 | * (required): [number, number] Display the Marker at the given position
173 | */
174 | coordinates: _propTypes.default.array.isRequired,
175 |
176 | /**
177 | * Properties of each Marker, will be passed back when events trigged
178 | */
179 | properties: _propTypes.default.oneOfType([_propTypes.default.array.isRequired, _propTypes.default.object.isRequired, _propTypes.default.string.isRequired]),
180 |
181 | /**
182 | * Apply the className to the container of the Marker
183 | */
184 | className: _propTypes.default.string,
185 |
186 | /**
187 | * Apply style to the Marker container
188 | */
189 | style: _propTypes.default.object,
190 |
191 | /**
192 | * Child node(s) of the component, to be rendered as custom Marker
193 | */
194 | children: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.arrayOf(_propTypes.default.node)]),
195 |
196 | /**
197 | * [Optional] The click event handler
198 | */
199 | onClick: _propTypes.default.func,
200 |
201 | /**
202 | * [Optional] The mouse down event handler
203 | */
204 | onMouseDown: _propTypes.default.func,
205 |
206 | /**
207 | * [Optional] The mouse enter event handler
208 | */
209 | onMouseEnter: _propTypes.default.func,
210 |
211 | /**
212 | * [Optional] The mouse leave event handler
213 | */
214 | onMouseLeave: _propTypes.default.func,
215 |
216 | /**
217 | * [Optional] The mouse move event handler
218 | */
219 | onMouseMove: _propTypes.default.func,
220 |
221 | /**
222 | * [Optional] The mouse out event handler
223 | */
224 | onMouseOut: _propTypes.default.func,
225 |
226 | /**
227 | * [Optional] The mouse over event handler
228 | */
229 | onMouseOver: _propTypes.default.func,
230 |
231 | /**
232 | * [Optional] The mouse up event handler
233 | */
234 | onMouseUp: _propTypes.default.func
235 | };
236 | var _default = MarkerLayer;
237 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/components/MarkerLayer/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | Object.defineProperty(exports, "MarkerLayer", {
9 | enumerable: true,
10 | get: function () {
11 | return _Component.default;
12 | }
13 | });
14 |
15 | var _Component = _interopRequireDefault(require("./Component"));
--------------------------------------------------------------------------------
/dist/components/ReactMapboxGlCluster/ClusterLayer.css:
--------------------------------------------------------------------------------
1 | .cluster-layer-container {
2 | width: 30px;
3 | height: 30px;
4 | }
5 |
6 | .cluster-layer--cluster {
7 | width: 30px;
8 | height: 30px;
9 | border-radius: 50%;
10 | background-color: rgba(33, 150, 243, 0.8);
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | color: white;
15 | cursor: pointer;
16 | }
17 |
18 | .cluster-layer--cluster:before {
19 | content: " ";
20 | position: absolute;
21 | border-radius: 50%;
22 | width: 40px;
23 | height: 40px;
24 | background-color: rgba(33, 150, 243, 0.6);
25 | z-index: -1;
26 | }
27 |
28 | .cluster-layer--point {
29 | width: 20px;
30 | height: 20px;
31 | border-radius: 50%;
32 | background-color: rgba(195, 38, 94, 0.8);
33 | border: 1px solid #ffffff;
34 | }
35 |
36 | .cluster-layer--point:hover {
37 | width: 25px;
38 | height: 25px;
39 | border-width: 2px !important;
40 | }
41 |
42 | .marker-content.hovered .cluster-layer--cluster {
43 | width: 40px;
44 | height: 40px;
45 | }
46 |
47 | .marker-content.hovered .cluster-layer--cluster:before {
48 | width: 50px;
49 | height: 50px;
50 | }
51 |
52 | .marker-content.hovered .cluster-layer--point {
53 | width: 25px;
54 | height: 25px;
55 | border-color: #51dbd3 !important;
56 | border-width: 2px !important;
57 | }
58 |
--------------------------------------------------------------------------------
/dist/components/ReactMapboxGlCluster/ClusterLayer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
6 |
7 | Object.defineProperty(exports, "__esModule", {
8 | value: true
9 | });
10 | exports.default = void 0;
11 |
12 | var _react = _interopRequireWildcard(require("react"));
13 |
14 | var _classnames = _interopRequireDefault(require("classnames"));
15 |
16 | var _invariant = require("@turf/invariant");
17 |
18 | var _reactMapboxGl = require("react-mapbox-gl");
19 |
20 | var _utils = require("../../common/utils");
21 |
22 | var _MarkerLayer = require("../MarkerLayer");
23 |
24 | require("./ClusterLayer.css");
25 |
26 | class ClusterLayer extends _react.PureComponent {
27 | constructor() {
28 | super(...arguments);
29 |
30 | this._clusterMarkerFactory = (coordinates, pointCount, getLeaves) => {
31 | const {
32 | clusterClassName
33 | } = this.props;
34 | const className = (0, _classnames.default)("cluster-layer--cluster", clusterClassName);
35 | const points = getLeaves();
36 |
37 | const pointsProps = this._getPointsProps(points);
38 |
39 | const clusterEventHandlers = (0, _utils.extractEventHandlers)(this.props, /^onCluster(.+)$/i);
40 | return /*#__PURE__*/_react.default.createElement(_MarkerLayer.MarkerLayer, Object.assign({
41 | key: coordinates.toString(),
42 | coordinates: coordinates,
43 | className: "cluster-layer-container",
44 | properties: pointsProps
45 | }, clusterEventHandlers), /*#__PURE__*/_react.default.createElement("div", {
46 | className: className
47 | }, /*#__PURE__*/_react.default.createElement("div", null, pointCount)));
48 | };
49 | }
50 |
51 | _getClusterProps() {
52 | const {
53 | radius,
54 | minZoom,
55 | maxZoom,
56 | extent,
57 | nodeSize
58 | } = this.props;
59 | return {
60 | radius,
61 | minZoom,
62 | maxZoom,
63 | extent,
64 | nodeSize
65 | };
66 | }
67 |
68 | _getPointsProps(points) {
69 | return points.map(point => {
70 | const feature = point.props["data-feature"];
71 | const {
72 | properties
73 | } = feature;
74 | return { ...properties,
75 | coordinates: (0, _invariant.getCoord)(feature)
76 | };
77 | });
78 | }
79 |
80 | _renderMarkers() {
81 | const {
82 | data,
83 | pointClassName,
84 | pointStyles = {},
85 | markerComponent: MarkerComponent
86 | } = this.props;
87 | const markerClassName = (0, _classnames.default)("cluster-layer--point", pointClassName);
88 | return data.features.map((feature, key) => {
89 | const {
90 | geometry: {
91 | coordinates
92 | },
93 | properties
94 | } = feature;
95 | const {
96 | style
97 | } = properties;
98 | const eventHandlers = (0, _utils.extractEventHandlers)(this.props);
99 | const cssObject = { ...pointStyles,
100 | ...style
101 | };
102 | return /*#__PURE__*/_react.default.createElement(_MarkerLayer.MarkerLayer, Object.assign({
103 | key: `cluster-layer-point${key}`,
104 | coordinates: coordinates,
105 | "data-feature": feature,
106 | properties: properties
107 | }, eventHandlers), MarkerComponent ? /*#__PURE__*/_react.default.createElement(MarkerComponent, {
108 | properties: properties,
109 | className: markerClassName,
110 | style: cssObject
111 | }) : /*#__PURE__*/_react.default.createElement("div", {
112 | className: markerClassName,
113 | style: cssObject
114 | }));
115 | });
116 | }
117 |
118 | render() {
119 | const clusterProps = this._getClusterProps();
120 |
121 | return /*#__PURE__*/_react.default.createElement(_reactMapboxGl.Cluster, Object.assign({
122 | ClusterMarkerFactory: this._clusterMarkerFactory
123 | }, clusterProps), this._renderMarkers());
124 | }
125 |
126 | }
127 |
128 | ClusterLayer.displayName = "ClusterLayer";
129 | ClusterLayer.defaultProps = {
130 | radius: 60,
131 | minZoom: 0,
132 | maxZoom: 20,
133 | extent: 512,
134 | nodeSize: 64
135 | };
136 | var _default = ClusterLayer;
137 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/components/ReactMapboxGlCluster/MapboxGlCluster.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _hoc = require("../../common/hoc");
11 |
12 | var _ClusterLayer = _interopRequireDefault(require("./ClusterLayer"));
13 |
14 | const ClusterLayerWithOverlappedPoints = (0, _hoc.detectLocationHasOverlappedPoints)(_ClusterLayer.default);
15 | const ZoomableClusterLayer = (0, _hoc.doZoomingOnClick)(ClusterLayerWithOverlappedPoints);
16 | const MapboxGlCluster = (0, _hoc.connectWithSpiderifierPoint)(ZoomableClusterLayer);
17 | var _default = MapboxGlCluster;
18 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/components/ReactMapboxGlCluster/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports.default = void 0;
9 |
10 | var _MapboxGlCluster = _interopRequireDefault(require("./MapboxGlCluster"));
11 |
12 | var _default = _MapboxGlCluster.default;
13 | exports.default = _default;
--------------------------------------------------------------------------------
/dist/components/ReactMapboxGlCluster/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "cluster-layer",
4 | "main": "./index.js"
5 | }
6 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | Object.defineProperty(exports, "ReactMapboxGlCluster", {
9 | enumerable: true,
10 | get: function () {
11 | return _ReactMapboxGlCluster.default;
12 | }
13 | });
14 |
15 | var _ReactMapboxGlCluster = _interopRequireDefault(require("./components/ReactMapboxGlCluster"));
--------------------------------------------------------------------------------
/dist/types/common/constants/ClusterOptions.d.ts:
--------------------------------------------------------------------------------
1 | export namespace ClusterOptions {
2 | let NearestPointsRadius: number;
3 | let ZoomLevel: number;
4 | }
5 |
--------------------------------------------------------------------------------
/dist/types/common/constants/GeoJSONTypes.d.ts:
--------------------------------------------------------------------------------
1 | export namespace GeoJSONTypes {
2 | let Point: string;
3 | let MultiPoint: string;
4 | let LineString: string;
5 | let MultiLineString: string;
6 | let Polygon: string;
7 | let MultiPolygon: string;
8 | let GeometryCollection: string;
9 | let FeatureCollection: string;
10 | }
11 | export const NormalTypes: string[];
12 | export const CollectionTypes: string[];
13 | export const ListKeysByType: {
14 | [x: string]: string;
15 | };
16 |
--------------------------------------------------------------------------------
/dist/types/common/hoc/connectWithSpiderifierPoint.d.ts:
--------------------------------------------------------------------------------
1 | export default connectWithSpiderifierPoint;
2 | /**
3 | * @type Class
4 | */
5 | declare const connectWithSpiderifierPoint: Class;
6 |
--------------------------------------------------------------------------------
/dist/types/common/hoc/detectLocationHasOverlappedPoints.d.ts:
--------------------------------------------------------------------------------
1 | export default detectLocationHasOverlappedPoints;
2 | /**
3 | * @type Class
4 | */
5 | declare const detectLocationHasOverlappedPoints: Class;
6 |
--------------------------------------------------------------------------------
/dist/types/common/hoc/doZoomingOnClick.d.ts:
--------------------------------------------------------------------------------
1 | export default doZoomingOnClick;
2 | /**
3 | * @type Class
4 | */
5 | declare const doZoomingOnClick: Class;
6 |
--------------------------------------------------------------------------------
/dist/types/common/hoc/index.d.ts:
--------------------------------------------------------------------------------
1 | import connectWithSpiderifierPoint from "./connectWithSpiderifierPoint";
2 | import detectLocationHasOverlappedPoints from "./detectLocationHasOverlappedPoints";
3 | import doZoomingOnClick from "./doZoomingOnClick";
4 | export { connectWithSpiderifierPoint, detectLocationHasOverlappedPoints, doZoomingOnClick };
5 |
--------------------------------------------------------------------------------
/dist/types/common/utils/calc.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculate the next zoom level base on the current zoom
3 | * @param {number} currentZoom the current zoom level
4 | * @param {number} maxZoom the max zoom level of the map
5 | * @param {number} extraZoomLevels how many extra level more for each zoom
6 | */
7 | export function calculateNextZoomLevel(currentZoom: number, maxZoom?: number, extraZoomLevels?: number): number;
8 |
--------------------------------------------------------------------------------
/dist/types/common/utils/cluster.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Find the list of point that inside a specific radius
3 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
4 | * @param {MapBox} mapBox Required. The mapbox instance
5 | * @param {number} zoom The zoom level, at which the points is clustered
6 | * @return {Array} The list of feature
7 | */
8 | export function createClusters(data: FeatureCollection, mapBox: MapBox, radius: number | undefined, zoom: number): Array;
9 | /**
10 | * Find the list of point that have a similar location (lngLat)
11 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
12 | * @param {Coordinate} lngLat Required. The coordinate follow format [longitude, latitude]
13 | * @param {MapBox} mapBox Required. The mapbox instance
14 | * @param {number} radius The radius of the cluster
15 | * @param {number} zoom The zoom level, at which the points is clustered
16 | * @return {Array} The list of point at the same location. Null if cannot find the
17 | * similar points
18 | */
19 | export function findPointsWithSameLocation(data: FeatureCollection, lngLat: Coordinate, mapBox: MapBox, radius: number | undefined, zoom: number): Array;
20 | /**
21 | * Group the list of point that inside a specific radius
22 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
23 | * @param {MapBox} mapBox Required. The mapbox instance
24 | * @param {number} radius Optional. The radius of the cluster
25 | * @return {Array>} The list of grouped feature
26 | */
27 | export function groupNearestPointsByRadius(data: FeatureCollection, mapBox: MapBox, radius?: number): Array>;
28 |
--------------------------------------------------------------------------------
/dist/types/common/utils/event.d.ts:
--------------------------------------------------------------------------------
1 | export function extractEventHandlers(props: any, eventPrefix?: RegExp): any;
2 | export function getExactEventHandlerName(event: any): any;
3 |
--------------------------------------------------------------------------------
/dist/types/common/utils/index.d.ts:
--------------------------------------------------------------------------------
1 | import { calculateNextZoomLevel } from "./calc";
2 | import { createClusters } from "./cluster";
3 | import { checkPropsChange } from "./props";
4 | import { extractEventHandlers } from "./event";
5 | import { findPointsWithSameLocation } from "./cluster";
6 | import { getExactEventHandlerName } from "./event";
7 | import { groupNearestPointsByRadius } from "./cluster";
8 | import { isReactComponent } from "./react";
9 | export { calculateNextZoomLevel, createClusters, checkPropsChange, extractEventHandlers, findPointsWithSameLocation, getExactEventHandlerName, groupNearestPointsByRadius, isReactComponent };
10 |
--------------------------------------------------------------------------------
/dist/types/common/utils/props.d.ts:
--------------------------------------------------------------------------------
1 | export function checkPropsChange(props: any, nextProps: any, keys: any, equalityChecker?: any): any;
2 |
--------------------------------------------------------------------------------
/dist/types/common/utils/react.d.ts:
--------------------------------------------------------------------------------
1 | export function isReactComponent(component: any): bool;
2 |
--------------------------------------------------------------------------------
/dist/types/components/MappedComponent.d.ts:
--------------------------------------------------------------------------------
1 | export default MappedComponent;
2 | declare class MappedComponent {
3 | static contextType: React.Context;
4 | getMapInstance(): any;
5 | }
6 |
--------------------------------------------------------------------------------
/dist/types/components/MarkerLayer/Component.d.ts:
--------------------------------------------------------------------------------
1 | export default MarkerLayer;
2 | declare class MarkerLayer extends MappedComponent {
3 | componentDidMount(): void;
4 | layer: import("mapbox-gl").Marker | undefined;
5 | componentDidUpdate(prevProps: any, prevState: any): void;
6 | componentWillUnmount(): void;
7 | attachChildren(props?: any): HTMLDivElement | undefined;
8 | element: HTMLDivElement | undefined;
9 | getContainerClassName(props: any): string;
10 | getContent(props: any): any;
11 | getProperties(): any;
12 | getOffset(): number[];
13 | getStyle(props: any): any;
14 | _bindEvents(): void;
15 | realHandlers: {} | undefined;
16 | _disableMapDragPan: () => void;
17 | _enableMapDragPan: () => void;
18 | _generateEventHander: (eventName: any) => (e: any) => void;
19 | _unbindEvents(): void;
20 | render(): null;
21 | }
22 | declare namespace MarkerLayer {
23 | let displayName: string;
24 | namespace propTypes {
25 | let coordinates: any;
26 | let properties: any;
27 | let className: any;
28 | let style: any;
29 | let children: any;
30 | let onClick: any;
31 | let onMouseDown: any;
32 | let onMouseEnter: any;
33 | let onMouseLeave: any;
34 | let onMouseMove: any;
35 | let onMouseOut: any;
36 | let onMouseOver: any;
37 | let onMouseUp: any;
38 | }
39 | }
40 | import MappedComponent from "../MappedComponent";
41 |
--------------------------------------------------------------------------------
/dist/types/components/MarkerLayer/index.d.ts:
--------------------------------------------------------------------------------
1 | export { MarkerLayer };
2 | import MarkerLayer from "./Component";
3 |
--------------------------------------------------------------------------------
/dist/types/components/ReactMapboxGlCluster/ClusterLayer.d.ts:
--------------------------------------------------------------------------------
1 | export default ClusterLayer;
2 | declare class ClusterLayer {
3 | _clusterMarkerFactory: (coordinates: any, pointCount: any, getLeaves: any) => any;
4 | _getClusterProps(): {
5 | radius: any;
6 | minZoom: any;
7 | maxZoom: any;
8 | extent: any;
9 | nodeSize: any;
10 | };
11 | _getPointsProps(points: any): any;
12 | _renderMarkers(): any;
13 | render(): any;
14 | }
15 | declare namespace ClusterLayer {
16 | let displayName: string;
17 | namespace propTypes {
18 | let data: any;
19 | let radius: any;
20 | let minZoom: any;
21 | let maxZoom: any;
22 | let extent: any;
23 | let nodeSize: any;
24 | let pointClassName: any;
25 | let pointStyles: any;
26 | let clusterClassName: any;
27 | let markerComponent: any;
28 | let onMouseLeave: any;
29 | let onClick: any;
30 | let onClusterClick: any;
31 | let onClusterMouseEnter: any;
32 | let onClusterMouseLeave: any;
33 | }
34 | namespace defaultProps {
35 | let radius_1: number;
36 | export { radius_1 as radius };
37 | let minZoom_1: number;
38 | export { minZoom_1 as minZoom };
39 | let maxZoom_1: number;
40 | export { maxZoom_1 as maxZoom };
41 | let extent_1: number;
42 | export { extent_1 as extent };
43 | let nodeSize_1: number;
44 | export { nodeSize_1 as nodeSize };
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/dist/types/components/ReactMapboxGlCluster/MapboxGlCluster.d.ts:
--------------------------------------------------------------------------------
1 | export default MapboxGlCluster;
2 | declare const MapboxGlCluster: any;
3 |
--------------------------------------------------------------------------------
/dist/types/components/ReactMapboxGlCluster/index.d.ts:
--------------------------------------------------------------------------------
1 | export default MapboxGlCluster;
2 | import MapboxGlCluster from "./MapboxGlCluster";
3 |
--------------------------------------------------------------------------------
/dist/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export { ReactMapboxGlCluster };
2 | import ReactMapboxGlCluster from "./components/ReactMapboxGlCluster";
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-mapbox-gl-cluster",
3 | "version": "1.20.0",
4 | "main": "dist/index.js",
5 | "types": "dist/types/index.d.ts",
6 | "module": "dist/index.js",
7 | "files": [
8 | "dist",
9 | "README.md"
10 | ],
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/thuanmb/react-mapbox-gl-cluster"
14 | },
15 | "dependencies": {
16 | "@babel/runtime": "^7.25.0",
17 | "@turf/invariant": "^7.1.0",
18 | "classnames": "^2.5.1",
19 | "lodash": "^4.17.21",
20 | "mapbox-gl": "^3.6.0",
21 | "prop-types": "^15.7.2",
22 | "react-mapbox-gl": "^5.1.1",
23 | "react-mapbox-gl-spiderifier": "^1.12.0",
24 | "supercluster": "^8.0.1"
25 | },
26 | "devDependencies": {
27 | "@babel/cli": "^7.24.8",
28 | "@tsconfig/create-react-app": "^2.0.5",
29 | "babel-preset-react-app": "^10.0.0",
30 | "postcss-normalize": "^10.0.1",
31 | "react": "^18.3.1",
32 | "react-dom": "^18.3.1",
33 | "react-scripts": "^5.0.1",
34 | "typescript": "^5.5.4"
35 | },
36 | "scripts": {
37 | "start": "react-scripts start",
38 | "build": "rm -rf dist && NODE_ENV=production babel src/lib --out-dir dist --copy-files --ignore __tests__,spec.js,test.js,__snapshots__ --presets=./babelPresets.js; npx tsc -p tsconfig.json",
39 | "test": "react-scripts test",
40 | "eject": "react-scripts eject"
41 | },
42 | "eslintConfig": {
43 | "extends": "react-app"
44 | },
45 | "browserslist": [
46 | "defaults",
47 | "not ie 11"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thuanmb/react-mapbox-gl-cluster/c7ba16f3b3726aaf96e6583af57ff6f69a825dd2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
16 |
17 |
26 | React App
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | height: 100vh;
3 | text-align: center;
4 | }
5 |
6 | .mapboxgl-map {
7 | height: 100%;
8 | }
9 |
10 | .mapboxgl-popup {
11 | z-index: 999;
12 | }
13 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactMapboxGl from "react-mapbox-gl";
3 | import { ReactMapboxGlCluster } from "./lib";
4 | import { data } from "./data";
5 | import "./App.css";
6 |
7 | const Map = ReactMapboxGl({
8 | accessToken: process.env.REACT_APP_MAPBOX_GL_TOKEN,
9 | });
10 |
11 | const mapProps = {
12 | center: [-95.7129, 37.0902],
13 | zoom: [3],
14 | style: "mapbox://styles/mapbox/streets-v8",
15 | };
16 |
17 | const CustomSpiralComponent = ({ properties, ...restProps }) => {
18 | const onClick = (e) => {
19 | console.log(`Receive event onClick in spiral at properties: ${JSON.stringify(properties)}`);
20 | };
21 | return ;
22 | };
23 |
24 | const CustomeMarkerComponent = ({ properties, className, cssObject }) => {
25 | const onClick = (e) => {
26 | console.log(`Receive event onClick in marker at properties: ${JSON.stringify(properties)}`);
27 | };
28 | return ;
29 | };
30 |
31 | class App extends Component {
32 | getEventHandlers() {
33 | return {
34 | onClick: (properties, coords, offset) =>
35 | console.log(`Receive event onClick at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
36 | onMouseEnter: (properties, coords, offset) =>
37 | console.log(`Receive event onMouseEnter at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
38 | onMouseLeave: (properties, coords, offset) =>
39 | console.log(`Receive event onMouseLeave at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
40 | onClusterClick: (properties, coords, offset) =>
41 | console.log(`Receive event onClusterClick at properties: ${properties}, coords: ${coords}, offset: ${offset}`),
42 | onClusterMouseEnter: (properties, coords, offset) =>
43 | console.log(
44 | `Receive event onClusterMouseEnter at properties: ${properties}, coords: ${coords}, offset: ${offset}`
45 | ),
46 | onClusterMouseLeave: (properties, coords, offset) =>
47 | console.log(
48 | `Receive event onClusterMouseLeave at properties: ${properties}, coords: ${coords}, offset: ${offset}`
49 | ),
50 | };
51 | }
52 |
53 | render() {
54 | return (
55 |
56 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default App;
71 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | it("renders without crashing", () => {
6 | const div = document.createElement("div");
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/data.js:
--------------------------------------------------------------------------------
1 | export const data = {
2 | type: "FeatureCollection",
3 | features: [
4 | {
5 | type: "Feature",
6 | properties: {},
7 | geometry: {
8 | type: "Point",
9 | coordinates: [-120.05859375, 45.644768217751924]
10 | }
11 | },
12 | {
13 | type: "Feature",
14 | properties: {},
15 | geometry: {
16 | type: "Point",
17 | coordinates: [-117.0703125, 46.01222384063236]
18 | }
19 | },
20 | {
21 | type: "Feature",
22 | properties: {},
23 | geometry: {
24 | type: "Point",
25 | coordinates: [-110.12695312499999, 45.767522962149876]
26 | }
27 | },
28 | {
29 | type: "Feature",
30 | properties: {},
31 | geometry: {
32 | type: "Point",
33 | coordinates: [-101.6015625, 44.96479793033101]
34 | }
35 | },
36 | {
37 | type: "Feature",
38 | properties: {},
39 | geometry: {
40 | type: "Point",
41 | coordinates: [-92.63671875, 40.97989806962013]
42 | }
43 | },
44 | {
45 | type: "Feature",
46 | properties: {},
47 | geometry: {
48 | type: "Point",
49 | coordinates: [-90.615234375, 37.3002752813443]
50 | }
51 | },
52 | {
53 | type: "Feature",
54 | properties: {},
55 | geometry: {
56 | type: "Point",
57 | coordinates: [-116.89453125, 40.245991504199026]
58 | }
59 | },
60 | {
61 | type: "Feature",
62 | properties: {},
63 | geometry: {
64 | type: "Point",
65 | coordinates: [-108.28125, 34.74161249883172]
66 | }
67 | },
68 | {
69 | type: "Feature",
70 | properties: {},
71 | geometry: {
72 | type: "Point",
73 | coordinates: [-92.373046875, 33.063924198120645]
74 | }
75 | },
76 | {
77 | type: "Feature",
78 | properties: {},
79 | geometry: {
80 | type: "Point",
81 | coordinates: [-83.583984375, 39.36827914916014]
82 | }
83 | },
84 | {
85 | type: "Feature",
86 | properties: {},
87 | geometry: {
88 | type: "Point",
89 | coordinates: [-102.3046875, 38.13455657705411]
90 | }
91 | },
92 | {
93 | type: "Feature",
94 | properties: {},
95 | geometry: {
96 | type: "Point",
97 | coordinates: [-100.986328125, 32.175612478499325]
98 | }
99 | },
100 | {
101 | type: "Feature",
102 | properties: {},
103 | geometry: {
104 | type: "Point",
105 | coordinates: [-82.880859375, 34.66935854524543]
106 | }
107 | },
108 | {
109 | type: "Feature",
110 | properties: {},
111 | geometry: {
112 | type: "Point",
113 | coordinates: [-98.61328125, 42.94033923363181]
114 | }
115 | },
116 | {
117 | type: "Feature",
118 | properties: {},
119 | geometry: {
120 | type: "Point",
121 | coordinates: [-107.490234375, 43.32517767999296]
122 | }
123 | },
124 | {
125 | type: "Feature",
126 | properties: {},
127 | geometry: {
128 | type: "Point",
129 | coordinates: [-92.43896484375, 40.66397287638688]
130 | }
131 | },
132 | {
133 | type: "Feature",
134 | properties: {},
135 | geometry: {
136 | type: "Point",
137 | coordinates: [-93.88916015625, 40.329795743702064]
138 | }
139 | },
140 | {
141 | type: "Feature",
142 | properties: {},
143 | geometry: {
144 | type: "Point",
145 | coordinates: [-97.0751953125, 40.43022363450862]
146 | }
147 | },
148 | {
149 | type: "Feature",
150 | properties: {},
151 | geometry: {
152 | type: "Point",
153 | coordinates: [-92.04345703125, 39.926588421909436]
154 | }
155 | },
156 | {
157 | type: "Feature",
158 | properties: {},
159 | geometry: {
160 | type: "Point",
161 | coordinates: [-108.017578125, 42.032974332441405]
162 | }
163 | },
164 | {
165 | type: "Feature",
166 | properties: {},
167 | geometry: {
168 | type: "Point",
169 | coordinates: [-88.59374999999999, 42.8115217450979]
170 | }
171 | },
172 | {
173 | type: "Feature",
174 | properties: {},
175 | geometry: {
176 | type: "Point",
177 | coordinates: [-85.78125, 39.095962936305476]
178 | }
179 | },
180 | {
181 | type: "Feature",
182 | properties: {},
183 | geometry: {
184 | type: "Point",
185 | coordinates: [-114.521484375, 36.03133177633187]
186 | }
187 | },
188 | {
189 | type: "Feature",
190 | properties: {},
191 | geometry: {
192 | type: "Point",
193 | coordinates: [-111.796875, 40.91351257612758]
194 | }
195 | },
196 | {
197 | type: "Feature",
198 | properties: {},
199 | geometry: {
200 | type: "Point",
201 | coordinates: [-105.46875, 47.81315451752768]
202 | }
203 | },
204 | {
205 | type: "Feature",
206 | properties: {},
207 | geometry: {
208 | type: "Point",
209 | coordinates: [-94.130859375, 45.706179285330855]
210 | }
211 | },
212 | {
213 | type: "Feature",
214 | properties: {},
215 | geometry: {
216 | type: "Point",
217 | coordinates: [-120.84960937499999, 38.41055825094609]
218 | }
219 | },
220 | {
221 | type: "Feature",
222 | properties: {},
223 | geometry: {
224 | type: "Point",
225 | coordinates: [-120.76171875, 42.94033923363181]
226 | }
227 | },
228 | {
229 | type: "Feature",
230 | properties: {},
231 | geometry: {
232 | type: "Point",
233 | coordinates: [-114.08203125, 44.902577996288876]
234 | }
235 | },
236 | {
237 | type: "Feature",
238 | properties: {},
239 | geometry: {
240 | type: "Point",
241 | coordinates: [-100.986328125, 32.24997445586331]
242 | }
243 | },
244 | {
245 | type: "Feature",
246 | properties: {},
247 | geometry: {
248 | type: "Point",
249 | coordinates: [-98.45947265625, 35.44277092585766]
250 | }
251 | },
252 | {
253 | type: "Feature",
254 | properties: {},
255 | geometry: {
256 | type: "Point",
257 | coordinates: [-96.43798828125, 37.020098201368114]
258 | }
259 | },
260 | {
261 | type: "Feature",
262 | properties: {},
263 | geometry: {
264 | type: "Point",
265 | coordinates: [-92.52685546875, 36.98500309285596]
266 | }
267 | },
268 | {
269 | type: "Feature",
270 | properties: {},
271 | geometry: {
272 | type: "Point",
273 | coordinates: [-75.4541015625, 41.65649719441145]
274 | }
275 | },
276 | {
277 | type: "Feature",
278 | properties: {},
279 | geometry: {
280 | type: "Point",
281 | coordinates: [-76.57470703125, 41.261291493919884]
282 | }
283 | },
284 | {
285 | type: "Feature",
286 | properties: {},
287 | geometry: {
288 | type: "Point",
289 | coordinates: [-76.70654296875, 40.84706035607122]
290 | }
291 | },
292 | {
293 | type: "Feature",
294 | properties: {},
295 | geometry: {
296 | type: "Point",
297 | coordinates: [-76.2890625, 40.463666324587685]
298 | }
299 | },
300 | {
301 | type: "Feature",
302 | properties: {},
303 | geometry: {
304 | type: "Point",
305 | coordinates: [-74.37744140625, 41.29431726315258]
306 | }
307 | },
308 | {
309 | type: "Feature",
310 | properties: {},
311 | geometry: {
312 | type: "Point",
313 | coordinates: [-74.68505859374999, 40.697299008636755]
314 | }
315 | },
316 | {
317 | type: "Feature",
318 | properties: {},
319 | geometry: {
320 | type: "Point",
321 | coordinates: [-78.20068359374999, 40.3130432088809]
322 | }
323 | },
324 | {
325 | type: "Feature",
326 | properties: {},
327 | geometry: {
328 | type: "Point",
329 | coordinates: [-77.47558593749999, 39.52099229357195]
330 | }
331 | },
332 | {
333 | type: "Feature",
334 | properties: {},
335 | geometry: {
336 | type: "Point",
337 | coordinates: [-78.06884765624999, 41.83682786072714]
338 | }
339 | },
340 | {
341 | type: "Feature",
342 | properties: {},
343 | geometry: {
344 | type: "Point",
345 | coordinates: [-74.11376953125, 42.76314586689492]
346 | }
347 | },
348 | {
349 | type: "Feature",
350 | properties: {},
351 | geometry: {
352 | type: "Point",
353 | coordinates: [-75.41015624999999, 42.68243539838623]
354 | }
355 | },
356 | {
357 | type: "Feature",
358 | properties: {},
359 | geometry: {
360 | type: "Point",
361 | coordinates: [-85.8251953125, 36.61552763134925]
362 | }
363 | },
364 | {
365 | type: "Feature",
366 | properties: {},
367 | geometry: {
368 | type: "Point",
369 | coordinates: [-75.03662109375, 39.470125122358176]
370 | }
371 | },
372 | {
373 | type: "Feature",
374 | properties: {},
375 | geometry: {
376 | type: "Point",
377 | coordinates: [-73.93798828125, 40.713955826286046]
378 | }
379 | },
380 | {
381 | type: "Feature",
382 | properties: {},
383 | geometry: {
384 | type: "Point",
385 | coordinates: [-72.7734375, 43.5326204268101]
386 | }
387 | },
388 | {
389 | type: "Feature",
390 | properties: {},
391 | geometry: {
392 | type: "Point",
393 | coordinates: [-81.5185546875, 43.068887774169625]
394 | }
395 | },
396 | {
397 | type: "Feature",
398 | properties: {},
399 | geometry: {
400 | type: "Point",
401 | coordinates: [-74.1357421875, 40.56389453066509]
402 | }
403 | },
404 | {
405 | type: "Feature",
406 | properties: {},
407 | geometry: {
408 | type: "Point",
409 | coordinates: [-74.1357421875, 40.56389453066509]
410 | }
411 | },
412 | {
413 | type: "Feature",
414 | properties: {},
415 | geometry: {
416 | type: "Point",
417 | coordinates: [-74.1357421875, 40.56389453066509]
418 | }
419 | },
420 | {
421 | type: "Feature",
422 | properties: {},
423 | geometry: {
424 | type: "Point",
425 | coordinates: [-74.1357421875, 40.56389453066509]
426 | }
427 | },
428 | {
429 | type: "Feature",
430 | properties: {},
431 | geometry: {
432 | type: "Point",
433 | coordinates: [-74.1357421875, 40.56389453066509]
434 | }
435 | }
436 | ]
437 | };
438 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 | import App from "./App";
4 | import * as ReactDOMClient from 'react-dom/client';
5 | import * as serviceWorker from "./serviceWorker";
6 |
7 | const container = document.getElementById("root");
8 | const root = ReactDOMClient.createRoot(container);
9 | root.render();
10 |
11 | // If you want your app to work offline and load faster, you can change
12 | // unregister() to register() below. Note this comes with some pitfalls.
13 | // Learn more about service workers: https://bit.ly/CRA-PWA
14 | serviceWorker.unregister();
15 |
--------------------------------------------------------------------------------
/src/lib/common/constants/ClusterOptions.js:
--------------------------------------------------------------------------------
1 | const ClusterOptions = {
2 | NearestPointsRadius: 7,
3 | ZoomLevel: 17
4 | };
5 |
6 | export { ClusterOptions };
7 |
--------------------------------------------------------------------------------
/src/lib/common/constants/GeoJSONTypes.js:
--------------------------------------------------------------------------------
1 | const GeoJSONTypes = {
2 | Point: "Point",
3 | MultiPoint: "MultiPoint",
4 | LineString: "LineString",
5 | MultiLineString: "MultiLineString",
6 | Polygon: "Polygon",
7 | MultiPolygon: "MultiPolygon",
8 | GeometryCollection: "GeometryCollection",
9 | FeatureCollection: "FeatureCollection"
10 | };
11 |
12 | const NormalTypes = [
13 | GeoJSONTypes.Point,
14 | GeoJSONTypes.MultiPoint,
15 | GeoJSONTypes.LineString,
16 | GeoJSONTypes.MultiLineString,
17 | GeoJSONTypes.Polygon,
18 | GeoJSONTypes.MultiPolygon
19 | ];
20 |
21 | const CollectionTypes = [GeoJSONTypes.GeometryCollection, GeoJSONTypes.FeatureCollection];
22 |
23 | const ListKeysByType = {
24 | [GeoJSONTypes.GeometryCollection]: "geometries",
25 | [GeoJSONTypes.FeatureCollection]: "features"
26 | };
27 |
28 | export { GeoJSONTypes, NormalTypes, CollectionTypes, ListKeysByType };
29 |
--------------------------------------------------------------------------------
/src/lib/common/hoc/connectWithSpiderifierPoint.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import _ from "lodash";
4 | import { checkPropsChange } from "../utils";
5 | import { ReactMapboxGlSpiderifier } from "react-mapbox-gl-spiderifier";
6 | import { getCoord } from "@turf/invariant";
7 | import { findPointsWithSameLocation, groupNearestPointsByRadius } from "../utils";
8 | import { ClusterOptions } from "../constants/ClusterOptions";
9 | import MappedComponent from "../../components/MappedComponent";
10 | import "./spiderifier.css";
11 |
12 | const SPIDERIFIER_PROPS = [
13 | "coordinates",
14 | "circleSpiralSwitchover",
15 | "circleFootSeparation",
16 | "spiralFootSeparation",
17 | "spiralLengthStart",
18 | "spiralLengthFactor",
19 | "animate",
20 | "animationSpeed",
21 | "transformSpiderLeft",
22 | "transformSpiderTop",
23 | "showingLegs",
24 | "onClick",
25 | "onMouseDown",
26 | "onMouseEnter",
27 | "onMouseLeave",
28 | "onMouseMove",
29 | "onMouseOut",
30 | "onMouseOver",
31 | "onMouseUp",
32 | ];
33 | const MARKER_PROPS = [
34 | "data",
35 | "radius",
36 | "minZoom",
37 | "maxZoom",
38 | "extent",
39 | "nodeSize",
40 | "pointClassName",
41 | "pointStyles",
42 | "clusterClassName",
43 | "clusterClassName",
44 | "markerComponent",
45 | "onMouseLeave",
46 | "onClick",
47 | "onClusterClick",
48 | "onClusterMouseEnter",
49 | "onClusterMouseLeave",
50 | "clusterClickEnabled",
51 | ];
52 |
53 | /**
54 | * @type Class
55 | */
56 | const connectWithSpiderifierPoint = (WrappedComponent) => {
57 | class ConnectedWithSpiderifierComponent extends MappedComponent {
58 | constructor(props) {
59 | super(props);
60 | this.state = {
61 | overlappedPointsGroup: null,
62 | };
63 | this.registeredEvents = false;
64 | }
65 |
66 | componentDidUpdate(prevProps) {
67 | this._checkAndUpdatePoints(prevProps);
68 | this.bindEvents();
69 | }
70 |
71 | shouldComponentUpdate(nextProps, nextState) {
72 | return (
73 | checkPropsChange(
74 | this.props,
75 | nextProps,
76 | [
77 | "data",
78 | "showInitialSpiderifier",
79 | "onlySpiderifier",
80 | "circleFootSeparation",
81 | "transformSpiderLeft",
82 | "showingLegs",
83 | ],
84 | _.isEqual
85 | ) || !_.isEqual(this.state, nextState)
86 | );
87 | }
88 |
89 | componentWillUnmount() {
90 | this.unbindEvents();
91 | }
92 |
93 | bindEvents() {
94 | const map = this.getMapInstance();
95 | if (map && !this.registeredEvents) {
96 | map.on("zoomend", this.onMapChange);
97 | this.registeredEvents = true;
98 | }
99 | }
100 | unbindEvents() {
101 | const map = this.getMapInstance();
102 | if (map) {
103 | map.off("zoomend", this.onMapChange);
104 | }
105 | }
106 |
107 | onClickOverlappedPoints = (points, coordinates) => {
108 | this._updateSpiderifierProps([points], coordinates);
109 | };
110 |
111 | onSpiderifierRemoved(lngLat) {
112 | const { overlappedPointsGroup } = this.state;
113 | if (_.isArray(overlappedPointsGroup)) {
114 | const removedIndex = overlappedPointsGroup.findIndex(({ coordinates }) => _.isEqual(coordinates, lngLat));
115 |
116 | if (removedIndex > -1) {
117 | const newGroup = [
118 | ...overlappedPointsGroup.slice(0, removedIndex),
119 | ...overlappedPointsGroup.slice(removedIndex + 1),
120 | ];
121 | this.setState({ overlappedPointsGroup: newGroup });
122 | }
123 | }
124 |
125 | const { onSpiderifierRemoved } = this.props;
126 | if (_.isFunction(onSpiderifierRemoved)) {
127 | onSpiderifierRemoved(lngLat);
128 | }
129 | }
130 |
131 | onMapChange = () => {
132 | const { onlySpiderifier } = this.props;
133 | if (!onlySpiderifier && _.isArray(this._spiderifieredLocations)) {
134 | const { data, radius } = this.props;
135 | const map = this.getMapInstance();
136 | this._spiderifieredLocations.forEach((lngLat) => {
137 | const points = findPointsWithSameLocation(data, lngLat, map, radius);
138 |
139 | if (!points) {
140 | this.onSpiderifierRemoved(lngLat);
141 | }
142 | });
143 | }
144 | };
145 |
146 | _checkAndUpdatePoints(prevProps) {
147 | if (checkPropsChange(this.props, prevProps, ["data", "showInitialSpiderifier", "onlySpiderifier"], _.isEqual)) {
148 | this._updatePoints();
149 | }
150 | }
151 |
152 | _getComponentProps(keys) {
153 | return _.pick(this.props, keys);
154 | }
155 |
156 | _getWrappedComponentProps() {
157 | return this._getComponentProps(MARKER_PROPS);
158 | }
159 |
160 | _getSpiderifierComponentProps() {
161 | return this._getComponentProps(SPIDERIFIER_PROPS);
162 | }
163 |
164 | _groupNearestPoint(props) {
165 | const { data, showInitialSpiderifier, onlySpiderifier } = props;
166 | const map = this.getMapInstance();
167 | const groupedPoints = groupNearestPointsByRadius(data, map, ClusterOptions.NearestPointsRadius);
168 |
169 | if (groupedPoints.length > 0) {
170 | if (onlySpiderifier && groupedPoints.length === 1) {
171 | this._updateSpiderifierProps(groupedPoints);
172 | } else if (showInitialSpiderifier) {
173 | let firstGroup = groupedPoints.find((group) => group.length > 1);
174 |
175 | if (firstGroup == null) {
176 | firstGroup = groupedPoints[0];
177 | }
178 |
179 | this._updateSpiderifierProps([firstGroup]);
180 | }
181 | }
182 | }
183 |
184 | _processSpiderifyProperties(props) {
185 | const { spiderifyPropsProcessor } = this.props;
186 | if (_.isFunction(spiderifyPropsProcessor)) {
187 | return spiderifyPropsProcessor(props);
188 | }
189 |
190 | return props;
191 | }
192 |
193 | _renderSpiderifierContent(key, properties) {
194 | const { spiralComponent: SpiralComponent } = this.props;
195 | if (SpiralComponent) {
196 | return ;
197 | }
198 | return (
199 |
200 |
{properties.label}
201 |
202 | );
203 | }
204 |
205 | _renderSpiderifier() {
206 | const { overlappedPointsGroup } = this.state;
207 |
208 | if (overlappedPointsGroup && overlappedPointsGroup.length > 0) {
209 | const spiderifierComponentProps = this._getSpiderifierComponentProps();
210 |
211 | return overlappedPointsGroup.map((overlappedPoints, index) => {
212 | const { coordinates, markers } = overlappedPoints;
213 |
214 | return (
215 |
216 | {markers.map((marker, index) => this._renderSpiderifierContent(index, marker))}
217 |
218 | );
219 | });
220 | }
221 |
222 | return null;
223 | }
224 |
225 | _shouldRenderClusterLayer() {
226 | const { onlySpiderifier, overlappedPointsGroup } = this.props;
227 | return !onlySpiderifier || !overlappedPointsGroup || overlappedPointsGroup.length > 1;
228 | }
229 |
230 | _updatePoints(props = this.props) {
231 | const { data, showInitialSpiderifier, onlySpiderifier } = props;
232 |
233 | if (data != null && (showInitialSpiderifier || onlySpiderifier)) {
234 | this._groupNearestPoint(props);
235 | }
236 | }
237 |
238 | _updateSpiderifierProps(group, coordinates) {
239 | this._spiderifieredLocations = [];
240 | if (group.length > 0) {
241 | const overlappedPointsGroup = group.map((points) => {
242 | if (points.length > 0) {
243 | const properties = points.map((feature) => feature.properties);
244 | let coords = coordinates;
245 |
246 | if (coords == null) {
247 | coords = getCoord(points[0]);
248 | }
249 | return {
250 | markers: this._processSpiderifyProperties(properties),
251 | coordinates: coords,
252 | };
253 | }
254 |
255 | return null;
256 | });
257 |
258 | const { onShowSpiderifier } = this.props;
259 | overlappedPointsGroup.forEach((group) => {
260 | const { coordinates, markers } = group;
261 |
262 | this._spiderifieredLocations.push(coordinates);
263 | if (_.isFunction(onShowSpiderifier)) {
264 | onShowSpiderifier(coordinates, markers);
265 | }
266 | });
267 |
268 | this.setState({
269 | overlappedPointsGroup,
270 | });
271 | }
272 | }
273 |
274 | render() {
275 | const wrappedComponentProps = this._getWrappedComponentProps();
276 |
277 | return (
278 |
279 | {this._shouldRenderClusterLayer() && (
280 |
281 | )}
282 | {this._renderSpiderifier()}
283 |
284 | );
285 | }
286 | }
287 |
288 | ConnectedWithSpiderifierComponent.propTypes = {
289 | /**
290 | * Indicate if the spiderifier should be shown for the first overlapped point onload
291 | */
292 | showInitialSpiderifier: PropTypes.bool,
293 |
294 | /**
295 | * Indicate if the spiderifier should be shown without wrapped component
296 | */
297 | onlySpiderifier: PropTypes.bool,
298 |
299 | /**
300 | * Handler to transform the properties of each point
301 | */
302 | spiderifyPropsProcessor: PropTypes.func,
303 |
304 | /**
305 | * Callback when a spiderifier shown
306 | */
307 | onShowSpiderifier: PropTypes.func,
308 |
309 | /**
310 | * [Optional] Handle when user do zoom/move to change the map and made the points
311 | * on the map changed and don't have overlapped points anymore
312 | */
313 | onSpiderifierRemoved: PropTypes.func,
314 |
315 | /**
316 | * Allow to customize the spiral component
317 | */
318 | spiralComponent: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
319 | };
320 |
321 | ConnectedWithSpiderifierComponent.defaultProps = {
322 | ...WrappedComponent.defaultProps,
323 | ...ReactMapboxGlSpiderifier.defaultProps,
324 | };
325 |
326 | return ConnectedWithSpiderifierComponent;
327 | };
328 |
329 | export default connectWithSpiderifierPoint;
330 |
--------------------------------------------------------------------------------
/src/lib/common/hoc/detectLocationHasOverlappedPoints.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import _ from "lodash";
4 | import { featureCollection as createFeatureCollection, point as createPoint } from "@turf/helpers";
5 | import { findPointsWithSameLocation } from "../utils";
6 | import { ClusterOptions } from "../constants/ClusterOptions";
7 | import MappedComponent from "../../components/MappedComponent";
8 |
9 | /**
10 | * @type Class
11 | */
12 | const detectLocationHasOverlappedPoints = WrappedComponent => {
13 | class LayerWithOverlappedPointComponent extends MappedComponent {
14 | onClick = (properties, lngLat, event, meta) => {
15 | const { onClick } = this.props;
16 | this._handleClick(properties, lngLat, event, meta, onClick);
17 | };
18 |
19 | onClusterClick = (properties, lngLat, event, meta) => {
20 | const { onClusterClick } = this.props;
21 | this._handleClick(properties, lngLat, event, meta, onClusterClick);
22 | };
23 |
24 | _handleClick(properties, lngLat, event, meta, callback) {
25 | if (!_.isArray(properties)) {
26 | if (_.isFunction(callback)) {
27 | callback(properties, lngLat, event, meta);
28 | }
29 |
30 | return true;
31 | }
32 | const { onClickOverlappedPoints } = this.props;
33 | const map = this.getMapInstance();
34 | const features = properties.map(prop => createPoint(prop.coordinates, prop));
35 | const data = createFeatureCollection(features);
36 | const points = findPointsWithSameLocation(
37 | data,
38 | lngLat,
39 | map,
40 | ClusterOptions.NearestPointsRadius,
41 | ClusterOptions.ZoomLevel
42 | );
43 | if (points) {
44 | if (_.isFunction(onClickOverlappedPoints)) {
45 | onClickOverlappedPoints(features, lngLat, event, meta);
46 | return false;
47 | }
48 | } else if (_.isFunction(callback)) {
49 | callback(properties, lngLat, event, meta);
50 | }
51 |
52 | return true;
53 | }
54 |
55 | render() {
56 | const props = {
57 | ...this.props,
58 | onClick: this.onClick,
59 | onClusterClick: this.onClusterClick
60 | };
61 |
62 | return ;
63 | }
64 | }
65 |
66 | LayerWithOverlappedPointComponent.propTypes = {
67 | /**
68 | * [Optional] Handle when user click on a location which has overlapped points
69 | */
70 | onClickOverlappedPoints: PropTypes.func
71 | };
72 |
73 | LayerWithOverlappedPointComponent.defaultProps = {
74 | ...WrappedComponent.defaultProps
75 | };
76 |
77 | return LayerWithOverlappedPointComponent;
78 | };
79 |
80 | export default detectLocationHasOverlappedPoints;
81 |
--------------------------------------------------------------------------------
/src/lib/common/hoc/doZoomingOnClick.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import _ from "lodash";
4 | import MappedComponent from "../../components/MappedComponent";
5 | import { calculateNextZoomLevel } from "../utils";
6 |
7 | /**
8 | * @type Class
9 | */
10 | const doZoomingOnClick = (WrappedComponent) => {
11 | class ZoomableComponent extends MappedComponent {
12 | onClusterClick = (properties, lngLat, event, meta) => {
13 | const { onClusterClick, clusterClickEnabled } = this.props;
14 |
15 | if (!clusterClickEnabled) {
16 | return;
17 | }
18 |
19 | const map = this.getMapInstance();
20 | const currentZoom = map.getZoom();
21 | const maxZoom = map.getMaxZoom();
22 | const zoom = calculateNextZoomLevel(currentZoom, maxZoom);
23 |
24 | map.flyTo({ center: lngLat, zoom });
25 |
26 | this._handleClick(properties, lngLat, event, meta, onClusterClick);
27 | };
28 |
29 | _handleClick(properties, lngLat, event, meta, callback) {
30 | if (_.isFunction(callback)) {
31 | callback(properties, lngLat, event, meta);
32 | }
33 | }
34 |
35 | render() {
36 | const props = {
37 | ...this.props,
38 | onClusterClick: this.onClusterClick,
39 | };
40 |
41 | return ;
42 | }
43 | }
44 |
45 | ZoomableComponent.propTypes = {
46 | clusterClickEnabled: PropTypes.bool,
47 | };
48 |
49 | ZoomableComponent.defaultProps = {
50 | ...WrappedComponent.defaultProps,
51 | clusterClickEnabled: true,
52 | };
53 |
54 | return ZoomableComponent;
55 | };
56 |
57 | export default doZoomingOnClick;
58 |
--------------------------------------------------------------------------------
/src/lib/common/hoc/index.js:
--------------------------------------------------------------------------------
1 | import connectWithSpiderifierPoint from "./connectWithSpiderifierPoint";
2 | import detectLocationHasOverlappedPoints from "./detectLocationHasOverlappedPoints";
3 | import doZoomingOnClick from "./doZoomingOnClick";
4 |
5 | export { connectWithSpiderifierPoint, detectLocationHasOverlappedPoints, doZoomingOnClick };
6 |
--------------------------------------------------------------------------------
/src/lib/common/hoc/spiderifier.css:
--------------------------------------------------------------------------------
1 | .spiderifier-marker-content {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 28px;
6 | height: 28px;
7 | color: rgb(255, 255, 255);
8 | background-color: rgb(195, 38, 94);
9 | border-color: rgb(255, 255, 255);
10 | border-width: 1px;
11 | border-radius: 50%;
12 | margin-left: -14px;
13 | margin-top: -14px;
14 | box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.29);
15 | font-size: 8px;
16 | font-weight: bold;
17 | text-align: center;
18 | border: 1px solid;
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/common/utils/calc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculate the next zoom level base on the current zoom
3 | * @param {number} currentZoom the current zoom level
4 | * @param {number} maxZoom the max zoom level of the map
5 | * @param {number} extraZoomLevels how many extra level more for each zoom
6 | */
7 | const calculateNextZoomLevel = (currentZoom, maxZoom = 20, extraZoomLevels = 2) => {
8 | if (currentZoom >= 14 && currentZoom < maxZoom - 2) {
9 | return maxZoom - 2;
10 | }
11 |
12 | if (currentZoom >= maxZoom - 2) {
13 | return maxZoom;
14 | }
15 |
16 | const delta = maxZoom - currentZoom;
17 | const percentage = delta / maxZoom;
18 | const zoom =
19 | currentZoom +
20 | extraZoomLevels * percentage +
21 | extraZoomLevels * Math.pow(percentage, 2) +
22 | extraZoomLevels * Math.pow(percentage, 3);
23 |
24 | return zoom;
25 | };
26 |
27 | export { calculateNextZoomLevel };
28 |
--------------------------------------------------------------------------------
/src/lib/common/utils/cluster.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import Supercluster from "supercluster";
3 | import { getCoord } from "@turf/invariant";
4 | import { LngLatBounds } from "mapbox-gl";
5 | import { GeoJSONTypes, CollectionTypes, ListKeysByType } from "../constants/GeoJSONTypes";
6 |
7 | const RADIUS_TO_EXTENDS = 200;
8 |
9 | const checkCollectionGeoJSON = data => CollectionTypes.indexOf(data.type) !== -1;
10 |
11 | const createBoundsFromCoordinates = (coordinates, bounds) => {
12 | if (bounds == null) {
13 | return new LngLatBounds(coordinates, coordinates);
14 | }
15 |
16 | return bounds.extend(coordinates);
17 | };
18 |
19 | const extendBounds = (boundary, radius = 100) => {
20 | const boundObj = new LngLatBounds(boundary);
21 | const ne = boundObj.getNorthEast();
22 | const neBound = ne.toBounds(radius / 2);
23 | const sw = boundObj.getSouthWest();
24 | const swBound = sw.toBounds(radius / 2);
25 | return _.flatten([swBound.getSouthWest().toArray(), neBound.getNorthEast().toArray()]);
26 | };
27 |
28 | const flattenCoordinates = (coordinates, positionType) => {
29 | let depth;
30 |
31 | switch (positionType) {
32 | case GeoJSONTypes.MultiPoint:
33 | case GeoJSONTypes.LineString:
34 | depth = 0;
35 | break;
36 | case GeoJSONTypes.Polygon:
37 | case GeoJSONTypes.MultiLineString:
38 | depth = 1;
39 | break;
40 | case GeoJSONTypes.MultiPolygon:
41 | depth = 2;
42 | break;
43 | case GeoJSONTypes.Point:
44 | default:
45 | depth = -1;
46 | }
47 |
48 | if (depth === -1) {
49 | return [coordinates];
50 | }
51 |
52 | return _.flattenDepth(coordinates, depth);
53 | };
54 |
55 | const getCoordinateForPosition = (position, geoJSONType = GeoJSONTypes.FeatureCollection) => {
56 | if (geoJSONType === GeoJSONTypes.FeatureCollection) {
57 | return position.geometry.coordinates;
58 | }
59 |
60 | return position.coordinates;
61 | };
62 |
63 | const getFeatureList = geoJSON => {
64 | const { type } = geoJSON;
65 | const key = ListKeysByType[type];
66 |
67 | return geoJSON[key];
68 | };
69 |
70 | const getTypeForPosition = (position, geoJSONType) => {
71 | if (geoJSONType === GeoJSONTypes.FeatureCollection) {
72 | return position.geometry.type;
73 | }
74 |
75 | return position.type;
76 | };
77 |
78 | const roundCoords = coords => [_.round(coords[0], 4), _.round(coords[1], 4)];
79 |
80 | /**
81 | * Calculate the boundary of a geojson
82 | * @param {object} data a geojson in any format
83 | * @param? {*} totalBounds [Optional] if given, the boundary will be calculated base on the current "totalBounds"
84 | * @return {LngLatBounds} the total boundary
85 | */
86 | const calculateBoundary = (data, totalBounds = null) => {
87 | const { type } = data;
88 |
89 | if (checkCollectionGeoJSON(data)) {
90 | const features = getFeatureList(data);
91 | features.forEach(feature => {
92 | let coordinates = getCoordinateForPosition(feature, type);
93 | let featureType = getTypeForPosition(feature, type);
94 | coordinates = flattenCoordinates(coordinates, featureType);
95 |
96 | if (!_.isArray(coordinates)) {
97 | return totalBounds;
98 | }
99 |
100 | if (!totalBounds) {
101 | totalBounds = new LngLatBounds(coordinates[0], coordinates[0]);
102 | }
103 |
104 | totalBounds = coordinates.reduce(function(bounds, coord) {
105 | return bounds.extend(coord);
106 | }, totalBounds);
107 | });
108 |
109 | return totalBounds;
110 | }
111 |
112 | const coordinates = getCoord(data);
113 | return createBoundsFromCoordinates(coordinates, totalBounds);
114 | };
115 |
116 | /**
117 | * Find the list of point that inside a specific radius
118 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
119 | * @param {MapBox} mapBox Required. The mapbox instance
120 | * @param {number} zoom The zoom level, at which the points is clustered
121 | * @return {Array} The list of feature
122 | */
123 | const createClusters = (data, mapBox, radius = 60, zoom) => {
124 | if (!data || !data.features || !_.isArray(data.features)) {
125 | throw new Error("Data cannot be empty");
126 | }
127 |
128 | if (!mapBox) {
129 | throw new Error("Mapbox instance must be provided");
130 | }
131 |
132 | const superC = new Supercluster({
133 | radius,
134 | maxZoom: mapBox.getMaxZoom()
135 | });
136 |
137 | const featureList = getFeatureList(data);
138 | superC.load(featureList);
139 | if (!zoom) {
140 | zoom = mapBox.getZoom();
141 | }
142 | let boundary = _.isEmpty(featureList) ? [0, 0, 0, 0] : _.flatten(calculateBoundary(data).toArray());
143 | // in case of all points at the same location,
144 | // extends its coords by 200 meters radius to make superC work.
145 | boundary = extendBounds(boundary, RADIUS_TO_EXTENDS);
146 |
147 | const clusters = featureList.length > 1 ? superC.getClusters(boundary, Math.round(zoom)) : featureList;
148 |
149 | return {
150 | superC,
151 | clusters
152 | };
153 | };
154 |
155 | /**
156 | * Find the list of point that have a similar location (lngLat)
157 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
158 | * @param {Coordinate} lngLat Required. The coordinate follow format [longitude, latitude]
159 | * @param {MapBox} mapBox Required. The mapbox instance
160 | * @param {number} radius The radius of the cluster
161 | * @param {number} zoom The zoom level, at which the points is clustered
162 | * @return {Array} The list of point at the same location. Null if cannot find the
163 | * similar points
164 | */
165 | const findPointsWithSameLocation = (data, lngLat, mapBox, radius = 5, zoom) => {
166 | if (!data || !data.features || !_.isArray(data.features)) {
167 | throw new Error("Data cannot be empty");
168 | }
169 |
170 | if (!lngLat || !_.isArray(lngLat)) {
171 | throw new Error("Specific location cannot be empty");
172 | }
173 |
174 | if (!mapBox) {
175 | throw new Error("Mapbox instance must be provided");
176 | }
177 |
178 | const { clusters, superC } = createClusters(data, mapBox, radius, zoom);
179 | const clusterAtLngLat = clusters.find(cluster =>
180 | _.isEqual(roundCoords(cluster.geometry.coordinates), roundCoords(lngLat))
181 | );
182 |
183 | if (clusterAtLngLat) {
184 | const { cluster, cluster_id, point_count } = clusterAtLngLat.properties;
185 | if (cluster && point_count > 1) {
186 | try {
187 | return superC.getLeaves(cluster_id, Infinity);
188 | } catch (e) {
189 | return null;
190 | }
191 | }
192 | }
193 |
194 | return null;
195 | };
196 |
197 | /**
198 | * Group the list of point that inside a specific radius
199 | * @param {FeatureCollection} data Required. A FeatureCollection of Point type
200 | * @param {MapBox} mapBox Required. The mapbox instance
201 | * @param {number} radius Optional. The radius of the cluster
202 | * @return {Array>} The list of grouped feature
203 | */
204 | const groupNearestPointsByRadius = (data, mapBox, radius = 60) => {
205 | if (!data || !data.features || !_.isArray(data.features)) {
206 | throw new Error("Data cannot be empty");
207 | }
208 |
209 | if (!mapBox) {
210 | throw new Error("Mapbox instance must be provided");
211 | }
212 |
213 | const zoom = mapBox.getMaxZoom() - 2;
214 | let { clusters, superC } = createClusters(data, mapBox, radius, zoom);
215 | clusters = clusters.map(cluster => {
216 | const { cluster: isCluster, cluster_id } = cluster.properties;
217 | if (isCluster) {
218 | try {
219 | return superC.getLeaves(cluster_id, Infinity);
220 | } catch (e) {
221 | return null;
222 | }
223 | }
224 |
225 | return [cluster];
226 | });
227 |
228 | return _.filter(clusters);
229 | };
230 |
231 | export { createClusters, findPointsWithSameLocation, groupNearestPointsByRadius };
232 |
--------------------------------------------------------------------------------
/src/lib/common/utils/event.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | const EVENT_PREFIX = /^on(.+)$/i;
4 |
5 | export const extractEventHandlers = (props, eventPrefix = EVENT_PREFIX) => {
6 | return _.reduce(
7 | Object.keys(props),
8 | (res, prop) => {
9 | const cb = props[prop];
10 | if (eventPrefix.test(prop) && _.isFunction(cb)) {
11 | const key = prop.replace(eventPrefix, (match, p) => `on${p}`);
12 | res[key] = cb;
13 | }
14 | return res;
15 | },
16 | {}
17 | );
18 | };
19 |
20 | export const getExactEventHandlerName = event => {
21 | if (!_.isString(event)) {
22 | return event;
23 | }
24 |
25 | return event.replace("on", "").toLowerCase();
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/common/utils/index.js:
--------------------------------------------------------------------------------
1 | import { calculateNextZoomLevel } from "./calc";
2 | import { createClusters, findPointsWithSameLocation, groupNearestPointsByRadius } from "./cluster";
3 | import { extractEventHandlers, getExactEventHandlerName } from "./event";
4 | import { checkPropsChange } from "./props";
5 | import { isReactComponent } from "./react";
6 |
7 | export {
8 | calculateNextZoomLevel,
9 | createClusters,
10 | checkPropsChange,
11 | extractEventHandlers,
12 | findPointsWithSameLocation,
13 | getExactEventHandlerName,
14 | groupNearestPointsByRadius,
15 | isReactComponent
16 | };
17 |
--------------------------------------------------------------------------------
/src/lib/common/utils/props.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export const checkPropsChange = (props, nextProps, keys, equalityChecker = _.isEqual) => {
4 | const propsToCheck = _.pick(props, keys);
5 | const nextPropsToCheck = _.pick(nextProps, keys);
6 |
7 | if (_.isFunction(equalityChecker)) {
8 | return equalityChecker(propsToCheck, nextPropsToCheck);
9 | }
10 |
11 | return propsToCheck === nextPropsToCheck;
12 | };
13 |
--------------------------------------------------------------------------------
/src/lib/common/utils/react.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import _ from "lodash";
3 |
4 | const isFunctionComponent = component =>
5 | component && _.isFunction(component.type) && String(component.type).includes("createElement");
6 |
7 | /**
8 | * Check if a component is a custom class React component or native DOM elements (e.g. div, span)
9 | * @param {*} component
10 | * @return {bool} True if the input component is React component
11 | */
12 | export const isReactComponent = component => {
13 | const isReactComponent = _.get(component, "type.prototype.isReactComponent");
14 | const isPureReactComponent = _.get(component, "type.prototype.isPureReactComponent");
15 | const isFunctionalComponent = isFunctionComponent(component);
16 | const isFragmentComponent = _.toString(_.get(component, "type")) === "Symbol(react.fragment)";
17 | const isReactMemoComponent = _.toString(_.get(component, "$$typeof")) === "Symbol(react.memo)";
18 |
19 | return (
20 | isReactMemoComponent ||
21 | (React.isValidElement(component) &&
22 | (isReactComponent || isPureReactComponent || isFunctionalComponent || isFragmentComponent))
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/lib/components/MappedComponent.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import { MapContext } from "react-mapbox-gl";
3 |
4 | class MappedComponent extends Component {
5 | static contextType = MapContext;
6 |
7 | getMapInstance() {
8 | return this.context;
9 | }
10 | }
11 |
12 | export default MappedComponent;
13 |
--------------------------------------------------------------------------------
/src/lib/components/MarkerLayer/Component.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import PropTypes from "prop-types";
4 | import _ from "lodash";
5 | import MapboxGl from "mapbox-gl";
6 | import { checkPropsChange, extractEventHandlers, getExactEventHandlerName } from "../../common/utils";
7 | import MappedComponent from "../MappedComponent";
8 |
9 | class MarkerLayer extends MappedComponent {
10 | componentDidMount() {
11 | const node = this.attachChildren(this.props);
12 | this.layer = new MapboxGl.Marker(node).setLngLat(this.props.coordinates).addTo(this.getMapInstance());
13 | }
14 |
15 | componentDidUpdate(prevProps, prevState) {
16 | if (prevProps.coordinates !== this.props.coordinates) {
17 | this.layer.setLngLat(prevProps.coordinates);
18 | }
19 | if (prevProps.children !== this.props.children || checkPropsChange(this.props, prevProps, ["style", "className"])) {
20 | this.attachChildren(prevProps);
21 | }
22 | }
23 |
24 | componentWillUnmount() {
25 | if (!this.layer) {
26 | return;
27 | }
28 | this.layer.remove();
29 | delete this.layer;
30 | }
31 |
32 | attachChildren(props = this.props) {
33 | const { children } = props;
34 |
35 | if (children) {
36 | if (!this.element) {
37 | this.element = document.createElement("div");
38 | } else {
39 | this._unbindEvents();
40 | }
41 |
42 | const style = this.getStyle(this.props);
43 | this.element.className = this.getContainerClassName(props);
44 | Object.keys(style).forEach(s => {
45 | this.element.style[s] = style[s];
46 | });
47 | this._bindEvents();
48 |
49 | const content = this.getContent(props);
50 | ReactDOM.render(content, this.element);
51 | }
52 |
53 | return this.element;
54 | }
55 |
56 | getContainerClassName(props) {
57 | return `mapboxgl-marker ${props.className}`;
58 | }
59 |
60 | getContent(props) {
61 | const { children } = props;
62 | return {children}
;
63 | }
64 |
65 | getProperties() {
66 | return this.props.properties;
67 | }
68 |
69 | getOffset() {
70 | return [0, 0];
71 | }
72 |
73 | getStyle(props) {
74 | return _.clone(props.style) || {};
75 | }
76 |
77 | _bindEvents() {
78 | const events = extractEventHandlers(this.props);
79 | this.realHandlers = {};
80 | _.forEach(events, (handler, name) => {
81 | const realHandler = this._generateEventHander(name);
82 | this.element.addEventListener(getExactEventHandlerName(name), realHandler);
83 | this.realHandlers[name] = realHandler;
84 | });
85 | this.element.addEventListener("mousedown", this._disableMapDragPan);
86 | this.element.addEventListener("mouseup", this._enableMapDragPan);
87 | }
88 |
89 | _disableMapDragPan = () => {
90 | const map = this.getMapInstance();
91 | if (map) {
92 | map.dragPan.disable();
93 | }
94 | };
95 |
96 | _enableMapDragPan = () => {
97 | const map = this.getMapInstance();
98 | if (map) {
99 | map.dragPan.enable();
100 | }
101 | };
102 |
103 | _generateEventHander = eventName => e => {
104 | const handler = this.props[eventName];
105 | if (_.isFunction(handler)) {
106 | const { coordinates } = this.props;
107 | const properties = this.getProperties();
108 | handler(properties, coordinates, this.getOffset(), e);
109 | }
110 | };
111 |
112 | _unbindEvents() {
113 | const events = extractEventHandlers(this.props);
114 | this.element.removeEventListener("mousedown", this._disableMapDragPan);
115 | this.element.removeEventListener("mouseup", this._enableMapDragPan);
116 | _.forEach(events, (handler, name) => {
117 | const realHandler = this.realHandlers[name];
118 | this.element.removeEventListener(getExactEventHandlerName(name), realHandler);
119 | });
120 |
121 | delete this.realHandlers;
122 | }
123 |
124 | render() {
125 | return null;
126 | }
127 | }
128 |
129 | MarkerLayer.displayName = "MarkerLayer";
130 |
131 | MarkerLayer.propTypes = {
132 | /**
133 | * (required): [number, number] Display the Marker at the given position
134 | */
135 | coordinates: PropTypes.array.isRequired,
136 |
137 | /**
138 | * Properties of each Marker, will be passed back when events trigged
139 | */
140 | properties: PropTypes.oneOfType([
141 | PropTypes.array.isRequired,
142 | PropTypes.object.isRequired,
143 | PropTypes.string.isRequired
144 | ]),
145 |
146 | /**
147 | * Apply the className to the container of the Marker
148 | */
149 | className: PropTypes.string,
150 |
151 | /**
152 | * Apply style to the Marker container
153 | */
154 | style: PropTypes.object,
155 |
156 | /**
157 | * Child node(s) of the component, to be rendered as custom Marker
158 | */
159 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
160 |
161 | /**
162 | * [Optional] The click event handler
163 | */
164 | onClick: PropTypes.func,
165 |
166 | /**
167 | * [Optional] The mouse down event handler
168 | */
169 | onMouseDown: PropTypes.func,
170 |
171 | /**
172 | * [Optional] The mouse enter event handler
173 | */
174 | onMouseEnter: PropTypes.func,
175 |
176 | /**
177 | * [Optional] The mouse leave event handler
178 | */
179 | onMouseLeave: PropTypes.func,
180 |
181 | /**
182 | * [Optional] The mouse move event handler
183 | */
184 | onMouseMove: PropTypes.func,
185 |
186 | /**
187 | * [Optional] The mouse out event handler
188 | */
189 | onMouseOut: PropTypes.func,
190 |
191 | /**
192 | * [Optional] The mouse over event handler
193 | */
194 | onMouseOver: PropTypes.func,
195 |
196 | /**
197 | * [Optional] The mouse up event handler
198 | */
199 | onMouseUp: PropTypes.func
200 | };
201 |
202 | export default MarkerLayer;
203 |
--------------------------------------------------------------------------------
/src/lib/components/MarkerLayer/index.jsx:
--------------------------------------------------------------------------------
1 | import MarkerLayer from "./Component";
2 |
3 | export { MarkerLayer };
4 |
--------------------------------------------------------------------------------
/src/lib/components/ReactMapboxGlCluster/ClusterLayer.css:
--------------------------------------------------------------------------------
1 | .cluster-layer-container {
2 | width: 30px;
3 | height: 30px;
4 | }
5 |
6 | .cluster-layer--cluster {
7 | width: 30px;
8 | height: 30px;
9 | border-radius: 50%;
10 | background-color: rgba(33, 150, 243, 0.8);
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | color: white;
15 | cursor: pointer;
16 | }
17 |
18 | .cluster-layer--cluster:before {
19 | content: " ";
20 | position: absolute;
21 | border-radius: 50%;
22 | width: 40px;
23 | height: 40px;
24 | background-color: rgba(33, 150, 243, 0.6);
25 | z-index: -1;
26 | }
27 |
28 | .cluster-layer--point {
29 | width: 20px;
30 | height: 20px;
31 | border-radius: 50%;
32 | background-color: rgba(195, 38, 94, 0.8);
33 | border: 1px solid #ffffff;
34 | }
35 |
36 | .cluster-layer--point:hover {
37 | width: 25px;
38 | height: 25px;
39 | border-width: 2px !important;
40 | }
41 |
42 | .marker-content.hovered .cluster-layer--cluster {
43 | width: 40px;
44 | height: 40px;
45 | }
46 |
47 | .marker-content.hovered .cluster-layer--cluster:before {
48 | width: 50px;
49 | height: 50px;
50 | }
51 |
52 | .marker-content.hovered .cluster-layer--point {
53 | width: 25px;
54 | height: 25px;
55 | border-color: #51dbd3 !important;
56 | border-width: 2px !important;
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/components/ReactMapboxGlCluster/ClusterLayer.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import PropTypes from "prop-types";
3 | import classnames from "classnames";
4 | import { getCoord } from "@turf/invariant";
5 | import { Cluster } from "react-mapbox-gl";
6 | import { extractEventHandlers } from "../../common/utils";
7 | import { MarkerLayer } from "../MarkerLayer";
8 | import "./ClusterLayer.css";
9 |
10 | class ClusterLayer extends PureComponent {
11 | _clusterMarkerFactory = (coordinates, pointCount, getLeaves) => {
12 | const { clusterClassName } = this.props;
13 | const className = classnames("cluster-layer--cluster", clusterClassName);
14 | const points = getLeaves();
15 | const pointsProps = this._getPointsProps(points);
16 | const clusterEventHandlers = extractEventHandlers(this.props, /^onCluster(.+)$/i);
17 |
18 | return (
19 |
26 |
29 |
30 | );
31 | };
32 |
33 | _getClusterProps() {
34 | const { radius, minZoom, maxZoom, extent, nodeSize } = this.props;
35 |
36 | return {
37 | radius,
38 | minZoom,
39 | maxZoom,
40 | extent,
41 | nodeSize
42 | };
43 | }
44 |
45 | _getPointsProps(points) {
46 | return points.map(point => {
47 | const feature = point.props["data-feature"];
48 | const { properties } = feature;
49 | return {
50 | ...properties,
51 | coordinates: getCoord(feature)
52 | };
53 | });
54 | }
55 |
56 | _renderMarkers() {
57 | const { data, pointClassName, pointStyles = {}, markerComponent: MarkerComponent } = this.props;
58 | const markerClassName = classnames("cluster-layer--point", pointClassName);
59 |
60 | return data.features.map((feature, key) => {
61 | const {
62 | geometry: { coordinates },
63 | properties
64 | } = feature;
65 | const { style } = properties;
66 | const eventHandlers = extractEventHandlers(this.props);
67 | const cssObject = {
68 | ...pointStyles,
69 | ...style
70 | };
71 |
72 | return (
73 |
80 | {MarkerComponent ? (
81 |
82 | ) : (
83 |
84 | )}
85 |
86 | );
87 | });
88 | }
89 |
90 | render() {
91 | const clusterProps = this._getClusterProps();
92 |
93 | return (
94 |
95 | {this._renderMarkers()}
96 |
97 | );
98 | }
99 | }
100 |
101 | ClusterLayer.displayName = "ClusterLayer";
102 |
103 | ClusterLayer.propTypes = {
104 | /**
105 | * Data source for layer. It must to follow FeatureCollection geojson format
106 | */
107 | data: PropTypes.shape({
108 | type: PropTypes.oneOf(["FeatureCollection"]).isRequired,
109 | features: PropTypes.arrayOf(
110 | PropTypes.shape({
111 | type: PropTypes.oneOf(["Feature"]).isRequired,
112 | geometry: PropTypes.shape({
113 | type: PropTypes.string.isRequired,
114 | coordinates: PropTypes.array.isRequired
115 | }).isRequired,
116 | properties: PropTypes.object.isRequired
117 | })
118 | ).isRequired
119 | }),
120 |
121 | /**
122 | * [Optional] Cluster radius, in pixels.
123 | */
124 | radius: PropTypes.number,
125 |
126 | /**
127 | * [Optional] Minimum zoom level at which clusters are generated.
128 | */
129 | minZoom: PropTypes.number,
130 |
131 | /**
132 | * [Optional] Maximum zoom level at which clusters are generated.
133 | */
134 | maxZoom: PropTypes.number,
135 |
136 | /**
137 | * [Optional] (Tiles) Tile extent. Radius is calculated relative to this value.
138 | */
139 | extent: PropTypes.number,
140 |
141 | /**
142 | * [Optional] Size of the KD-tree leaf node. Affects performance.
143 | */
144 | nodeSize: PropTypes.number,
145 |
146 | /**
147 | * [Optional] The class name of each point.
148 | */
149 | pointClassName: PropTypes.string,
150 |
151 | /**
152 | * [Optional] The styles name of each point.
153 | */
154 | pointStyles: PropTypes.object,
155 |
156 | /**
157 | * [Optional] The class name of each cluster.
158 | */
159 | clusterClassName: PropTypes.string,
160 |
161 | /**
162 | * [Optional] Customize the marker
163 | */
164 | markerComponent: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
165 |
166 | /**
167 | * [Optional] Handle when user move the mouse leave a point
168 | */
169 | onMouseLeave: PropTypes.func,
170 |
171 | /**
172 | * [Optional] Handler for when user on marker
173 | **/
174 | onClick: PropTypes.func,
175 |
176 | /**
177 | * [Optional] Handle when user click on cluster
178 | */
179 | onClusterClick: PropTypes.func,
180 |
181 | /**
182 | * [Optional] Handle when user move the mouse enter a cluster
183 | */
184 | onClusterMouseEnter: PropTypes.func,
185 |
186 | /**
187 | * [Optional] Handle when user move the mouse leave a cluster
188 | */
189 | onClusterMouseLeave: PropTypes.func
190 | };
191 |
192 | ClusterLayer.defaultProps = {
193 | radius: 60,
194 | minZoom: 0,
195 | maxZoom: 20,
196 | extent: 512,
197 | nodeSize: 64
198 | };
199 |
200 | export default ClusterLayer;
201 |
--------------------------------------------------------------------------------
/src/lib/components/ReactMapboxGlCluster/MapboxGlCluster.jsx:
--------------------------------------------------------------------------------
1 | import { connectWithSpiderifierPoint, detectLocationHasOverlappedPoints, doZoomingOnClick } from "../../common/hoc";
2 | import ClusterLayer from "./ClusterLayer";
3 |
4 | const ClusterLayerWithOverlappedPoints = detectLocationHasOverlappedPoints(ClusterLayer);
5 |
6 | const ZoomableClusterLayer = doZoomingOnClick(ClusterLayerWithOverlappedPoints);
7 |
8 | const MapboxGlCluster = connectWithSpiderifierPoint(ZoomableClusterLayer);
9 |
10 | export default MapboxGlCluster;
11 |
--------------------------------------------------------------------------------
/src/lib/components/ReactMapboxGlCluster/index.js:
--------------------------------------------------------------------------------
1 | import MapboxGlCluster from "./MapboxGlCluster";
2 |
3 | export default MapboxGlCluster;
4 |
--------------------------------------------------------------------------------
/src/lib/components/ReactMapboxGlCluster/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "cluster-layer",
4 | "main": "./index.js"
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import ReactMapboxGlCluster from "./components/ReactMapboxGlCluster";
2 |
3 | export { ReactMapboxGlCluster };
4 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener("load", () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | "This web app is being served cache-first by a service " +
44 | "worker. To learn more, visit https://bit.ly/CRA-PWA"
45 | );
46 | });
47 | } else {
48 | // Is not localhost. Just register service worker
49 | registerValidSW(swUrl, config);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | if (installingWorker == null) {
62 | return;
63 | }
64 | installingWorker.onstatechange = () => {
65 | if (installingWorker.state === "installed") {
66 | if (navigator.serviceWorker.controller) {
67 | // At this point, the updated precached content has been fetched,
68 | // but the previous service worker will still serve the older
69 | // content until all client tabs are closed.
70 | console.log(
71 | "New content is available and will be used when all " +
72 | "tabs for this page are closed. See https://bit.ly/CRA-PWA."
73 | );
74 |
75 | // Execute callback
76 | if (config && config.onUpdate) {
77 | config.onUpdate(registration);
78 | }
79 | } else {
80 | // At this point, everything has been precached.
81 | // It's the perfect time to display a
82 | // "Content is cached for offline use." message.
83 | console.log("Content is cached for offline use.");
84 |
85 | // Execute callback
86 | if (config && config.onSuccess) {
87 | config.onSuccess(registration);
88 | }
89 | }
90 | }
91 | };
92 | };
93 | })
94 | .catch(error => {
95 | console.error("Error during service worker registration:", error);
96 | });
97 | }
98 |
99 | function checkValidServiceWorker(swUrl, config) {
100 | // Check if the service worker can be found. If it can't reload the page.
101 | fetch(swUrl)
102 | .then(response => {
103 | // Ensure service worker exists, and that we really are getting a JS file.
104 | const contentType = response.headers.get("content-type");
105 | if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) {
106 | // No service worker found. Probably a different app. Reload the page.
107 | navigator.serviceWorker.ready.then(registration => {
108 | registration.unregister().then(() => {
109 | window.location.reload();
110 | });
111 | });
112 | } else {
113 | // Service worker found. Proceed as normal.
114 | registerValidSW(swUrl, config);
115 | }
116 | })
117 | .catch(() => {
118 | console.log("No internet connection found. App is running in offline mode.");
119 | });
120 | }
121 |
122 | export function unregister() {
123 | if ("serviceWorker" in navigator) {
124 | navigator.serviceWorker.ready.then(registration => {
125 | registration.unregister();
126 | });
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/create-react-app/tsconfig.json",
3 | "include": [
4 | "src/lib/**/*.js"
5 | ],
6 | "compilerOptions": {
7 | "emitDeclarationOnly": true,
8 | "noEmit": false,
9 | "declaration": true,
10 | "outDir": "dist/types"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------