Create a map with React-Art

Intro

Just Like the title states, we’re going to make a map with react-art. When you think of maps many people jump straight to leaflet, google maps, or mapbox. Well one of the key things they are doing is just plotting map tiles.

Map tiles are just images that can be stitched together and form a map. That is why whenever you drag on a map portions of it pop in in squares.

Don’t worry, I won’t get deep into mapping terminology because I don’t know it. If you want to learn checkout this post http://www.macwright.org/2012/05/15/how-web-maps-work.html

All you’ll need to know is Latitude, Longitude, and Tile aka (an image of a piece of a map).

Our tilemap source will be the fabulous OpenStreetMap. It’s a community driven mapping solution. Check it out and contribute if you can!

This was going to be a blog post about creating a map but I ended up turning it into a library.

What I Built

I ended up writing up a library to show it off.

Checkout https://github.com/browniefed/react-art-map for the library and examples.

We use https://github.com/zacbarton/node-googlemaps-utils and https://github.com/gagan-bansal/map-the-tiles.

These 2 libraries are helper libraries.

googlemaps-utils takes a width/height and a central lat/long and gives us a bounding box which is just the top/left coordinate and the bottom/right coordinate.

We then take that bounding box and convert it to meter points so we can generate a mercator projection.

The map-the-tiles takes those meter points and returns x,y,z points;

Those x,y,z points get fed into the OSM url http://{s}.tile.osm.org/{z}/{x}/{y}.png and we get our tile images.

We can then render them at their given top/left points w/ react-art.

Ultimately this library was built with A LOT of googling and assembling some tools people already constructed.

Some Internals

Most of the logic is just in the TileUtil. There are likely much more efficient ways to do this however this is my first stab at it with my limited geo knowledge.

Converts lat/long to meter points

1
2
3
4
5
6
degrees2meters(lon,lat) {
    var x = lon * 20037508.34 / 180;
    var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
    y = y * 20037508.34 / 180;
    return [x, y]
}

Converts meters to coordinates

1
2
3
4
5
meters2degress(x,y) {
    var lon = x *  180 / 20037508.34 ;
    var lat = Number(180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2));
    return [lon, lat]
}

The main layout generator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
getTileLayout(options) {
    var layout = [];
    var bounds = gmu.calcBounds(options.center[1], options.center[0], options.zoom, options.width, options.height); // GET COORDINATE BOUNDS

    var topLeftMeters = TileUtil.degrees2meters(bounds.left, bounds.top),
        bottomRightMeters = TileUtil.degrees2meters(bounds.right, bounds.bottom);

    //Conver the coordinates each to meters
    
    var tiler = new MapTheTiles(null, options.tileWidth); // Create a generic tiler based on our tile width
    
    var layoutForBounds = {
        top: topLeftMeters[1],
        left: topLeftMeters[0],
        right: bottomRightMeters[0],
        bottom: bottomRightMeters[1]
    };
    //Pass in the meters for each point

    var tiles = tiler.getTiles(layoutForBounds, options.zoom) // Get the x,y,z points for our zoom level

    tiles.forEach(function(tile) {
        var coordPoint = {
            x: tile.X,
            y: tile.Y,
            z: tile.Z
        },
        coord = {
            x: tile.left,
            y: tile.top,
            img: TileUtil.getTileUrl(options.tileSource, coordPoint, options.subdomains) //Just format the OSM tile resource
        };

        layout.push(coord);
    }, this);

    return layout;
}

This is how we render each tile. We have the x/y coordinates thanks to our tiler.

We use Paths.js to create a generic rectangular path. This is so we can support react-native in the future since the shapes have yet to be created.

Then we create a new fill with the tile image and set it to the width/height of the generic tile at 0,0 of the shape.

This technically is a pattern for the background but because we set it to the exact width/height of the image it just renders the image once.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var rectanglePath = Rectangle({
  top: 0,
  left: 0,
  right: 256,
  bottom: 256
}).path.print();
///UP ABOVE

        return layout.map(function(tile) {
            return (
                <Shape
                    d={rectanglePath}
                    x={tile.x}
                    y={tile.y}
                    fill={new Pattern(tile.img, this.props.tileWidth , this.props.tileWidth, 0, 0)}
                />
            )
        }, this);

The Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
var React = require('react');
var ReactMap = require('react-art-map');
var ReactArt = require('react-art'),
    Circle = require('react-art/shapes/circle');

var Map = ReactMap.Map;

var viewportWidth = function() {
    return  window.innerWidth - 100;
}
var viewportHeight = function() {
    return window.innerHeight - 100;
}

var center = [
    -122.668197,45.525292
],
offset = 3;

var Demo = React.createClass({
  getInitialState: function() {
    return {
      center: center,
      zoom: 15,
      x: 100
    }
  },
  componentDidMount: function() {
    requestAnimationFrame(this.updateCircle);
  },
  updateCircle: function() {
    if (this.state.x >= viewportWidth()) {
        offset = -3;
    } else if ( this.state.x <= 99) {
        offset = 3;
    }

    this.state.x += offset;
    this.setState(this.state, function() {
        requestAnimationFrame(this.updateCircle);
    });
  },
  handleDrag: function(newCenter) {
    this.setState({
      center: newCenter
    });
  }, 
  render: function () {
    return (
      <div>
        <Map
            width={viewportWidth()}
            height={viewportHeight()}
            center={this.state.center}
            zoom={this.state.zoom}
            tileSource="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
            onDrag={this.handleDrag}
        >
            <Circle 
                x={this.state.x}
                y={100}
                radius={30}
                stroke="#000"
                strokeWidth={5}
            />
        </Map>
      </div>
    );
  }
});

module.exports = Demo;

React Native?!?!?!

React Native has a map implementation but it doesn’t allow for much flexibility. You can render pins but that is about it.

With this library once the Pattern fill gets implemented you can render any map tile based service + any cool visualiztions on the map that you want.

I’ve logged an issue here https://github.com/facebook/react-native/issues/1462 so follow along for when it gets implemented.

Results

See the Pen PqWRvz by Jason Brown (@browniefed) on CodePen.

Tagged under mapping, react, react-art, react-native, tiles

Comments