-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add React Source and Layer components (PR 1/2) (#896)
- Loading branch information
1 parent
46ecd99
commit 2c66725
Showing
8 changed files
with
554 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Layer | ||
|
||
This component allows apps to create a [map layer](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) using React. | ||
|
||
```js | ||
import React from 'react'; | ||
import ReactMapGL, {Layer} from 'react-map-gl'; | ||
|
||
const parkLayer = { | ||
id: 'landuse_park', | ||
type: 'fill', | ||
source: 'mapbox', | ||
'source-layer': 'landuse', | ||
filter: ['==', 'class', 'park'] | ||
}; | ||
|
||
class Map extends React.Component { | ||
render() { | ||
const {parkColor = '#dea'} = this.props; | ||
return ( | ||
<ReactMapGL latitude={37.78} longitude={-122.41} zoom={8}> | ||
<Layer {...parkLayer} paint={{'fill-color': parkColor}} /> | ||
</ReactMapGL> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
## Properties | ||
|
||
The props provided to this component should be conforming to the [Mapbox layer specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers). | ||
|
||
When props change *shallowly*, the component will perform style diffing to update the layer. Avoid defining constant objects/arrays inline may help performance. | ||
|
||
### Identity Properties | ||
|
||
Once a `<Layer>` is mounted, the following props should not change. If you add/remove multiple JSX layers dynamically, make sure you use React's [key prop](https://reactjs.org/docs/lists-and-keys.html#keys) to give each element a stable identity. | ||
|
||
##### `id` {String} (optional) | ||
Unique identifier of the layer. If not provided, a default id will be assigned. | ||
|
||
##### `type` {String} (required) | ||
Type of the layer. | ||
|
||
### Options | ||
|
||
##### `beforeId` {String} (optional) | ||
The ID of an existing layer to insert this layer before. If this prop is omitted, the layer will be appended to the end of the layers array. This is useful when using dynamic layers with a map style from a URL. | ||
|
||
Note that layers are added by the order that they mount. They are *NOT* reordered later if their relative positions in the JSX tree change. If dynamic reordering is desired, you should manipulate `beforeId` for consistent behavior. | ||
|
||
##### `source` {String} (optional) | ||
`source` is required by some layer types in the Mapbox style specification. If `<Layer>` is used as the child of a [Source](/docs/components/source.md) component, this prop will be overwritten by the id of the parent source. | ||
|
||
## Source | ||
[layer.js](https://github.com/uber/react-map-gl/tree/5.0-release/src/components/layer.js) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# Source | ||
|
||
This component allows apps to create a [map source](https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources) using React. It may contain [Layer](/docs/components/layer.md) components as children. | ||
|
||
```js | ||
import React from 'react'; | ||
import ReactMapGL, {Source, Layer} from 'react-map-gl'; | ||
|
||
const geojson = { | ||
type: 'FeatureCollection', | ||
features: [ | ||
{type: 'Feature', geometry: {type: 'Point', coordinates: [-122.4, 37.8]}} | ||
] | ||
}; | ||
|
||
class Map extends React.Component { | ||
render() { | ||
return ( | ||
<ReactMapGL latitude={37.78} longitude={-122.41} zoom={8}> | ||
<Source id="my-data" type="geojson" data={geojson}> | ||
<Layer | ||
id="point" | ||
type="circle" | ||
paint={{ | ||
'circle-radius': 10, | ||
'circle-color': '#007cbf' | ||
}} /> | ||
</Source> | ||
</ReactMapGL> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
## Properties | ||
|
||
The props provided to this component should be conforming to the [Mapbox source specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/#sources)or [CanvasSourceOptions](https://docs.mapbox.com/mapbox-gl-js/api/#canvassourceoptions). | ||
|
||
When props change *shallowly*, the component will attempt to update the source. Do not define objects/arrays inline to avoid perf hit. | ||
|
||
Once a `<Source>` is mounted, the following props should not change. If add/remove multiple JSX sources dynamically, make sure you use React's [key prop](https://reactjs.org/docs/lists-and-keys.html#keys) to give each element a stable identity. | ||
|
||
##### `id` {String} (optional) | ||
Unique identifier of the source. If not provided, a default id will be assigned. | ||
|
||
##### `type` {String} (required) | ||
Type of the source. | ||
|
||
## Source | ||
[source.js](https://github.com/uber/react-map-gl/tree/5.0-release/src/components/source.js) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// @flow | ||
// Copyright (c) 2015 Uber Technologies, Inc. | ||
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
|
||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
import React, {PureComponent} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import MapContext from './map-context'; | ||
import assert from '../utils/assert'; | ||
import deepEqual from '../utils/deep-equal'; | ||
|
||
import type {MapContextProps} from './map-context'; | ||
|
||
const propTypes = { | ||
type: PropTypes.string.isRequired, | ||
id: PropTypes.string, | ||
source: PropTypes.string, | ||
beforeId: PropTypes.string | ||
}; | ||
|
||
type LayerProps = { | ||
id?: string, | ||
type: string, | ||
source?: string, | ||
beforeId?: string, | ||
layout: any, | ||
paint: any, | ||
filter?: Array<mixed>, | ||
minzoom?: number, | ||
maxzoom?: number | ||
}; | ||
|
||
/* eslint-disable complexity */ | ||
function diffLayerStyles(map: any, id: string, props: LayerProps, prevProps: LayerProps) { | ||
const {layout = {}, paint = {}, filter, minzoom, maxzoom, beforeId, ...otherProps} = props; | ||
|
||
if (beforeId !== prevProps.beforeId) { | ||
map.moveLayer(id, beforeId); | ||
} | ||
if (layout !== prevProps.layout) { | ||
for (const key in layout) { | ||
if (!deepEqual(layout[key], prevProps.layout[key])) { | ||
map.setLayoutProperty(id, key, layout[key]); | ||
} | ||
} | ||
} | ||
if (paint !== prevProps.paint) { | ||
for (const key in paint) { | ||
if (!deepEqual(paint[key], prevProps.paint[key])) { | ||
map.setPaintProperty(id, key, paint[key]); | ||
} | ||
} | ||
} | ||
if (!deepEqual(filter, prevProps.filter)) { | ||
map.setFilter(id, filter); | ||
} | ||
if (minzoom !== prevProps.minzoom || maxzoom !== prevProps.maxzoom) { | ||
map.setLayerZoomRange(id, minzoom, maxzoom); | ||
} | ||
for (const key in otherProps) { | ||
if (!deepEqual(otherProps[key], prevProps[key])) { | ||
map.setLayerProperty(id, key, otherProps[key]); | ||
} | ||
} | ||
} | ||
/* eslint-enable complexity */ | ||
|
||
let layerCounter = 0; | ||
|
||
export default class Layer<Props: LayerProps> extends PureComponent<Props> { | ||
static propTypes = propTypes; | ||
|
||
constructor(props: Props) { | ||
super(props); | ||
this.id = props.id || `jsx-layer-${layerCounter++}`; | ||
this.type = props.type; | ||
} | ||
|
||
componentDidMount() { | ||
this._createLayer(); | ||
} | ||
|
||
componentDidUpdate(prevProps: LayerProps) { | ||
this._updateLayer(prevProps); | ||
} | ||
|
||
componentWillUnmount() { | ||
this._map.removeLayer(this.id); | ||
} | ||
|
||
id: string; | ||
type: string; | ||
_map: any = null; | ||
|
||
_createLayer() { | ||
const map = this._map; | ||
const options = Object.assign({}, this.props); | ||
options.id = this.id; | ||
delete options.beforeId; | ||
|
||
if (map.style._loaded) { | ||
// console.log('adding layer'); | ||
map.addLayer(options, this.props.beforeId); | ||
} else { | ||
map.once('styledata', () => this.forceUpdate()); | ||
} | ||
} | ||
|
||
/* eslint-disable complexity */ | ||
_updateLayer(prevProps: LayerProps) { | ||
const {props} = this; | ||
assert(!props.id || props.id === this.id, 'layer id changed'); | ||
assert(props.type === this.type, 'layer type changed'); | ||
|
||
const map = this._map; | ||
|
||
if (!map.getLayer(this.id)) { | ||
this._createLayer(); | ||
return; | ||
} | ||
|
||
try { | ||
diffLayerStyles(map, this.id, props, prevProps); | ||
} catch (error) { | ||
console.warn(error); // eslint-disable-line | ||
} | ||
} | ||
/* eslint-disable complexity */ | ||
|
||
_render(context: MapContextProps) { | ||
this._map = context.map; | ||
return null; | ||
} | ||
|
||
render() { | ||
return <MapContext.Consumer>{this._render.bind(this)}</MapContext.Consumer>; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// @flow | ||
// Copyright (c) 2015 Uber Technologies, Inc. | ||
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
|
||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
import React, {PureComponent, cloneElement} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import MapContext from './map-context'; | ||
import assert from '../utils/assert'; | ||
|
||
import type {MapContextProps} from './map-context'; | ||
|
||
const propTypes = { | ||
type: PropTypes.string.isRequired, | ||
id: PropTypes.string | ||
}; | ||
|
||
type SourceProps = { | ||
id?: string, | ||
type: string, | ||
children?: any | ||
}; | ||
|
||
let sourceCounter = 0; | ||
|
||
export default class Source<Props: SourceProps> extends PureComponent<Props> { | ||
static propTypes = propTypes; | ||
|
||
constructor(props: Props) { | ||
super(props); | ||
this.id = props.id || `jsx-source-${sourceCounter++}`; | ||
this.type = props.type; | ||
} | ||
|
||
componentWillUnmount() { | ||
this._map.removeSource(this.id); | ||
} | ||
|
||
id: string; | ||
type: string; | ||
_map: any = null; | ||
_sourceOptions: any = {}; | ||
|
||
getSource() { | ||
return this._map.getSource(this.id); | ||
} | ||
|
||
_createSource() { | ||
const map = this._map; | ||
if (map.style._loaded) { | ||
map.addSource(this.id, this._sourceOptions); | ||
} else { | ||
map.once('styledata', () => this.forceUpdate()); | ||
} | ||
} | ||
|
||
/* eslint-disable complexity */ | ||
_updateSource() { | ||
const {_sourceOptions: sourceOptions, props} = this; | ||
assert(!props.id || props.id === this.id, 'source id changed'); | ||
assert(props.type === this.type, 'source type changed'); | ||
|
||
let changedKey = null; | ||
let changedKeyCount = 0; | ||
|
||
for (const key in props) { | ||
if (key !== 'children' && key !== 'id' && sourceOptions[key] !== props[key]) { | ||
sourceOptions[key] = props[key]; | ||
changedKey = key; | ||
changedKeyCount++; | ||
} | ||
} | ||
|
||
const {type, _map: map} = this; | ||
const source = this.getSource(); | ||
if (!source) { | ||
this._createSource(); | ||
return; | ||
} | ||
if (!changedKeyCount) { | ||
return; | ||
} | ||
if (type === 'geojson') { | ||
source.setData(sourceOptions.data); | ||
} else if (type === 'image') { | ||
source.updateImage({url: sourceOptions.url, coordinates: sourceOptions.coordinates}); | ||
} else if ( | ||
(type === 'canvas' || type === 'video') && | ||
changedKeyCount === 1 && | ||
changedKey === 'coordinates' | ||
) { | ||
source.setCoordinates(sourceOptions.coordinates); | ||
} else { | ||
map.removeSource(this.id); | ||
map.addSource(sourceOptions); | ||
} | ||
} | ||
/* eslint-enable complexity */ | ||
|
||
_render(context: MapContextProps) { | ||
this._map = context.map; | ||
this._updateSource(); | ||
return React.Children.map(this.props.children, child => | ||
cloneElement(child, { | ||
source: this.id | ||
}) | ||
); | ||
} | ||
|
||
render() { | ||
return <MapContext.Consumer>{this._render.bind(this)}</MapContext.Consumer>; | ||
} | ||
} |
Oops, something went wrong.