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.
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.
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
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
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.
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.
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);
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 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.