Adobe SVG Viewer and mousewheel zoom (Part I)

WaterSums 1.1 introduced many new features that were only available with the Silverlight plugin. WaterSums 1.1.1 aims to backport some of this functionality to the Adobe SVG Viewer (ASV) plugin.

One of the desirable features is zooming with the scroll wheel. Thanks to various contributors, this can be easily enough added through Javascript (see http://adomas.org/javascript-mouse-wheel/ for a native Javascript method or http://plugins.jquery.com/project/mousewheel for a JQuery plugin), but once the messages are being received, that is where the fun really starts.

Conceptually, using the mousewheel to scroll is very simple: AutoCAD or Google Maps both use the same method; move the mouse wheel away from your hand to zoom in and towards your hand to zoom out. It can give you a nasty surprise if you were expecting the window to scroll/pan, but once you are used to it, it is quick and easy way of navigating around a map or drawing.

The extra feature that seems obvious but is really rather clever, is that the view should zoom with a focus about where the mouse is pointing. To zoom in on your area of interest, you simply point at it with the mouse and use the mouse wheel. Intuitive and simple.

Then comes ASV. From Javascript, the current location of the cursor can be obtained, and then there is the need to control the zoom within ASV. WaterSums uses JQuery to make some Javascript tasks easier and also the SVG plugin from Keith Wood (http://keith-wood.name/svg.html). ASV makes available the current scale and panning translation of the SVG document being viewed through the currentScale and currentTranslate attributes. By a simple matter of maths, we should now be able to set the necessary scale and adjust the translation so that the mouse pointer continues to point at the same location on the map. To provide it in simplified form, it can look something like: 

// posx, posy are screen coordinates to be used
// as the focus of the zoom operation.
// If they are not set, the centre of the currently visible map
// area will be used.
// The SVG drawing has been previously attached to a div called 'SVGdiv'
svg = $.svg._getSVG('#SVGdiv');
if (svg && svg._svg) {
 
    if (posx == undefined) posx = Math.ceil(svg._width() / 2.0) - 1;
    if (posy == undefined) posy = Math.ceil(svg._height() / 2.0) - 1;
 
    // Get the map location we want to tie to the screen position (posx,posy).
    // We want to keep the screen point (posx,posy) so that it is over the same
    // map location after our change of scale.
    // Function getMapXY converts screen position (posx,posy) into the map
    // coordinates used in the WaterSums network.  (To keep this example simple,
    // the complexity of this function is not shown, but it involves getting
    // the inverse of the screen transformation matrix, and applying it with
    // the ASV SVGPoint.matrixTransform function.)
    var pt = getMapXY(posx,posy);    // SVGPoint in map units
 
    // find the current settings first
    var oldscale = svg._svg.currentScale;
 
    // currentTranslate is in pixels and records how far the
    // top left of the map has been moved from its position
    // with x increasing as the map is moved to the right and
    // y increasing down.
    var svgpt = svg._svg.currentTranslate;
    var oldxoffset = svgpt.x;
    var oldyoffset = svgpt.y;
 
    // first we zoom
    // zooming uses the top left of the view area as the anchor
    if (!factor) factor = 2.0;
    svg._svg.currentScale = oldscale * factor;
 
    // and then we pan to keep pt at the same screen location
    // note that svgpt is a 'live' object - changes made to
    // it are reflected in the map 'immediately'
    svgpt.setX(posx + Math.round(factor * (oldxoffset - posx)));
    svgpt.setY(posy + Math.round(factor * (oldyoffset - posy)));
 
}

Quite a lot of comment, but not much code.  If you wanted to, the code could easily be contained in 10 lines or fewer.

But I still haven't got to the difficult part.  When all this code is in place, the scrollwheel zooms the map in and out as expected - as long as you do it slowly, and as long as you don't have any animation taking place!  If you move the mousewheel too quickly, the map will sometimes 'jump' - scroll vertically.  If you have animation in progress (even the selection of a pipe/pump/valve), the map will probably not zoom at all.  Yes, as long as there is any animation visible, the map will scroll vertically instead of zooming.  You will notice that in the above Javascript, the vertical adjustment is what is done last.  It seems that the rescaling and the horizontal adjustment are simply lost if there is any active animation visible.  If the last two lines are swapped, the map is scrolled horizontally instead.

Experimentation revealed that turning off the animation will work around the problem.  But wait, attempting to turn it off while we do the dynamic zoom and then turn it on again afterwards caused the browser to become unstable.  When testing with IE6, unexpected errors were displayed and the browser would frequently crash.  Attempts at more sophisticated methods of working around the problem did not help - the browser would still crash after using the zoom feature a few times.

So, where to from here?  Obviously, having the mouse wheel zoom at times, and scroll/pan at other times was not acceptable and neither was frequent crashing.  The only option seemed to be to pause animation whenever the mouse wheel was used for zooming, but not even this would work without problems. Pausing visible animations at that time meant that the first click of the mousewheel would always scroll the map and this was not acceptable either.  The final 'best solution' involves several asynchronous steps and some fancy visibility control.

Another example of how appearances can be misleading.  What seemed a simple feature to add, becomes a lengthy and difficult process.

The next blog entry describes the final solution a little more.