Skip to content

Commit

Permalink
Add React Source and Layer components (PR 1/2) (#896)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Sep 26, 2019
1 parent 46ecd99 commit 2c66725
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 0 deletions.
57 changes: 57 additions & 0 deletions docs/components/layer.md
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)

51 changes: 51 additions & 0 deletions docs/components/source.md
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)

153 changes: 153 additions & 0 deletions src/components/layer.js
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>;
}
}
128 changes: 128 additions & 0 deletions src/components/source.js
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>;
}
}

0 comments on commit 2c66725

Please sign in to comment.