27 March 2013

Enabling basic OpenLayers pinch zooming for Internet Explorer 10 touch events

A phenomenon described by the term ‘webkit monoculture’ is causing quite some concern in the web development community. A lot of web developers are basically coding for webkit and webkit only, or more specifically Safari on IOS. HTML5 and standards are great, but certain parts of the web development stack are moving back to a ‘works on my environment’ status that we just were getting rid of. This phenomenon rears it’s ugly head itself on all kind of places, including in the OpenLayers toolkit that I am using for my work at Vicrea.

For those not familiar with OpenLayers: think Google Maps, but then with real GIS functionality, without commercial licensing, without ads in the map, and without all kind of legal strings attached. Pure open source client side web GIS. With the advent of touch devices its community added some basic touch functionality to it, like pinch zoom. That works very smooth, provided – you guessed it – you work on a web kit based browser. Microsoft, in all its wisdom, has chose to implement touch events in a completely different way. As to why this is, and what exactly is standard or not – that is not exactly my concern here. I am making a web GIS that is not supported my Windows 8 touch devices and Windows Phone 8. That, of course, is unacceptable to me ;-)

So I created a little OpenLayers style control that adds pinch zoom to Internet Explorer 10. It’s pure JavaScript and a little primitive – it’s basically zooming in on the map center, and not on the point between your fingers, but it’s working pretty well IMHO.

It also works pretty simple: upon the activate command, it hooks itself onto two events of the map’s layerContainerDiv – MSPointerDown and MSGestureChanged. The first one is fired at the first touch point going down, the second one when MSGesture recognizes an MSGestureChanged. Important is also setting the map’s fractionalZoom property to true.

OpenLayersWindowsPinchZoom = OpenLayers.Class(OpenLayers.Control,
  {
    autoActivate: true,

    gesture: null,

    defaultHandlerOptions: {},

    initialize: function (options)
    {
      this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
      OpenLayers.Control.prototype.initialize.apply(this, options);
    },

    activate: function ()
    {
      if (OpenLayers.Control.prototype.activate.apply(this, arguments))
      {
        if (window.navigator.msPointerEnabled)
        {
          this.map.fractionalZoom = true;

          this.gesture = new MSGesture();
          this.gesture.target = this.map.layerContainerDiv;
          var self = this;

          this.gesture.target.addEventListener("MSPointerDown", function (evt)
          {
            self.gesture.addPointer(evt.pointerId);
          });

          this.gesture.target.addEventListener("MSGestureChange", function (evt)
          {
            // Make scale result smaller to prevent high zoom speeds.
            if (evt.scale !== 1)
            {
              var scale = 1;
              if (evt.scale > 1)
              {
                scale = (evt.scale - 1) / 4 + 1;
              }
              else
              {
                scale = 1 - ((1 - evt.scale) / 4);
              }
              // map.zoomTo is buggy as hell so I use this convoluted way to 
              // calculate a new zoom area
              var resolution = self.map.getResolutionForZoom(self.map.zoom * scale);
              var bounds = self.map.calculateBounds(self.map.getCenter(), resolution);
              self.map.zoomToExtent(bounds);
            }
          });
        }
        return true;
      }
      else
      {
        return false;
      }
    },

    CLASS_NAME: "OpenLayersWindowsTouch"
  }
);

The MSGestureChanged event has a scale, which is a number either bigger (zoom in) or smaller (zoom out) than 1. After that it’s simply calling some standard map functions to calculate the new display area and fire away. The most logical one to use would be map.ZoomTo, but that completely messes up the map tile layout after a few times and this workaround via the resolution calculation prevents that. I assume there is a bug in the zoomTo code.

There is another detail – to prevent Internet Explorer to handle the zoom events itself, you have to mark the div in which the map will come with css style:

-ms-touch-action: none

I did that inline for the sake of simplicity ;-)

<div id="map2" class="smallmap" style="-ms-touch-action: none"></div>

As for creating the map with the control enabled: this is a normal map with just the standard controls:

map1 = new OpenLayers.Map('map1', 
	 {controls: [new OpenLayers.Control.Navigation(), 
                       new OpenLayers.Control.PanZoomBar()], 
	  numZoomLevels: 15});
while the second one sports my new control as well:
map2 = new OpenLayers.Map('map2', 
	 {controls: [new OpenLayers.Control.Navigation(), 
		  new OpenLayers.Control.PanZoomBar(),
		  new OpenLayersWindowsPinchZoom()], 
	  numZoomLevels: 15});

imageAnd that’s all there is to it. For the OpenLayers purists: yes, I am aware that I don’t implement destroy and potentially create memory leaks. I just wanted to kick off IE10 support. I hope the ‘real’ OpenLayers developers do better and now start supporting IE10 by themselves ;-)

I have made a little live demo site which looks like showed on the left. You can watch it live here and download a zip file containing all the necessary file in one go here.

The control has been tested successfully on a Nokia Lumia Windows Phone 8, a Microsoft RT and a Microsoft Surface Pro.

2 comments:

Unknown said...

Hey this looks good and interesting, many thanks to you, but may I ask why is it not included in the OpenLayers already by default? Have you contacted the community? I'd rather run "git pull" than manage extra js files of my own...

Joost van Schaik said...

@Unknown They wanted to solve it in a different way. I basically haven't put time into wrapping my head around GIT and this was my simple low impact DIY solution.