complex line symbols with cumberland

June 6th, 2009

Back in the day, when I was writing some awesome ArcIMS app, I could create some elaborate line symbology in ArcXML:

...
      <LAYER type="featureclass" name="ROADS" visible="true" id="2">
        <DATASET name="ROADS" type="line" workspace="shp_ws-0" />
        <GROUPRENDERER>
          <SIMPLERENDERER>
            <SIMPLELINESYMBOL transparency="1.0" type="solid" width="8" captype="round" jointype="round" color="0,0,0" />
          </SIMPLERENDERER>
          <SIMPLERENDERER>
            <SIMPLELINESYMBOL transparency="1.0" type="solid" width="6" captype="round" jointype="round" color="255,0,0" />
          </SIMPLERENDERER>
          <SIMPLERENDERER>
            <SIMPLELINESYMBOL transparency="1.0" type="solid" width="1" captype="round" jointype="round" color="255,255,255" />
          </SIMPLERENDERER>
        </GROUPRENDERER>
      </LAYER>
...

(source)

What's nice about this is that it is all built into a single layer element. Now, you can't do this directly in cumberland map xml, but you can still achieve much of this functionality. The trick is to just duplicate the layer using different symbology:

...
    <Layer>
      <!--Interstates 1-->
      ...
      <Styles>
         ...
        <Style>
          <Simplify>true</Simplify>
          <SimplifyTolerance>200</SimplifyTolerance>
          <MaxScale>399</MaxScale>
          <LineColor>255,155,56,0</LineColor>
          <LineWidth>6</LineWidth>
        </Style>
      </Styles>
    </Layer>
    <Layer>
      <!--Interstates 2-->
      ...
      <Styles>
         ...
        <Style>
          <ShowLabels>true</ShowLabels>
          <LabelPixelOffset>0</LabelPixelOffset>
          <LabelFont>SansSerif</LabelFont>
          <LabelFontEmSize>6</LabelFontEmSize>
          <DrawPointSymbolOnPolyLine>true</DrawPointSymbolOnPolyLine>
          <CalculateLabelAngleForPolyLine>false</CalculateLabelAngleForPolyLine>
          <PointSymbol>Image</PointSymbol>
          <PointSize>15</PointSize>
          <FillColor>255,255,0,0</FillColor>
          <PointSymbolImagePath>images/interstate.png</PointSymbolImagePath>
          <Simplify>true</Simplify>
          <SimplifyTolerance>200</SimplifyTolerance>
          <MaxScale>399</MaxScale>
          <LineColor>255,255,156,0</LineColor>
          <LineWidth>4</LineWidth>
        </Style>
      </Styles>
    </Layer>
...

This draws the layer twice, giving us our complex symbol effect:

complex line symbols

complex line symbols

There are additional variations that could be done with this, such as drawing a dashed/dotted line on top of a solid line and so forth. Full demo and source of this map is here.

On a side note: Isn't cumberland xml so much cleaner than ArcXML? Excessive use of attributes is an XML no-no. Let the xml schema take care of preventing duplicates.

feature simplification

February 9th, 2009

I have just added an implementation of the Douglas-Peucker algorithm, which provides the ability to simplify complicated lines and polygons. This can be enabled from within a Style element:

<Style>
    <Simplify>true</Simplify>
    <SimplifyTolerance>0.000000001</SimplifyTolerance>
    ...

The advantage of this is you can set styles that are enabled at higher scales (more zoomed out) that will draw simpler features. These look better. The SimplifyTolerance is in the data's units and will filter out vertices less than it. For example, I have Florida counties data in meters:

polygon simplify tolerance examples

As the tolerance increases, the polygons look smoother. The trade-off is accuracy as you can see, and by a tolerance of 12,800 meters, the features are indistinct polygons. Finding the right value is an art, but definitely worth the work:

before:

no simplification

after:

after simplification

xml schema for maps

February 4th, 2009

I have been playing around a bit with xml schema.

XML Schemas express shared vocabularies and allow machines to carry out rules made by people. They provide a means for defining the structure, content and semantics of XML documents.

One cool thing is that both MonoDevelop and Visual Studio (and I am sure others) provide tag completion and validation for them. This is very nice when you have an arbitrary xml format such as I do for cumberland maps.

Since I have no WYSIWYG editor like ArcMap or something, it is kind of tough to get started building a map. Xml schema provides a way to speed this process up. So, I used Xsd.exe to generate a schema from one of my maps, and then extended it by hand. It's available in the source here.

Now I'll just run through a quick example with the ubiquitous Visual C# Express:

First, create a project. Next, add a New item->Xml File. When this opens we'll get an XML menu item. Select 'Schemas'. This will open a dialog listing all available xml schemas, we want to add ours. Click the 'Add' button and select the above-linked cumberland.xsd (which you will need to download). The schema will now show up in the schemas:

Added schema in VS

Press 'Ok' and if you go back to your xml document, and type '<', you should see intellisense pop up with the 'Map' element! Press tab and it should complete the element and add the namespace. From there, you can create your map without having to refer to any wiki page. It will complete tags, show warnings when you are missing required elements or when elements contain invalid values:

Xml schema warnings in VS C# Express

And list elements when the values are restricted:

intellisense on xml element values

sweet.

Using OpenGL to interact with 2D maps, part 3: putting it together

December 21st, 2008

In the previous post, I went over the basics of setting up OpenGL for drawing our maps. We got our 2D map rendered onto 3D space, and we were able to interact with it. The last step is to manage the interaction between the 3D space and the 2D map so that we can do things like calculate extents, zoom to extents, and other typical mapping functionality.

I mentioned the frustrum, which is how we want to represent the 3D space and project it onto our 2D computer screen. Well, if we stand on the x axis it would look like so:

the frustrum on the x axis

the frustrum on the x axis

The near and far clipping planes (zNear/zFar) are not pictured. For our purposes, they can be 1 and Integer.MaxValue, respectively. Neither is the aspect ratio, which will just be set to the aspect ratio of our window (width/height). That leaves us with the Field of View Y. We can start that at a value of 90.

I've placed the map in the frustrum, and as you can see, if we want to zoom to the full height of the map, we need to calculate z. Now, from the diagram, we can create a right triangle from the camera eye, the center of the map, and the top-center of the map. From there it is simple trigonometry:

// fovy = Field of View Y
eye.Z = (actualExtents.Height / 2) / Math.Tan((Math.PI/180) * fovy/2);

The X/Y coordinate of the eye/camera is simply the map center, so now we know where to put the camera in 3d space to frame our 2D picture. We can also now acquire the map scale:

scale = actualExtents.Height / Allocation.Height; // scale = map units / pixels

Accounting for window resizing

I've mentioned the field of view Y, but not X. Why is that? Well, the field of view X can be changed by changing the aspect ratio. That is, if the field of view Y is fixed, and the aspect ratio changes, then the field of view X must change. This causes problems in window resizing. Say, we expand the window in height, but not in width, since the field of view Y is fixed, we have to shrink the field of view X, which will have the effect of zooming the map in.

The solution is that we must adjust the field of view Y on every window resize by reversing our calculation to get the z value:

// calculate field of view y with our eye.Z, and new map height
// field of view x will be taken care of by setting aspect ratio
fovy = Math.Atan((actualExtents.Height/2) / eye.Z) *
                                (180/Math.PI) * 2;      // convert to degrees and double

Enough math already!

Now we have the math to interact with our map. We can zoom/pan/etc. by moving our 3d coordinate (eye) in space. If we want to work backwards and figure out our extents from our point, we can simply reverse the function for discovering eye.Z above.

The final post in this series will summarize this topic and how it will be integrated in the next version of Appomattox.

Note: The source code from this post is available here.

labels yo

December 6th, 2008

in the quest to make cumberland contain the bare minimum of map making functionality, I added labelling. They can be set to angles, positioned relative to a point, and also outlined to make them stick out. The point in polygons is the centroid. Polylines are a little trickier. Ideally, you'd want to the label to flow with the line, but for now it just looks for the longest segment and sets the angle to its slope and uses its midpoint as the point.

label examples

labels in action

labels in action

Also, Go Gators!

VirtualEarth support

December 4th, 2008

I added support for generating tiles for VirtualEarth to cumberland. I did it because it's super trivial after you have support for Google Maps. The tiles are the same except they have different min and max zoom levels and you need to save them with their quad key for VE. Luckily, MSDN provides sample code for calculating this.

As always, a demo is up.

The TileProvider class has a new TileConsumer, VirtualEarth, and the tilepyramider now excepts a new 've' argument to its consumer option. This will create a directory full of images named by their quad key. Then you'll just need to wire up some script:

function init()
{
    var map = new VEMap('myMap');
    map.LoadMap(new VELatLong(21,-100), 3, null, null, null, null, null, null);

    var bounds = [
        new VELatLongRectangle(new VELatLong(34,-120),
                new VELatLong(12,-82))];
    var layerID = "test";
    var tileSource =
        "ve/%4.png";
    var tileSourceSpec =
        new VETileSourceSpecification(layerID, tileSource);
    tileSourceSpec.NumServers = 1;
    tileSourceSpec.Bounds     = bounds;
    tileSourceSpec.MinZoom    = 1;
    tileSourceSpec.MaxZoom    = 5;
    tileSourceSpec.Opacity    = 1;
    tileSourceSpec.ZIndex     = 110;

    map.AddTileLayer(tileSourceSpec, true);
}

Create the map div and call your method:

<body onload="init();">
<div id='myMap' style="position:absolute; width:400px; height:400px;"></div>
...

And you're good to go! You'll need to build from SVN to get this.

Cumberland + KML = pure fire

December 1st, 2008

Keyhole Markup Language (KML) is the xml format used in Google Earth for overlaying data. I've implemented functionality for creating kml from a cumberland map and a command line tool (map2kml.exe) to use it. It creates a kml document with folders for each layer using the various styles, including thematic mapping, and adds the features as defined in the feature source, transforming coordinate systems if necessary. An example result:

cumberland generated kml on google earth

You could use this tool to load up a subset of your data from postgis or sql server and pass it on to a customer with your own specialized symbology. A future improvement would be to allow importing of kml so that people could make edits in google earth and then load the changes into your database.

You'll need to build from SVN to get this.

Cumberland: TMS / OpenLayers support

November 22nd, 2008

The TileProvider class and the tilepyramider tool have been updated to create tiles for the un-official Tile Map Service specification. The main reason for this is to use OpenLayers.

Check out a demo.

It's pretty simple to set up. There is a new TileConsumer, TileMapService. tilepyramider has a new 'consumer' option you can set to "tms", and it will handle the rest, including creating the tilemapresource.xml file. (There are some properties in this file you'll need to update manually if you want it to be accurate)

OpenLayers

Currently, a TMS has to have an EPSG of 4326 and worldwide extents to work in OpenLayers (source). This means you can't clip to a certain region, so the above example generated a lot of empty tiles. Given that your map is projecting to epsg 4326, you'll run something like:

tilepyramider.exe -e=-180,-90,180,90 -o=/home/scottell/Desktop/tms -c=tms -x=6 ../../../Cumberland.Tests/maps/mexico.xml

Then you'll deploy the resulting 'tms' directory to your website and wire up a page with the following script:

function initialize()
{
    var options = {
        maxExtent: new OpenLayers.Bounds(-180, -90, 180, 90),
            numZoomLevels: 6,
    };

    var map = new OpenLayers.Map('map', options);
    var layer = new OpenLayers.Layer.TMS( "Mexico", ["http://localhost/~scottell/"],
    {
        layername: 'tms',
        serviceVersion: '',
        type:'png',
    } );
   
    map.addLayer(layer);
    map.setCenter(new OpenLayers.LonLat(-105, 24), 4);
}

Add a script reference to OpenLayers:

<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript"></script>

Call initialize() on load and add your map div:

<body onload="initialize()">
    <div id="map" style="width: 500px; height: 300px"></div>

That's it! You'll need to build from SVN to get this.

ineffective mapping: an example

November 17th, 2008

So I was about to go on a little hike with my parents, and this park (like many) has a map billboard thing:

trail map billboard

Which is good, but we walk up to it and I'm just confused. What am I a looking at? It takes me a second to orient myself. Here is the map (complete with my annotations):

trail map with annotations

There a couple of issues:

  1. You are looking at the map which is to the east, but the map is north/south, so it is disorienting. This could be fixed by moving the billboard so that you are facing north when you read it.
  2. The trail which you are probably going on is not actually on the map!. Doh!

cumberland 0.1

November 14th, 2008

I posted cumberland 0.1 on the project page.

I think it's a really elegant and simple API for drawing maps, and it's also under the awesomest, most reasonable, licensing scheme there is.