simplegeo / polymaps

Polymaps is a free JavaScript library for making dynamic, interactive maps in modern web browsers.
http://polymaps.org/
Other
1.6k stars 213 forks source link

Support for WMS servers, eg MapServer? #54

Closed stodge closed 13 years ago

stodge commented 13 years ago

I have MapServer working but from reading Polymap's documentation it doesn't have a WMS specific layer? I know about the image layer but I don't think it can be used as it doesn't generate the expected WMS request?

Thanks

mbostock commented 13 years ago

You can use WMS. You will, however, need to provide your own implementation of the URL format, so that Polymaps knows what images to ask for. Take a look at the po.url class as an example, and see this documentation:

http://polymaps.org/docs/image.html#url

shawnbot commented 13 years ago

And since the docs aren't that helpful, here's a suggestion:

var tiles = po.image().url("http://my.wms.server.com/?...&BBOX={B}");

The {B} placeholder gets replaced with a string that looks like S,W,N,E (south, west, north, east latitude/longitude). If you need those to be re-ordered you can implement your own url function. I'd suggest looking at the format() function inside of po.url() (in the polymaps.js source) as a starting point. Your code will probably look something like this:

var tiles = po.image().url(function(c) {
    // NW, SE
    var nw = po.map.coordinateLocation(c),
        se = po.map.coordinateLocation({row: c.row + 1, column: column + 1, zoom: c.zoom}),
        // decimal precision based on zoom level
        pn = Math.ceil(Math.log(c.zoom) / Math.LN2),
        // number precision formatting function
        fmt = function(n) { return n.toFixed(pn); };
    // S,W,N,E (re-order these as you see fit)
    var bbox = [se.lat, se.lon, nw.lat, nw.lon].map(fmt).join(",");
    return "http://my.wms.server.com/?...&BBOX=" + bbox;
});
drewwells commented 13 years ago

I have this same problem. I will try to use your function as a starting point. You sure po is able to do this without the help of proj4? OpenLayers has good wrappers for supporting Spherical Mercator

shawnbot commented 13 years ago

Sorry, I should have noted that the above function assumes your WMS output coordinates are projected in WGS84 (EPSG:4326). I wouldn't recommend doing lots of coordinate re-projection client side if you can avoid it. :)

drewwells commented 13 years ago

I'm overlaying ogc:1.0:googlemapscompatible.... epsg 3857??? onto the openstreetmap demos. So this would probably work for 3857 but not 4326. I'm currently playing with it to see what sort of magic I can make happen.

drewwells commented 13 years ago

Okay I've succeeded (mostly). I wish polymaps would document or link to the documentation to how their xyz works. I'm getting it by trial and error at the moment.

To solve this problem you need two know two things. Polymaps gives you 3857 coordinates in lon/lat. WMS requests uses the format "NE.lon, NE.lat, SW.lon, SW.lat" so the above code will give you server errors.

In order to overlay a WMS in 3857, you will need to take Polymaps lon/lat and transform it to 3857 (sounds retarded eh? well thats OGC for you). The transform outputs x/y by the way. You can use proj4js to provide this math for free. I'm still working out the kinks, but am pretty confident to have this working 100% tomorrow.

Maybe this is something that can be included as a demo. I won't be stealing proj4js's algorithm, so the example would require proj4js as a dependency.

drewwells commented 13 years ago

here's my WMS bbox calculator, proj4js is required to reproject from 4326 coordinates of Polymaps to 3857 mercator that it is requesting tiles in. http://proj4js.org/ is required to use this.

var tiles = po.image().url(function(c) {

    return "http://<WMSSERVER>?BBOX=" +
    bbox(c) + 
    "&LAYERS=<LAYERNAME>&REQUEST=GetMap&VERSION=1.1.1&SERVICE=WMS&dpi=96&backendFeatureTypeNames=&SRS=EPSG:3857&FORMAT=image/gif&TRANSPARENT=TRUE&BGCOLOR=0xFFFFFF&EXCEPTIONS=application/vnd.ogc.se_inimage&WIDTH=256&HEIGHT=256&STYLES=default";

});

function epsg(p){
    var src = new Proj4js.Proj('EPSG:4326');
    var dest = new Proj4js.Proj('EPSG:900913');
    return Proj4js.transform(src,dest, new Proj4js.Point(p));
}

function bbox(c){
    var lowerleft, upperright;
    var ur = upperright = po.map.coordinateLocation({
        row: c.row,
        column: c.column + 1,
        zoom: c.zoom
    }),
    ll = lowerleft = po.map.coordinateLocation({
        row: c.row + 1,
        column: c.column,
        zoom: c.zoom
    });
    //Transform to Spherical Mercator 900913 ala ESPG: 3857
    ll = epsg(ll.lon + ", " + ll.lat);
    ur = epsg(ur.lon + ", " + ur.lat);
    //WMS requests in lowerleft to upperright
    return [ll.x, ll.y, ur.x, ur.y].join(", ");
}

pmap.add(tiles);
drewwells commented 13 years ago

I'm sure you're like me and want a working URL to try. So here's a server/layer you can use. Replace this in the tiles function.

var tiles = po.image().url(function(c) {

    return "http://demo.mapserver.org/cgi-bin/mapserv?BBOX=" +
        bbox(c) + "&map=/osgeo/mapserver/msautotest/world/world.map&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=world_merc&FORMAT=image/jpeg&WIDTH=720&HEIGHT=360&SRS=EPSG:3857";
}
shawnbot commented 13 years ago

Forgive my ignorance, but isn't there a way to specify an input SRS in WMS query string parameters? Couldn't you just use "&SRS=EPSG:4326" instead of "&SRS=EPSG:3857" in your query string?

drewwells commented 13 years ago

The two are fundamentally different. You could specify 4326, but then the maps would not overlay. I can best show this by looking at two maps in different EPSG's of the Earth.

4326 3857 Stretching of the image would not be sufficient to make these two overlay correctly.

shawnbot commented 13 years ago

I think you need to use WMS's tile mode instead of GetMap, which seems only to expose the input SRS for the BBOX parameter and not an output (re-projection) SRS. But I'm happy to be proven wrong if you can point me toward documentation for how to do it!

Anyway, this URL template will give you Polymaps-compatible tiles (like this one) from the MapServer demo services in "tile mode":

po.image().url("http://demo.mapserver.org/cgi-bin/mapserv" +
  "?map=/osgeo/mapserver/msautotest/world/world.map" +
  "&mode=tile&tile={X}+{Y}+{Z}&LAYERS=world_latlong");

If you wanted to parameterize that into a Polymaps-like layer interface, it might look like this (warning: untested code!):

var mapServerTile = function() {
    var layer = po.image(),
        service = "http://demo.mapserver.org/cgi-bin/mapserv",
        map = "/osgeo/mapserver/msautotest/world/world.map",
        layers = ["world_latlong"];

    function updateTemplate() {
        layer.url(service +
            "?mode=tile&map=" + map +
            "&layers=" + layers.join(" ") +
            "&tile={X}+{Y}+{Z}"
        );
    }

    layer.service = function(x) {
        if (arguments.length) {
            service = x;
            updateTemplate();
            return layer;
        } else {
            return service;
        }
    };

    layer.map = function(x) {
        if (arguments.length) {
            map = x;
            updateTemplate();
            return layer;
        } else {
            return map;
        }
    };

    layer.layers = function(x) {
        if (arguments.length) {
            layers = (typeof x == "string") ? x.split(" ") : x;
            updateTemplate();
            return layer;
        } else {
            return layers;
        }
    };

    return layer;
};
shawnbot commented 13 years ago

And just in case that doesn't work, here are two reference tiles: MapServer word_latlong ("2 1 2" in "X Y Z" format) and acetate base layer ("2/2/1" in "Z/X/Y" format).

drewwells commented 13 years ago

I'm not familiar with MapServer at all, they just expose an OGC WMS complaint service, which is why I linked to them in my little demo here.

Polymaps is mostly compatible with WMTS which is a tiling interface. WMS on the other hand was made to display tiles in any size and format. Usually, you request these images in the size of the entire map. It's extremely costly, inefficient, and well crappy. However, a lot of data is only served via WMS. The tile mode you are referencing must be something specific to MapServer, I have never seen a URL like that. It probably corresponds to the new WMTS standard. WMTS urls look like TileMatrixSet={Z}&TileRow={X}&TileColumn={Y} (roughly).

My company builds OGC specific servers, and WMTS is something new to us. So our customers will be serving data only in WMS for quite some time until they upgrade. I think it is still important to support this interface. Polymaps is pretty flexible, so I'm pretty happy with the function I have written as an effective way of supporting WMS.

I have had great success using Polymaps with WMTS, but to support WMS you need to use the functions I wrote. At a minimum the x/y/z need to be translated to lon/lat or x/y in the relevant EPSG.

By the way, did you overlay this MapServer tile request on OpenStreet or Bing to make sure it looks like 3857 in my link? It should, most providers are moving to 3857, since Microsoft and Google decided to go with it over the more prevalent 4326 of olden days. You can read way more about this from some experts, I'm no expert on this topic, I'm just a JavaScript developer that has a new job at a Geo-spatial company. http://docs.openlayers.org/library/spherical_mercator.html

drewwells commented 13 years ago

I'll answer the bounding box question separately. The bounding boxes change between EPSG. In fact, I believe 4326 is in lon/lat and 3857 is in X/Y I hear this relates to meters from somewhere, but I am unclear. Anyways 3857 usually looks like 3E07 and 4326 looks like standard longitude and latitude. Confused yet? I sure as hell am :X

drewwells commented 13 years ago

Trust me, you guys are on the right track. WMS and OGC are the wrong track. X/Y/Z makes way more sense than all the garbage I'm talking about here. I'm just trying to show demos of our old server technology working on a new age Tiling solution.

shawnbot commented 13 years ago

Yeah, I guess you're right about most of this stuff. What's totally unworkable, though, is a service that generates coordinates (in GeoJSON format, for instance) that aren't in WGS84—in other words, anything that requires client-side re-projection of lots of points. Polymaps, Bing, OSM, and every other service that assumes spherical mercator (EPSG:3857/SR-ORG:6864/whatever) addresses images in either Z/X/Y or quadkey because they're mathematically easier to compute than projections between arbitrary SRSs.

So the issue is not really about right or wrong, but whose stuff presents fewer obstacles to integration with other systems. Proj4js certainly shouldn't be a prerequisite to working with WMS or anything conforming to OGC specs. I think that it should be easy to flip whatever switch in those tools is necessary to produce output that's easy to get into Polymaps, Google, Bing, or anything else that prefer spherical mercator. Or that switch should be switched by default so that people in your position never have to worry about this type of stuff.

At least, that's my opinion, sitting in my little world, blissfully ignorant of both OGC and ESRI specs. ;)

drewwells commented 13 years ago

Agk! ESRI is our rival, they aren't writing the specs! They've basically conformed to Google and Bing. Proj4js is currently necessary, unless Polymaps decided to provide these coordinate transformations (which I don't think it should). I'd rather work with WMTS requests, we just had 0 (seriously 0 internal or public) good maps to test polymaps with. So, in order to build my demo I needed to write something to use WMS. I could probably rip apart proj4js and do something that converts b/t 4326 and 900913, but it would feel wrong to steal their work. OpenLayers provides this, other projections require Proj4js in order to use them.

shawnbot commented 13 years ago

Well, if you're using WTMS it's probably still better to just point Polymaps at the GetTile service and use a URL template that substitutes the proper {Z}, {X} and {Y} values in the right places. (I tried looking in the OGC specs for the "right" way to do this and gave up when I couldn't figure out what the hell a "TileMatrixSet" is.)

I'm sorry this is such a pain in the ass! Trust me, though: it's definitely better—i.e., cleaner, easier to debug, and simpler to fix later if it breaks—to write an ugly URL template than rely on another library for re-projection of bounding box coordinates. And your users won't have to download any additional JavaScript either. :)