nodetiles/nodetiles-core

View on GitHub
lib/projector.js

Summary

Maintainability
A
55 mins
Test Coverage
// TODO this should support passing in projection strings in many formats, including preconstructed Proj4 objects 

var Proj4js = require('proj4js');
require('proj4js-defs')(Proj4js);
var __ = require('lodash');

var A = 6378137,
    MAXEXTENT = 20037508.34,
    ORIGIN_SHIFT = Math.PI * 6378137,
    D2R = Math.PI / 180,
    R2D = 180 / Math.PI; //20037508.342789244

// Cache for for storing and reusing Proj4 instances
var projectorCache = {};

// Ensure that you have a Proj4 object, pulling from the cache if necessary
var getProj4 = function(projection) {
  if (projection instanceof Proj4js.Proj) {
    return projection;
  }
  else if (projection in projectorCache) {
    return projectorCache[projection];
  }
  else {
    return projectorCache[projection] = new Proj4js.Proj(projection);
  }
};

//projection defs: we should add more here

// Credit for the math: http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/
// TODO: just use https://github.com/mapbox/node-sphericalmercator/blob/master/sphericalmercator.js
var util = {
  cleanProjString: function(text) {
    if (typeof text == "number") {
      return "EPSG:"+text;
    } else if (text.indexOf("EPSG:") > -1){
      return text;
    } else if (text.indexOf("+proj") > -1) {
      // proj4 string
      Proj4js.defs["NODETILES:9999"] = text;
      return "NODETILES:9999";
    } else {
      console.warn("Invalid projection string");
      return "EPSG:4326"
    }
  },
  pixelsToMeters: function(x, y, zoom, tileSize) {
    var mx, my;
    var tileSize = tileSize || 256;
    // meters per pixel at zoom 0
    var initialResolution = 2 * Math.PI * 6378137 / tileSize;
    //Resolution (meters/pixel) for given zoom level (measured at Equator)"
    var res = initialResolution / Math.pow(2,zoom);
    // return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom)
    mx = x * res - ORIGIN_SHIFT;
    my = y * res - ORIGIN_SHIFT;
    return [mx, my];
  },

  // Thanks to https://github.com/mapbox/node-sphericalmercator/blob/master/sphericalmercator.js
  metersToLatLon: function(c) {
    return [
        (c[0] * R2D / A),
        ((Math.PI*0.5) - 2.0 * Math.atan(Math.exp(-c[1] / A))) * R2D
    ];
  },
  latLonToMeters: function(c) {
    var xy = [
        A * c[0] * D2R,
        A * Math.log(Math.tan((Math.PI*0.25) + (0.5 * c[1] * D2R)))
    ];
    // if xy value is beyond maxextent (e.g. poles), return maxextent.
    (xy[0] > MAXEXTENT) && (xy[0] = MAXEXTENT);
    (xy[0] < -MAXEXTENT) && (xy[0] = -MAXEXTENT);
    (xy[1] > MAXEXTENT) && (xy[1] = MAXEXTENT);
    (xy[1] < -MAXEXTENT) && (xy[1] = -MAXEXTENT);
    return xy;
  },
  tileToMeters: function(x, y, zoom, tileSize){
    var tileSize = tileSize || 256;
    y = (Math.pow(2,zoom) - 1) - y; // TMS to Google tile scheme
    var min = util.pixelsToMeters(x*tileSize, y*tileSize, zoom);
    var max = util.pixelsToMeters((x+1)*tileSize, (y+1)*tileSize, zoom);
    return [min[0], min[1], max[0], max[1]];
  }
}
var project = {

  'FeatureCollection': function(inProjection, outProjection, fc) {
    var from = getProj4(inProjection);
    var to = getProj4(outProjection);
    
    var _fc = __.clone(fc);
    //console.log(_fc.features[0].geometry.coordinates[0]);
    _fc.features = _fc.features.map(project.Feature.bind(null, from, to));
    //console.log(_fc.features[0].geometry.coordinates[0]);
    return _fc;
  },
  'Feature': function(inProjection, outProjection, f) {
    var _f = __.clone(f);
    _f.geometry = __.clone(f.geometry);
    _f.geometry.coordinates = project[f.geometry.type](inProjection, outProjection, _f.geometry.coordinates);
    return _f;
  },
  'MultiPolygon': function(inProjection, outProjection, mp) {
    return mp.map(project.Polygon.bind(null, inProjection, outProjection));
  },
  'Polygon': function(inProjection, outProjection, p) {
    return p.map(project.LineString.bind(null, inProjection, outProjection));
  },
  'MultiLineString': function(inProjection, outProjection, ml) {
    return ml.map(project.LineString.bind(null, inProjection, outProjection));
  },
  'LineString': function(inProjection, outProjection, l) {
    return l.map(project.Point.bind(null, inProjection, outProjection));
  },
  'MultiPoint': function(inProjection, outProjection, mp) {
    return mp.map(project.Point.bind(null, inProjection, outProjection));
  },
  'Point': function(inProjection, outProjection, c) {
    if (inProjection && outProjection) {
      var inProjectionCode = inProjection instanceof Proj4js.Proj ?
        'EPSG:' + inProjection.srsProjNumber : inProjection;
      var outProjectionCode = outProjection instanceof Proj4js.Proj ?
        'EPSG:' + outProjection.srsProjNumber : outProjection;
      
      if (inProjectionCode == 'EPSG:4326' && outProjectionCode == 'EPSG:900913') {
        return util.latLonToMeters(c);
      }
      else if (inProjectionCode == 'EPSG:900913' && outProjectionCode == 'EPSG:4326') {
        return util.metersToLatLon(c);
      }
      
      var from = getProj4(inProjection);
      var to = getProj4(outProjection);      
      var point = new Proj4js.Point(c);
      Proj4js.transform(from, to, point);
      return [point.x, point.y];
    }
    return c;
  }
};

// TODO: cleanup interface
module.exports.util = util;
module.exports.project = project;


// {};
// this is sexy but doesn't work
/*Object.keys(project).forEach(function(featureType) {
  exports.project[featureType] = function(inProjection, outProjection, feature) {
  var from = inProjection && new Proj4js.Proj(inProjection),
  to = outProjection && new Proj4js.Proj(outProjection);

  return project[featureType](null, null, feature);
  };
  });
  */