Tag Archives: Javascript

Restricting Zoom with Multiple OL Basemaps

DistrictBuilder logoAs David recently posted, our team has been hard at work implementing DistrictBuilder, where we’ve been investing a great deal of effort on both performance and usability. One feature we added in the spring was the addition of basemaps to the user interface. Before this addition, users labored over drawing the perfect district configurations without a whole lot of context of the surrounding environment (e.g. roads, water boundaries, etc.). When the time came to add a basemap to the application, it didn’t feel right restricting it to a single type of map, or even a single provider. We wanted to allow for users to have the choice to select the best map for the task at hand. Could an application promoting democracy really have it any other way?

We set out to support several base map options as well as any combination of options, including:

  • Bing Maps (satellite, roads and hybrid)
  • GoogleMaps (satellite, roads and hybrid)
  • ArcGIS Online (any of several maps)
  • OpenStreetMap

Since DistrictBuilder needs to be flexible enough to meet the needs of users and administrators in a variety of situations, we decided on a two step approach to basemap configuration. First, the administrator specifies, in the configuration file, which of the combinations of map providers and map types are allowed to be selected. Then DistrictBuilder presents all of the configured options to the user, where they can be toggled among at any time while a plan is being viewed or edited.

Here’s an example of an instance configured with an OpenStreetMap road layer, a Bing hybrid layer, and a Google satellite layer:

Road, Hybrid, and Satellite

Here’s another example with only road layers — one for each of the three configured providers:

Roads for three vendors

DistrictBuilder currently allows the configuration of basemaps using permutations of each of the three vendors and three map types described above. Adding more options is a relatively easy task, however. With the launch of Fix Philly Districts, we wanted the basemap colors to be slightly more muted than the above options, and ended up adding support for the ArcGIS Online World Topographic Map. We also experimented with the Google Maps V3 custom styling API, which looked great, but introduced performance problems when panning and zooming (animations).

There were, of course, some hoops that needed to be jumped through in order to get all of these basemaps behaving correctly on the same map, which will be discussed below. I’ve extracted the logic required to do so into a small demo that can be viewed/downloaded here. The demo has also been embedded into this post, and can be interacted with without going anywhere:

Zoom Levels

Many of the challenges that needed to be overcome to get this working correctly were brought about because we needed to restrict the zoom levels to the area at hand. We wanted to eliminate superfluous zoom levels to ensure the user was always operating within the appropriate boundaries (note: it is not done in this demo, but in DistrictBuilder we also restrict the extent with the ‘restrictedExtent’ map parameter, so users can’t even pan outside of the area).

One difficulty with setting zoom levels on the different layers is that the layers don’t use zoom parameters consistently. In Bing (the VirtualEarth layer), minZoomLevel and maxZoomLevel are needed. In Google, minZoomLevel is needed, but it requires numZoomLevels instead of maxZoomLevel. And in OpenStreetMap (the OSM layer), well…no combination of those seem to work — we needed to slightly modify the XYZ layer (OSM’s base class) to allow maxResolution to be changed based on the minZoomLevel. To see how this is done, view the demo source. With that change in place, the list of required layer parameters is as follows:

  • Bing – minZoomLevel, maxZoomLevel, projection, sphericalMercator, maxExtent
  • Google – numZoomLevel, minZoomLevel, projection, sphericalMercator, maxExtent
  • OpenStreetMap - numZoomLevel, minZoomLevel, projection

Coordinate Systems

We also faced some problems related to coordinate systems. DistrictBuilder uses GeoServer and GeoWebCache to serve up WMS layers. The coordinate system of our data is one version of the the ever-changing “Popular Visualization CRS / Mercator” projection. We needed to match up the OpenLayers projection to the one used on our data, or else we were seeing slight offsets on our overlays. Unfortunately, the ‘projection’ layer parameter isn’t always used within the layers correctly. For example, any layer using the SphericalMercator class gets its projection automatically hardcoded to 900913. We needed to make a slight modification to the SphericalMercator class to allow the ‘projection’ parameter to carry through. This can be seen by viewing the demo source.

Bonus: Math Time!

One interesting part about implementing zoom restriction was that we needed it to work in any instance of DistrictBuilder — from large states to small towns, which may have vastly different extents. Instead of having an administrator figure out the proper minimum zoom level, we calculate it automatically based on the extent, which requires a little bit of basic algebra.

For Philadelphia, the extent of our area is:

[-8397913.926216, 4842467.609439, -8329120.600772, 4895973.529229]

In DistrictBuilder, we calculate this dynamically on the server side (using Django) by filtering all of the geounits in the database and calling the ‘extent’ function on the query set. For the demo, this is hardcoded. Here’s how to transform this extent into a Spherical Mercator minZoomLevel:

  • Find the width of the area in meters.
var studyWidthMeters = extent[2] - extent[0];
  • Find the width of the map in pixels. In the demo, this is hardcoded, because we are setting the div size of the map. In DistrictBuilder, the map takes up the whole screen, and this value is calculated on the fly based on the size of the div in which the map occupies.
var mapWidthPixels = 450;
  • Find the map resolution, or meters per pixel.
var resolution = studyWidthMeters / mapWidthPixels;
  • Find the maximum map resolution. In Spherical Mercator, the maximum resolution is one 256×256 tile taking up the entire circumference Earth. So dividing the circumference of the earth (~40,000km) by 256 gives us the maximum meters per pixel, which is a constant.
var maxResolution = 156543.033928;
  • Spherical Mercator zoom levels work like a pyramid. Each zoom breaks the current tile up into a 2×2 group of 256×256 tiles, essentially halving the resolution each time. Therefore, finding the resolution at a given zoom level looks like this:
maxresolution / 2^zoom = resolution
  • We know the resolution and max resolution already, and need to find the zoom:
zoom = log(maxresolution/resolution)/log(2)
  • Or in javascript:
var minZoom = Math.log(maxResolution / resolution) / Math.LN2;

When is seven days not a week?

I’ve been using Date.js on a project, and it’s an awesome library. I especially love when someone else handles dates for me (am-i-right fellow developers? ;) ), and everything was going great until we discovered a rare time bug. I had forgotten something very important regarding time: it’s relative.

We’re not writing guidance software for rockets, so we don’t have to deal with relativity in the Einstein sense, but relative in a more day-to-day and somewhat artificial sense. Although I’m not exactly sure how Benjamin Franklin really felt about daylight savings time, it certainly makes programming a little trickier for us Philadelphians.

Here is an example for those following along in firebug:

var start = new Date('2010/01/01');
var end = new Date('2011/01/01');
var diff = (new TimeSpan(end - start)).days;

for(var i=0;i<diff;i++) {
    var today = new Date(start).add(i).days();
    var plusSeven = today.clone().add(7).days();
    var oneWeek = (new TimeSpan(plusSeven - today));

    if ((oneWeek.days != 7) || (oneWeek.hours != 0)) {
        console.debug("Time Warp: ", today);
    }
}

Say you made some bit of code that generated a date range by adding or subtracting some number of days off a date, you’d have a bug from March 8th through March 14th, and then it’d be gone until November 1st through the 7th! Not only is this sneaky, it’s hard to predict! Although this isn’t really a bug in Date.js, it requires us to change how we think about time.

The recommended Googling didn’t provide a straightforward, and rock-solid technique for identifying and adjusting for this effect in any given time zone.

So I made one:

First, a quick example:
(my apologies if this is obvious or full of mistakes)

//Lets say you wanted to setup some DateTime controls
//with a rolling date range, lets say using an offset in days
//with Date.js you'd probably do something like this:

var offsetDays = 7;
var start = new Date('2010/03/09');
var end = start.clone().add(offsetDays).days();

//so far so good, we got the intended date:
//Tue, March 16th, 2010
//but we want to update and store the rolling date range,
//so lets get that range back out:

// ... snip! ...

var diff = (new TimeSpan(end - start));
if (diff.days != offsetDays) {
    //daylight savings time caused time to 'spring ahead',
    //and our 7 days is instead 6 days 23 hours
    console.debug('user changed date');
}

We’re going to need a simple function that gets a TimeSpan that is adjusted for the daylight savings time adjustment:

// gets the difference between two dates,
// adjusting for daylight savings time.
var getDSTAdjustedDiff = function(startDate, endDate) {
    var startOffset = startDate.getUTCOffset();
    var endingOffset = endDate.getUTCOffset();

    if (startOffset > endingOffset) {
        startDate = startDate.clone().add({hours: -1 });
    }
    if (endingOffset > startOffset) {
        endDate = endDate.clone().add({hours: -1 });
    }
    return (new TimeSpan(endDate - startDate));
};

If we revisit our example from before:

var start = new Date('2010/01/01');
var end = new Date('2011/01/01');
var diff = (new TimeSpan(end - start)).days;

for(var i=0;i<diff;i++) {
    var today = new Date(start).add(i).days();
    var plusSeven = today.clone().add(7).days();
    var oneWeek = (new TimeSpan(plusSeven - today));
    var adjustedDiff = getDSTAdjustedDiff(today, plusSeven);

    if ((oneWeek.days != 7) || (oneWeek.hours != 0)) {
        console.debug("Time Warp: ", today);
    }
    if ((adjustedDiff.days != 7) || (adjustedDiff.hours != 0)) {
        console.debug("Adjustment fail!: ", today, adjustedDiff );
    }
}

If everything went well, this code block should run and run without the second block ever firing. I hope this helps someone else out there avoid this pitfall, now if I can just figure out time zones…

jQuery, OpenLayers.Layer.Vector and IE8

One of the very first things you learn about OpenLayers is the importance if initializing maps in either the body tag’s onload event or in script tags at the bottom of the page. The basic reason for this is that many of the OpenLayers components need a page’s dom to be complete before it starts adding it’s own dom structure. The onload event is thrown after the browser is finished loading everything else, and the bottom of the page naturally gets rendered last, so these two places tend to work the best. Makes sense right?

If you’ve been using additional frameworks, like  jQuery ( or EXT or Django or Drupal or whatever) you’ve learned to rely on that framework’s onready event, or its rough equivalent.  This event tends to be thrown before the onload event so that the frameworks don’t need to wait for every last image and tag to be loaded. In theory, this could be a very problematic place to put OpenLayers map initialization code. The dom elements that OpenLayers is looking for may or may not be there, and there’s no way to tell. In practice, most browsers, and most OpenLayers components, work fine when initialized in a framework’s onready event. One layer in particular (OpenLayers.Layer.Vector) will not play nice with one particular browser (IE8) if initialized in a framework’s onready event. The vector layer depends on the document object’s namespace attribute, and this attribute simply isn’t always available before IE’s onload event (which is called after the onready event). As you can imagine, this was one headache I wish I could have avoided.

Thankfully, there is a fairly simple fix. Instead of the usual convention of :

$.ready(function() { init_my_map(); });

Add a stand-alone script tag to the extreme end of the body content on the pages where you need a vector layer:

<script>
    init_my_map();
</script>

Scriptable JSLint

In order to build our Azavea Minifier (discussed in my previous post on JavaScript build process), we needed to be able to run JSLint from a script.

If you search, you’ll find references to the versions of JSLint available on jslint.com for Rhino and for Windows Script Host, but those links now give you a really impressive 404.  They are hosted instead as files on the JSLint Yahoo Group (membership required).

Alternatively, you may now download a scriptable version of JSLint here, on our Commons page (no sign-in required).

This version was tweaked to run in both Rhino and WScript (WScript on Windows is faster than Rhino, though Rhino will run on Windows). We created it because we need a single version that works in various build environments, some on Windows and some on Linux.

Happy Linting!

What’s your JavaScript build process?

JavaScript is a piece of cake, right?  Step 1: Write it.  Step 2: It runs on the client.  What else is there to do?

Well, a couple of things.  The first huge time saver is to run your code through JSLint.  JSLint reports all kinds of bad practices that are worth considering, but even more helpful, it reports syntax errors that you won’t notice until you try to run your code in a browser.  Getting these types of errors reported immediately saves a lot of time.

Next, it’s a good idea to merge as many JavaScript files as possible into one (same with CSS).  When writing the code it is often handy to have lots of small files (one per widget for example), but that adds overhead when a user visits your page and their browser has to make a bunch of separate requests.

The last thing you probably want to do is minify your code.  Removing all the whitespace, using shorter local variable names, etc. can shrink your file sizes by more than half, but also make them completely unreadable.  So for the best of both worlds, write clean nicely formatted code (and save it in your repository) but then run it through a minification tool before using it on your site.  There are several available, but we settled on the YUI Compressor because it is easy to use, cross-platform (written in Java) and also can compress CSS files.

But now this sounds like a lot of steps, right?  So allow me to introduce… the Azavea Minifier!  This is a python script that reads a simple config file and then performs all three steps for you, for easy use in your build process.

Here’s an example (all example files are included in the Minifier zip file):

Let’s say you have the following tree of input files:

src/ui.js (12kb)
src/images/banner-rtn.jpg
src/images/commonspace-banner.jpg
src/images/banner-ph.jpg
src/css/typography.css (10kb)
src/css/view.css (0.6kb)
src/css/style.css (30kb)
src/css/images/azavea-logo.png
src/css/images/azavea-homepage-hunchlab-banner.jpg
src/crypticfilenames/sfga.js (9kb)
src/crypticfilenames/ccm.js (2kb)
src/crypticfilenames/css/ccm.css (1kb)
src/crypticfilenames/css/images/banner-walkshed.jpg

All nicely arranged for development, but not so handy for deployment.  So let’s minify it. First, create a config file.

The default config file name is “minifier.conf”, though you can specify a different file on the command line if you wish.  The format consists of sections of files to be merged, with a header that is the new output file, like this:

[output/output.js]
input/input1.js
input/input2.js

[output/output.css]
input/input1.css

You can also use the minifier to simply copy files to an output directory, which can be useful when you have some random images or whatever that aren’t used directly by the CSS.

[output/outputdir]
input/somefiletocopy.txt
input/someotherfile.jpg

In our case, we have JavaScript code and CSS and related images scattered around our source tree, so our config looks like this:

[output/cryptic.js]
src/crypticfilenames/ccm.js
src/crypticfilenames/sfga.js

[output/main.js]
src/ui.js

[output/all.css]
src/css/style.css
src/css/typography.css
src/css/view.css
src/crypticfilenames/css/ccm.css

[output/images]
src/images/banner-ph.jpg
src/images/banner-rtn.jpg
src/images/commonspace-banner.jpg

Except there’s one problem: ui.js is chock full of JSLint errors, and I just downloaded it from somewhere and don’t intend to rewrite it. So let’s tweak that section so it won’t get JSLint run against it:

[output/main.js] --nolint
src/ui.js

Now run minifier.py, and your output should look like this:

output/all.css (43kb)
output/all-min.css (33kb)
output/cryptic.js (12kb)
output/cryptic-min.js (6kb)
output/main.js (12kb)
output/main-min.js (6kb)
output/images/azavea-homepage-hunchlab-banner.jpg
output/images/azavea-logo.png
output/images/banner-ph.jpg
output/images/banner-rtn.jp
output/images/banner-walkshed.jpg
output/images/commonspace-banner.jpg

Note that the minifier was kind enough to relocate any images it found referenced in the CSS files to the same location relative to the new CSS file (CSS paths are relative to the CSS file, not the page).  Images it can’t find will generate a warning like this:

warning : Minifier can't find the file for this url referenced in the CSS: /foo/images/bullet-sm-gray.png

I left a few errors in the example JS so you can see the JSLint output as well.  Currently it is formatted like this:

/foo/example/src/crypticfilenames/ccm.js: error JsLint : 28
line 18 character 28: Expected '===' and instead saw '=='.
if (ccm_siteActivated == false) {

(Which happens to be the format that Visual Studio will detect and display nicely in its error window.)

Note that in the output there are two versions of each output file:

file.ext
file-min.ext

The first one is what’s produced by merging, but not minifying, your files.  This allows you to switch back and forth easily between the non-minified version when you need to debug (because minified files are pretty unreadable) and the minified version for production.  The merged file has comments inserted indicating where each source file starts and ends, so after you find the bug you can easily fix it in the original file.

I hope you find this helpful!  Feel free to offer any suggestions or bug reports.

Download the Azavea Minifier

Ubiquity Firefox Plugin

I have been following the Ubiquity project from Mozilla Labs, and I gotta say, it’s pretty rad. If you are a Javascript Ninja or an aspiring one, Ubiquity can make your web surfing super slick.

I slapped together a browser command that I use to search our internal wiki in record time. Sample code included!

» Continue Reading

Distance? Math? What?

A few days ago, I became painfully aware that the distance equation we all learn in geometry class, while it works perfectly well on flat, local projections,  is woefully inadequate when we’re talking about global coordinate systems.

In the midst of implementing Google Maps in our up-coming Sajara sample application, I realized that the International Date Line was causing problems. Large problems. Whenever the IDL was in play, I never retrieved the correct results from our database. One large problem was how we defined our bounding box inside Sajara itself. Once that was fixed, however, I realized that something was still wrong. The Nearest-to-Farthest sort was not behaving quite right: it was really doing a Nearest-on-this-side-of-the-IDL-first sort! The IDL was acting like a wall for our distance sort.

distance

Example of linear distance with IDL in play. X = center of map

Here’s what we used before (in sql-speak):

RETURN sqrt(power(@FromX - @ToX,2) + power(@FromY - @ToY,2))

Your standard square-root of difference-squared plus difference-squared. This formula is fast and worked perfectly well so long as I operated under these two criteria:

1. a FLAT projection
2. POSITIVE numbers only

Once either of those things aren’t true anymore, I can’t use the basic distance formula. In my case, both things stopped being true at the same time! The solution? Something fairly math-magical to my mind, but accepted as THE formula to get at the distance between any two global coordinates: the Haversine Formula. Delving into descriptions of the formula brings up concepts such as spherical triangles, the planet’s elipticity, great circles and other things very important to navigation. It’s accurate down to about a meter, which is plenty for anything I’ll be doing with it.

Haversine Formula in Javascript:

var R = 6371; // radius of the earth
var dLat = (lat2-lat1).toRad();
var dLon = (lon2-lon1).toRad();
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
        Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) *
        Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var distance = R * c;

So here is the resulting SQL function that gives me the right distance between any two global coordinates.. basically a direct translation of the above javascript into T-SQL:

CREATE FUNCTION [dbo].[distance] (@FromX float, @FromY float, @ToX float, @ToY float)
RETURNS float AS BEGIN
DECLARE @R AS FLOAT;
SET @R = 6371;
DECLARE @DLAT AS FLOAT;
DECLARE @DLON AS FLOAT;
DECLARE @A AS FLOAT;
DECLARE @C AS FLOAT;
DECLARE @D AS FLOAT;
SET @DLAT = RADIANS(@ToY - @FromY);
SET @DLON = RADIANS(@ToX - @FromX);
SET @A =
SIN(@DLAT/2) * SIN(@DLAT/2) +
COS(RADIANS(@FromY)) * COS(RADIANS(@ToY)) * SIN(@DLON/2) * SIN(@DLON/2);
SET @C = 2 * ATN2(SQRT(@A), SQRT(1-@A));
SET @D = @R * @C;
RETURN @D
END