Viewport Viewer
Introduction
Some time ago I picked up a copy of Pragmatic Ajax, and in the book they have a chapter titles "Creating Google Maps". It sounded like a cool project, so using the book to guide me I created my own version of Google Maps. The resulting application was fun to see, but it wasn't very stable. A couple of weeks ago I got to thinking about that old project, and started writing a new one, but this time in GWT. The result was far more than I had expected, and I made a widget out of it. I call it ViewportViewer, and this article explains its capabilities and how to use it. As with all projects on this site, the API isn't considered final, and I humbly request your feedback on the API (iamroberthanson at gmail dot com).
Before we get into the code, lets discuss the function and parts of the ViewportViewer. Its function is to present a viewport to some image that is larger than the viewport area. The view area, where the image is held, may be made up of a single image or a set of image times. A viewport renderer is responsible for taking the view image(s), and displaying them in the viewport.
So far so good, but there are two other objects in the system that are critical to its flexibility. The first is the viewport model. The model contains information the current position of the view within the viewport, and the current magnification level. The model is instantiated outside of the viewport, so you can share the same model between viewports, or add a listener to the model for you own use.
The second critical part of the system is the 0tile properties object. The tile properties provides the renderer information about the number of tiles in the view, and maps a tile to an actual file name. This allows you to have different sets of images for different magnification levels, with each set having a different number of tiles. For example, at 100% magnification you may have 100 tiles that can be displayed, but at 25% you may only have 25.
Instantiating the ViewportViewer
The example code provided here is the code used to generate the demo. You may want to take a look at the demo and use it for reference as we cover the APU. We start with a simple EntryPoint class, and the skeleton of the demo application.
public class ViewportViewerMain implements EntryPoint { private ViewportViewer viewer; private ViewportModel model; public void onModuleLoad () { TileProperties tileProps = new TileProperties() { // todo }; model = new ViewportModel(3500, 2800); viewer = new ViewportViewer(model, tileProps, 500, 500); RootPanel.get().add(viewer, 290, 30); final VerticalPanel status = new VerticalPanel(); status.setWidth("260px"); RootPanel.get().add(status, 10, 30); Grid controls = new Grid(3, 2); controls.setWidget(0, 1, lblCoordX); controls.setWidget(1, 1, lblCoordY); controls.setWidget(2, 1, lblZoom); status.add(controls); model.setCenterXY(1710, 1460); model.setCurrentPercentSize(1.0); } }
First we create a TilesProperties instance and a ViewportModel. The ViewportModel arguments specify that the view area is 3500 by 2800 pixels at 100% magnification. Both the tile properties and model are passed to the ViewportViewer constructor along with the size of the viewport, 500 by 500 pixels. Other variations of the ViewportViewer constructor include the ability to disallow dragging the view area and passing a custom renderer. We then add the viewport viewer, status panel, and status display labels to the page.
Implementing a ViewportModelHandler
The last two lines of the onModuleLoad() method set the coordinates to center in the viewport and the magnification level. Setting the model properties will in turn trigger listeners attached to the model. This is how the zoom buttons on the demo work, they simply call model.setCurrentPercentSize() to alter the magnification level. In the demo application the EntryPoint is one of the objects listenting to the viewport model, allowing it to update the status fields. Below EntryPoint class with the model handler included.
public class ViewportViewerMain implements EntryPoint, ViewportModelHandler { // .... public void onChange (ViewportModelEvent event) { switch (event.getEventType()) { case ViewportModelEvent.MOVE_EVENT: lblCoordX.setText("X: " + event.getModel().getCenterX()); lblCoordY.setText("Y: " + event.getModel().getCenterY()); break; case ViewportModelEvent.ZOOM_EVENT: lblZoom.setText("Z: " + (int)(event.getModel().getCurrentPercentSize() * 100) + "%"); break; default: break; } } }
The event object passed to the handler includes information about what field was updated in the model, and the model object itself. Note that the model is never read-only, so the handler could modify the model, which in turn would trigger another handler event to all listeners.
Using a Shared Model
Next we want to add that mini-viewport that you see in the demo. If you tried the demo you probably realized that the two viewports share the same model, but there is one problem with that. The magnification level is part of the model, so to have two viewports share the same coordinated, but at different magnification levels, you need to use the DelegatingViewportModel. The DelegatingViewportModel allows you to create a new model that is based on a different model, allowing you to override methods as needed.
DelegatingViewportModel modelTenPct = new DelegatingViewportModel(model) { public double getCurrentPercentSize () { return 0.10; } }; ViewportViewer miniViewer = new ViewportViewer(modelTenPct, tileProps, 150, 150); RootPanel.get().add(miniViewer, 630, 40);
Here we create a new model based on the original, and override the getCurrentPercentSize() to set a static magnification level of 10%. We use this new model to create the mini-viewport and add it to the page.
Building the TilesProperties
The last piece, and perhaps the one that requires the most work, is writing the TilesProperties implementation. There are five methods that need to be implemented; getViewHeightInTiles(), getViewWidthInTiles(), getTileHeight(), getTimeWidth(), and getFilename(); The first four methods recieve the current magnification as an argument, allowing them to alter the number of tiles and the tile dimensions based on the magnification. In the code below you can see how these methods return values for four different sets of tiles (20x20, 10x10, 6x6, 1x1) depending on the magnification.
TileProperties tileProps = new TileProperties() { public int getViewHeightInTiles (double currentPercentSize) { if (currentPercentSize == 0.10) return 1; else if (currentPercentSize == 0.25) return 6; else if (currentPercentSize == 0.50) return 10; else return 20; } public int getViewWidthInTiles (double currentPercentSize) { return getViewHeightInTiles(currentPercentSize); } public int getTileWidth (double currentPercentSize) { if (currentPercentSize == 0.10) return 350; else if (currentPercentSize == 0.25) return 146; else if (currentPercentSize == 0.50) return 175; else return (int) (175 * currentPercentSize); } public int getTileHeight (double currentPercentSize) { if (currentPercentSize == 0.10) return 280; else if (currentPercentSize == 0.25) return 116; else if (currentPercentSize == 0.50) return 140; else return (int) (140 * currentPercentSize); } { String pieceNum; if (currentPercentSize == 0.10) pieceNum = "10_1"; else if (currentPercentSize == 0.25) else if (currentPercentSize == 0.5) else return "images/tower_bridge_" + pieceNum + ".jpg"; } };
The final method, getFilename() receives both the magnification and the tile coordinates as parameters. The filenames we use for the 100% tiles are tower_bridge_[num], where [num] is a number from 1 to 400 (20x20 grid is 400 tiles). The other three tile sets include their magnification as part of the filename. For example tower_bridge_50_25, is the 25th tile (x=5, y=2) in the 50% magnification set. Because you can have complete control of the tile filenames you could, for instance, target a servlet on the server instead of static images, to allow for dynamic tile generation.
Other Uses
So, what is the ViewporViewer good for? I think the demo shows how it can be used to allow a user to explore a set of tiles that together make up a very large image. The mini-viewport is an example of how you can use a second viewport to provide a high level view of the topic. This could be good for detailed star maps, microscope slides, or any other highly detailed image.
Because you can control the model from outside the viewer it is possible to link a static overview image with the viewport. You could allow the user to click the static image, perhaps of the world, and move the viewport to the specified location. You could even use an animation loop to have the viewport appear to scroll over to a new position. This is the same functionality that is provided by Mootools' Fx.Scroll effect. These are just a few ideas, hopefully you can think of others.
As usual, feedback is greatly appreciated. You can email me directly at iamroberthanson at gmail.com.
- Login to post comments
GWT Sandbox