po.geoJson = function(fetch) {
  var geoJson = po.layer(load, unload),
      container = geoJson.container(),
      url,
      clip = true,
      clipId = "org.polymaps." + po.id(),
      clipHref = "url(#" + clipId + ")",
      clipPath = container.insertBefore(po.svg("clipPath"), container.firstChild),
      clipRect = clipPath.appendChild(po.svg("rect")),
      scale = "auto",
      zoom = null,
      features;

  container.setAttribute("fill-rule", "evenodd");
  clipPath.setAttribute("id", clipId);

  if (!arguments.length) fetch = po.queue.json;

  function projection(proj) {
    var l = {lat: 0, lon: 0};
    return function(coordinates) {
      l.lat = coordinates[1];
      l.lon = coordinates[0];
      var p = proj(l);
      coordinates.x = p.x;
      coordinates.y = p.y;
      return p;
    };
  }

  function geometry(o, proj) {
    return o && o.type in types && types[o.type](o, proj);
  }

  var types = {

    Point: function(o, proj) {
      var p = proj(o.coordinates),
          c = po.svg("circle");
      c.setAttribute("r", 4.5);
      c.setAttribute("transform", "translate(" + p.x + "," + p.y + ")");
      return c;
    },

    MultiPoint: function(o, proj) {
      var g = po.svg("g"),
          c = o.coordinates,
          p, // proj(c[i])
          x, // svg:circle
          i = -1,
          n = c.length;
      while (++i < n) {
        x = g.appendChild(po.svg("circle"));
        x.setAttribute("r", 4.5);
        x.setAttribute("transform", "translate(" + (p = proj(c[i])).x + "," + p.y + ")");
      }
      return g;
    },

    LineString: function(o, proj) {
      var x = po.svg("path"),
          d = ["M"],
          c = o.coordinates,
          p, // proj(c[i])
          i = -1,
          n = c.length;
      while (++i < n) d.push((p = proj(c[i])).x, ",", p.y, "L");
      d.pop();
      if (!d.length) return;
      x.setAttribute("d", d.join(""));
      return x;
    },

    MultiLineString: function(o, proj) {
      var x = po.svg("path"),
          d = [],
          ci = o.coordinates,
          cj, // ci[i]
          i = -1,
          j,
          n = ci.length,
          m;
      while (++i < n) {
        cj = ci[i];
        j = -1;
        m = cj.length;
        d.push("M");
        while (++j < m) d.push((p = proj(cj[j])).x, ",", p.y, "L");
        d.pop();
      }
      if (!d.length) return;
      x.setAttribute("d", d.join(""));
      return x;
    },

    Polygon: function(o, proj) {
      var x = po.svg("path"),
          d = [],
          ci = o.coordinates,
          cj, // ci[i]
          i = -1,
          j,
          n = ci.length,
          m;
      while (++i < n) {
        cj = ci[i];
        j = -1;
        m = cj.length - 1;
        d.push("M");
        while (++j < m) d.push((p = proj(cj[j])).x, ",", p.y, "L");
        d[d.length - 1] = "Z";
      }
      if (!d.length) return;
      x.setAttribute("d", d.join(""));
      return x;
    },

    MultiPolygon: function(o, proj) {
      var x = po.svg("path"),
          d = [],
          ci = o.coordinates,
          cj, // ci[i]
          ck, // cj[j]
          i = -1,
          j,
          k,
          n = ci.length,
          m,
          l;
      while (++i < n) {
        cj = ci[i];
        j = -1;
        m = cj.length;
        while (++j < m) {
          ck = cj[j];
          k = -1;
          l = ck.length - 1;
          d.push("M");
          while (++k < l) d.push((p = proj(ck[k])).x, ",", p.y, "L");
          d[d.length - 1] = "Z";
        }
      }
      if (!d.length) return;
      x.setAttribute("d", d.join(""));
      return x;
    },

    GeometryCollection: function(o, proj) {
      var g = po.svg("g"),
          i = -1,
          c = o.geometries,
          n = c.length,
          x;
      while (++i < n) {
        x = geometry(c[i], proj);
        if (x) g.appendChild(x);
      }
      return g;
    }

  };

  function rescale(o, e, k) {
    return o.type in rescales && rescales[o.type](o, e, k);
  }

  var rescales = {

    Point: function (o, e, k) {
      var p = o.coordinates;
      e.setAttribute("transform", "translate(" + p.x + "," + p.y + ")" + k);
    },

    MultiPoint: function (o, e, k) {
      var c = o.coordinates,
          i = -1,
          n = p.length,
          x = e.firstChild,
          p;
      while (++i < n) {
        p = c[i];
        x.setAttribute("transform", "translate(" + p.x + "," + p.y + ")" + k);
        x = x.nextSibling;
      }
    }

  };

  function load(tile, proj) {
    var g = tile.element = po.svg("g");
    tile.features = [];

    proj = projection(proj(tile).locationPoint);

    function update(data) {
      var updated = [];

      /* Fetch the next batch of features, if so directed. */
      if (data.next) tile.request = fetch(data.next.href, update);

      /* Convert the GeoJSON to SVG. */
      switch (data.type) {
        case "FeatureCollection": {
          for (var i = 0; i < data.features.length; i++) {
            var feature = data.features[i],
                element = geometry(feature.geometry, proj);
            if (element) updated.push({element: g.appendChild(element), data: feature});
          }
          break;
        }
        case "Feature": {
          var element = geometry(data.geometry, proj);
          if (element) updated.push({element: g.appendChild(element), data: data});
          break;
        }
        default: {
          var element = geometry(data, proj);
          if (element) updated.push({element: g.appendChild(element), data: {type: "Feature", geometry: data}});
          break;
        }
      }

      tile.ready = true;
      updated.push.apply(tile.features, updated);
      geoJson.dispatch({type: "load", tile: tile, features: updated});
    }

    if (url != null) {
      tile.request = fetch(typeof url == "function" ? url(tile) : url, update);
    } else {
      update({type: "FeatureCollection", features: features || []});
    }
  }

  function unload(tile) {
    if (tile.request) tile.request.abort(true);
  }

  function move() {
    var zoom = geoJson.map().zoom(),
        tiles = geoJson.cache.locks(), // visible tiles
        key, // key in locks
        tile, // locks[key]
        features, // tile.features
        i, // current feature index
        n, // current feature count, features.length
        feature, // features[i]
        k; // scale transform
    if (scale == "fixed") {
      for (key in tiles) {
        if ((tile = tiles[key]).scale != zoom) {
          k = "scale(" + Math.pow(2, tile.zoom - zoom) + ")";
          i = -1;
          n = (features = tile.features).length;
          while (++i < n) rescale((feature = features[i]).data.geometry, feature.element, k);
          tile.scale = zoom;
        }
      }
    } else {
      for (key in tiles) {
        i = -1;
        n = (features = (tile = tiles[key]).features).length;
        while (++i < n) rescale((feature = features[i]).data.geometry, feature.element, "");
        delete tile.scale;
      }
    }
  }

  geoJson.url = function(x) {
    if (!arguments.length) return url;
    url = typeof x == "string" && /{.}/.test(x) ? po.url(x) : x;
    if (url != null) features = null;
    if (typeof url == "string") geoJson.tile(false);
    return geoJson.reload();
  };

  geoJson.features = function(x) {
    if (!arguments.length) return features;
    if (features = x) {
      url = null;
      geoJson.tile(false);
    }
    return geoJson.reload();
  };

  geoJson.clip = function(x) {
    if (!arguments.length) return clip;
    if (clip) container.removeChild(clipPath);
    if (clip = x) container.insertBefore(clipPath, container.firstChild);
    var locks = geoJson.cache.locks();
    for (var key in locks) {
      if (clip) locks[key].element.setAttribute("clip-path", clipHref);
      else locks[key].element.removeAttribute("clip-path");
    }
    return geoJson;
  };

  var __tile__ = geoJson.tile;
  geoJson.tile = function(x) {
    if (arguments.length && !x) geoJson.clip(x);
    return __tile__.apply(geoJson, arguments);
  };

  var __map__ = geoJson.map;
  geoJson.map = function(x) {
    if (x && clipRect) {
      var size = x.tileSize();
      clipRect.setAttribute("width", size.x);
      clipRect.setAttribute("height", size.y);
    }
    return __map__.apply(geoJson, arguments);
  };

  geoJson.scale = function(x) {
    if (!arguments.length) return scale;
    if (scale = x) geoJson.on("move", move);
    else geoJson.off("move", move);
    if (geoJson.map()) move();
    return geoJson;
  };

  geoJson.show = function(tile) {
    if (clip) tile.element.setAttribute("clip-path", clipHref);
    else tile.element.removeAttribute("clip-path");
    geoJson.dispatch({type: "show", tile: tile, features: tile.features});
    return geoJson;
  };

  geoJson.reshow = function() {
    var locks = geoJson.cache.locks();
    for (var key in locks) geoJson.show(locks[key]);
    return geoJson;
  };

  return geoJson;
};
