Improving khtml.maplib

From OpenStreetMap Wiki
Jump to: navigation, search

This project has been approved for the Google Summer of Code 2011.

Its general purpose is to improve the OpenSource JavaScript Slippy WebMap Library khtml.maplib.

As this GSoC-project is already finished, this site will not be maintained any more. The API and the code for khtml.maplib may still change, so please see Simple_marker_API for more up to date information or the JSdoc API-documentation that comes in the tar-ball with khtml.maplib.

Project Ideas

  • Developing an easily integratable marker system for khtml.maplib.
  • Design a webpage for the khtml.org server, where users can design their maps via drag & drop, place markers and automatically get the code generated to integrate the webmap into their website.


Already existing interface

There is already a very simple implementation for a marker system, but it is still very inconvenient for the user and has a lack of features.It only lets the user add dom elements as an overlay.

For an example see section Markers in khtml-help.

Improvements

  • increasing features for markers (make them dragable, clickable, infobubble with text, ...)
  • improving interface, making it easier to integrate into own website


Approach

  • learning Object Oriented JavaScript
  • researching marker interfaces from other webmap-services (Google Maps, Bing, Yahoo, ...)
  • developing own interface for markers


Prospective Timeline

  • April 26th: Starting intensive research when I am accepted for the project. Involving in the community, collecting ideas, discussing with mentor, summarizing, …
  • May 30th: Starting development. Writing code, testing performance, ...
  • July 2nd: When all my exams are over, I will have plenty of time to work on the project. Letting others test my code.
  • July 11th: Submit intermediate results for evaluations.
  • August 8th: Starting final testing and debugging. Finishing documentation.
  • August 15th: Hand In date.
  • August 22nd: Deadline

Actual Timeline

  • April 26th: Started research phase.
  • June 15th: Started programming phase.
  • July 2nd: All exams finished. Working heavily to catch up with timeline.
  • July 11th: Had a great progress so far (see updates). Submitted evaluations. Think I have caught up with the original timeline.
  • August 8th: Tested with many different browsers on different systems.
  • August 15th: Started final documentation work.

Project Progress

Update 1: May 15th

Research

  • I did a lot of online research on marker APIs of
Google Maps Markers,
Yahoo YMarkers and
BING Pushpins.
  • I read some ebooks about
OpenStreetMaps,
Mapscripting 101,
Google Maps API V3 and
Object-oriented JavaScript.

Preliminary work

  • I started a mailinglist especially for khtml: kthml.maplib@freelists.org
( to join, send an email with "subscribe" in the header to khtml.maplib-request@freelists.org)



Update 2: June 30th

Due to heavy workload on other projects at university, I could not start as early as planned, but now as all my exams are over, I am already really into coding and testing. I am now about 2 weeks behind the planned timeline.

  • I used khtml.maplib for another project at my university. It is called SharedPainting and a form of collaborative painting tool. khtml is used for pinch/zoom navigation in the PaintingOverview. Each drawing-area is one tile in the form of map-tiles. Therefore I crawled really deep into the sourcecode of khtml and found nearly by chance some bugs, that I fixed.
If you are interested, goto SharedPainting with you iPhone or iPod touch.
On desktop PCs use this link
Note: It is still very buggy and only optimized for iPhone-screen, since it's supposed to be a mobile application.

Bugfixes

  1. When using touch-zooming with 2 fingers and you reach the maximum zoom level, the map suddenly jumps a few centimeters and jumps back if you drag your fingers towards each other again.
    The problem was here. The zoomDelta is set to a fixed value (1) that causes the jump. I used a flag to store the old value.
    1.         if (evt.touches.length == 2) {
    2.             this.mousedownTime = null;
    3.             var X1 = evt.touches[0].pageX;
    4.             var Y1 = evt.touches[0].pageY;
    5.             var X2 = evt.touches[1].pageX;
    6.             var Y2 = evt.touches[1].pageY;
    7.             var Distance = Math.sqrt(Math.pow((X2 - X1), 2) + Math.pow((Y2 - Y1), 2));
    8.             var zoomDelta = (Distance / this.startDistance);
    9.             var zz = this.startZZ + zoomDelta - 1;
    10.             if (zz < 1) {
    11.                 zz = 1;
    12.             }
    13.             if (zz > this.tileSource.maxzoom) {
    14.                 zz = this.tileSource.maxzoom;
    15.                 zoomDelta = 1;
    16.             }
    17.             var x = (X1 + X2) / 2;
    18.             var y = (Y1 + Y2) / 2;
    19.  
    20.             faktor = Math.pow(2, zz);
    21.             var zoomCenterDeltaX = x / faktor - this.width / 2;
    22.             var zoomCenterDeltaY = y / faktor - this.height / 2;
    23.             var f = Math.pow(2, zoomDelta - 1);
    24.             var dx = zoomCenterDeltaX - zoomCenterDeltaX * f;
    25.             var dy = zoomCenterDeltaY - zoomCenterDeltaY * f;
    26.  
    27.             this.moveX = (x + dx) / faktor + this.startMoveX;
    28.             this.moveY = (y + dy) / faktor + this.startMoveY;
    29.  
    30.             var center = new khtml.maplib.Point(this.lat, this.lng);
    31.             this.setCenter2(center, zz);
    32.         }
    I replaced it with:
    1. if (zz > this.tileSource.maxzoom) {
    2.         zz = this.tileSource.maxzoom;
    3.         zoomDelta = this.zoomDeltaOld;
    4. }
    5. else
    6.         this.zoomDeltaOld = zoomDelta;
    Here are the two testcases: before bugfix #1 and after bugfix #1
    Only on touchscreen-devices! Zoom in to maximum zoomlevel with two fingers and watch the map jumping.
  2. Again with touch-zooming and panning. When you first use one finger to drag the map, then apply a second one to zoom and then release one finger and continue dragging with the other one, the map stays still and does not move any more.
    I located the problem here:
    1. this.end = function (evt) {
    2.         if (evt.preventDefault) {
    3.             evt.preventDefault(); // The W3C DOM way
    4.         } else {
    5.             evt.returnValue = false; // The IE way
    6.         }
    7.         window.clearInterval(this.zoomOutInterval);
    8.         this.zoomOutStarted = false;
    9. 	this.zoomOutSpeed = 0.01;
    10.  
    11.         if (evt.touches.length == 0) {
    12.             this.moveok = true;
    13.             if (this.moveAnimationMobile) {
    14.                 if (this.moveAnimationBlocked == false) {
    15.                     var speedX = this.lastMoveX - this.moveX;
    16.                     var speedY = this.lastMoveY - this.moveY;
    17.                     clearTimeout(this.animateMoveTimeout);
    18.                     this.animateMove(speedX, speedY);
    19.                 }
    20.             }
    21.         }
    22.  
    23.     }
    I added this to avoid the problem. It's the same calculation that is done, when a second finger touches the screen, but now I also do it, when the second finger is removed.
    1. if (evt.touches.length == 1) {
    2.         this.moveok = true;
    3. 	this.startMoveX = this.moveX - evt.touches[0].pageX / this.faktor / this.sc;
    4.         this.startMoveY = this.moveY - evt.touches[0].pageY / this.faktor / this.sc;
    5. }
    Here are the two testcases: before bugfix #2 and after bugfix #2
    Only on touchscreen-devices! Drag with one finger, then apply the second one to zoom and then release one finger and try dragging.
  3. When double-tap zooming on a map that's not fullscreen, but only a part of the screen, the map doesn't calculate the offsetLeft and offsetTop of the map and so often zooms the tapped point out of the view, especially when it's near the edge.
    The problem was here:
    1.     this.autoZoomIn = function (x, y, z) {
    2.         //console.log(x,y,z);
    3.         if (this.autoZoomInTimeout) {
    4.             window.clearTimeout(this.autoZoomInTimeout);
    5.         }
    6.         var stepwidth = 0.20;
    7.  
    8.         if (z < 0) {
    9. 		stepwidth = -stepwidth
    10.         }
    11.         zoomGap = false;
    12.         if (Math.abs(z) <= Math.abs(stepwidth)) {
    13.             zoomGap = true;
    14.         }
    15.         //this.hideOverlays();
    16.         var dzoom = stepwidth;
    17.         var zoom = this.position.zoom + dzoom;
    18.         zoom = Math.round(zoom * 1000) / 1000;
    19.         if (zoomGap) {
    20. 		//console.log("---: "+z+":"+zoom);
    21. 			if(z<0){
    22. 			    zoom = Math.floor(zoom);
    23. 			}else{
    24. 			    zoom = Math.ceil(zoom -0.2);
    25. 			}
    26.  
    27. 		//console.log("---"+zoom);
    28.             //dzoom = z;
    29.             dzoom = zoom - this.position.zoom ;
    30.             //console.log("gap: "+dzoom+" : "+zoom);
    31.         }
    32.  
    33.         faktor = Math.pow(2, zoom);
    34.         var zoomCenterDeltaX = x - this.width / 2;
    35.         var zoomCenterDeltaY = y - this.height / 2;
    36.         var f = Math.pow(2, dzoom);
    37.  
    38.         var dx = zoomCenterDeltaX - zoomCenterDeltaX * f;
    39.         var dy = zoomCenterDeltaY - zoomCenterDeltaY * f;
    40.  
    41.         var that = this;
    I substracted the top and left attributes of the map:
    1.         var zoomCenterDeltaX = (x - this.mapLeft) - this.width / 2;
    2.         var zoomCenterDeltaY = (y - this.mapTop) - this.height / 2;
    Here are the two testcases: before bugfix #3 and after bugfix #3
    Only on touchscreen-devices! Double tap on a point at the left edge of the map and watch it move away.
    There is a similar bug with the 2 finger zoom and non fullscreen map, which I was able to locate roughly but could not fix so far.



Update 3: July 7th

During the last week I started implementing a marker-interface for khtml.maplib and played around a little bit, to make it good looking with nice cursors.

Markers

There was already a very simple marker interface, as you can see here and here (scroll down to section markers).
I did a lot of research and comparison on Google Maps, Microsoft Bing Maps, Yahoo Maps and also OpenLayers.
Google Maps has by far the most complex structure in their DOM-tree, but still is the most powerful Maps API with a lot of features.
But for the normal user it is still very easy to implement.


Only few lines of code are needed, to get a moveable standard marker with a shadow for 3D-effect and a nice drag and drop animation.
var marker = new google.maps.Marker({
                position: new google.maps.LatLng(18, 48),
                map: map,
                draggable: drag,
                title: "Marker #1"             
             });
example on Google Maps JavaScript API V3


And only some more lines are needed to get a nice litte infowindow:
google.maps.event.addListener(marker, 'click', function() {
		var infowindow = new google.maps.InfoWindow({
			content: "This is " + this.getTitle() + " and you just clicked it!"
		});
		infowindow.open(map,this);
	});
example on Google Maps JavaScript API V3


First I started to analyze the existing marker interface and soon found out, that using 2 divs where the marker is placed in, is 1 too much (source code). I started playing around with the code and very soon got a working version with only one div (source code).
For testing and comparing the performance of the different maps, I found some nice codes on mappertests.codeplex.com, which I took and modified a little bit, to meet my requirements.
Try the two versions with many markers (>1000) and see the difference in the time:
On my computer i got an perfomance increase when generating the markers of about 10%. Of course this performance tests highly depend on which browser you use and how powerful your computer is.
Compared to the other Maps, it's quite fast, but still lacks features:
Only Google Maps is much faster in this test than the other, because
  1. they don't create DOM-elements for normal markers, but place the images on a canvas ( watch this video for this interesting topic)
  2. when creating draggable markers, they use normal DOM-elements, but the timer stops much earlier than the markers appear. I have not found a solution for this problem so far.


The next step was, to increase the features of the marker and the interface. I added:
  • EventListeners for touch-events,
  • a nice DragAnimation like Google-Markers and
  • the possibility to define a shape for custom markers
Here you can try the current version: markers with increased features
Of course these features have a heavy influence on the performance: performance test with increased features
I designed a nice little marker in photoshop:
Red pin3.png
With the help of this homepage, I also made a shadow for it:
Red pin3 shadow.png
And I decided to embed it as standard marker via data-URI, base 64 directly in the source code. This tool helped me with that.
But since IE6 and IE7 don't support data-URI, I had to do a workaround and provide the images as separate files as well (IE8 works with data-URI).
IE6 also has problems with transparent PNGs, so I had to do another workaround for that. I tried a special "filter"-method Microsoft provides for this case (see here). It worked pretty well, displaying the marker and background on the monitor, but when trying to print the map, the transparent parts went black.
This is what Google did, now see what it looks like, when you try to print a map on IE6:
Google IE6 small.png


I didn't want to look for another workaround for the workaround, so I decided, to create separate files with reduced quality (IE6 supports transparency in 256 color images):
Red pin3 test.png Standardmarkershadow ie.png
They are not the prettiest, but at least they still look good, when you print the map. All other browsers do get the high-res version.


I also added some nice cursors which make it more fun to use, than the normal pointer.

Cursors

At Google Maps, Bing and Yahoo Maps you can see, that they all use this nice little finger, hand and fist mousecursor in the map, when dragging it and hovering over an UI-element or a marker.
I also tried to implement this cursors, which is quite easy if you only work on one browser. But if you start testing it with another one, you will always find a new situation, that you hadn't considered before and that needs a special workaround.
Therefore I did a lot of testing with events, and this page helped me to understand the difference between capturing and bubbeling phase.
Here is the old version and here the new one.
I wrote a little script, that helps me setting the cursor in dependence of the browser:
// set cursor for an object via css-style
// vendor specific cursors
// or via url
setCursor = function( object, string) {
    // Internet Explorer
    if (navigator.userAgent.indexOf("MSIE") != -1) {	// tested on IE6&8
        if (string == "grab")
            object.style.cursor = "url('./hand.cur'), default";
        else if (string == "grabbing")
            object.style.cursor = "url('./fist.cur'), move";
        else
        object.style.cursor = "pointer";
    }
    // others
    else {
        object.style.cursor = "-moz-" + string;			// tested on firefox 3.6
        if (object.style.cursor != "-moz-" + string) {
            object.style.cursor = "-webkit-" + string;		// tested on safari 5 and chrome 12
            if (object.style.cursor != "-webkit-" + string) {
                object.style.cursor = "-ms-" + string;
                if (object.style.cursor != "-ms-" + string) {		// missing, not sure if it works
                    object.style.cursor = "-khtml-" + string;
                    if (object.style.cursor != "-khtml-" + string) {		// missing, not sure if it works
                        object.style.cursor = string;
                        if (object.style.cursor != string){
                            object.style.cursor = "pointer";
                        }
                    }
                }
            }
        }
    }
};


The cursor files for the Internet Explorer, I created with the help of the screenzoom on my mac and a little software called JustCursors.
Cursor Hand Open.pngCursor Hand Closed.png
So far I successfully tested the cursors on Firefox 3.6, Safari 5 and Chrome 12. IE6 and IE8 still have some small bugs, that I am working on.
Other browser are still to be tested. If you find any bugs, please tell me!



Update 4: July 14th

This week I improved the cursors I implemented last week and I also implemented a first infobox based on HTML5 into khtml.maplib. With this progress I think I can say that have caught up with the original timeplan.

Cursors

I did some more improvement on the cursor-code and successfully tested it on IE6 & IE8.
Therefore I had to do a little workaround for the standardmarker, because IE has troubles with the shape area.
I also took out the shape area for touch devices (tested in iPhone 4 and iPad 1 so far), to make the markers better grabbable with the finger.
You can test it: here

Infobox

For the infobox, I did a lot of research again, how the others do it (click on the marker to get the infobox):
Google has again by far the most complicated approach, breaking their infowindow in more than 20 divs styling it with sprites and a nice shadow. But their infowindow also has the most aesthetics compared to the others.
My first approach to the infobox is using HTML5. Of course I am aware that it will not work on older browsers, but I want to concentrate on the core-functionality first and try to improve it later.
The pointer and the close-cross are embedded images again to save http-requests (again this will not work on older browsers, but I will deal with that later):
Pointer2.png Close.png
The rest of the infobox is done with a div with round-corners.
One special thing about the infobox is, that it can be opened on any object on the map, not only markers. Therefore the object only has to provide a position property and maybe a pixelOffset for this position.
An infobox can also be attached to a position without any object.
This works pretty nice as you can see here (click on a marker to open the according infobox).
This was pretty easy for desktop-browsers, but I was facing some problems on touch devices:
  1. The infobox could not be opened, because the click-event was suppressed by the map. So I had to do a manual click-detection with touchstart- and touchend-events.
  2. The close-cross was too small to be touched with a finger, so I had to display it larger on touch-devices.


So you can see it is working, but there are still improvements to make:
  • graphics for non-HTML5 browsers
  • create a shadow for infobox
  • re-centering map when opening infobox
  • rendering of infobox when moving marker with opened box
  • prevent unwanted opens of infobox



Update 5: July 21nd

This week I added the feature to move the map, when dragging a marker to the edge.

Marker

I was facing some minor troubles with the rendering function of the marker. With the old one, the dragged marker was flickering heavily when the map was moved.
The reason was, that the new pixel-coordinates of the dragged marker were always calculated from the gps-coordinates and that was too slow when the map was moved. So I had to differ in the rendering-function between the marker that is currently dragged and the other ones and do a different calculation for the pixel-coordinates.
Here are the two versions. Grab a marker and drag it to the edge of the map to see the difference.



Update 6: July 28th

This week I added the feature to move the map, when an infobox is opened that projects beyond the edge of the map.

Infobox

Here are the two versions. Drag the map so that a markers is near the edge and then click the marker to open the infobox.



Update 7: August 4th

This week I worked on the layout of the infobox to make it compatible to non-HTML5 browsers.

Infobox & Markers

Here are the two versions. The first one is a single HTML5-div with border-radius, the second one consists of 9 divs for edges and corners with an embedded graphic to make the infobox good looking on older browsers. I also removed all of the innerHTML-statements for security reasons and replaced them with standard DOM-manipulation.
I also did a rework on the mapmove-function when dragging a marker to an edge due to bad usability test. The map moved to fast, so I changed the function that it does not increment the speed to a fixed value, but that the speed the map moves depends on the distance of the marker to the edge. The nearer you drag it to the edge, the faster the map moves.


These are the embedded images: corners corners pointer pointer and close-button close-button



Update 8: August 11th

I did a lot of more work on the Infobox and on the marker API to make it compatible to the Google API.

I also found out, that I made a mistake saving the PNG-files in photoshop. The "File" - "Save for Web" - command helped me save file size with the embedded images.

Infobox

The size of the infowindow is now calculated according to the content with a maximum of the mapsize and if the content exceeds the mapsize, a scrollbar is added.
See these two sites for comparison:
also notice the scrolling function and the improvement in the mapmove-function


For more text styling, see these two sites:
  • Google's simple infowindow example: Mount Uluru (click on the marker!)
  • and mine: Mount Uluru (click on the marker and compare!)


I also created a shadow for the infobox with an embedded image. This will not work on older browsers, therefore a workaround would be needed.
The shadow is divided onto 9 divs to match the size of the infobox. Each div contains a part of the same image correctly adjusted to look like one image.


shadow: Infobox Shadow.png
There are still improvements to be done, to make the infowindow work on older browsers (IE6).
I also made another major achievement this week: The infobox is not opening unwanted any more when dragging a marker and is now also moving with the marker as you drag it!
Check it out here!

Marker

If you look at the source-code, you may notice that they are now pretty much the same. Together with my mentor I decided to use the same parameters to make it easy to port the code from google markers to khtml.maplib. You can now use the code directly from sites like this.
This is what it looks like:
var point = new mr.LatLng(50.875311, 0.351563);

var image = new khtml.maplib.overlay.MarkerImage(
       'marker-images/image.png',        // url to image
       new khtml.maplib.Size(20,20),     // size of the image
       new khtml.maplib.Point(0,0),       // origin in a sprite
       new khtml.maplib.Point(10,20)    // anchorpoint on the map
);
       
var shadow = new khtml.maplib.overlay.MarkerImage(
       'marker-images/shadow.png',      // url to image
       new khtml.maplib.Size(34,20),     // size of the image
       new khtml.maplib.Point(0,0),       // origin in a sprite
       new khtml.maplib.Point(10,20)    // anchorpoint on the map
);
       
var shape = {
       coord: [14,0, 15,1, 16,2, 17,3, 18,4, 19,5, 19,6, 19,7, 19,8, 19,9, 19,10, 19,11, 19,12, 19,13, 19,14, 18,15, 17,16, 16,17, 15,18, 13,19, 5,19, 4,18, 2,17, 2,16, 1,15, 0,14, 0,13, 0,12, 0,11, 0,10, 0,9, 0,8, 0,7, 0,6, 0,5, 1,4, 2,3, 2,2, 4,1, 5,0, 14,0],
       type: 'poly'
};
       
var marker = new khtml.maplib.overlay.Marker2({
       draggable: true,
       raiseOnDrag: true,
       icon: image,
       shadow: shadow,
       shape: shape,
       map: map,
       position: point
});
I also added a function to suppress map-zooming when doubleclicking on a marker or a infowindow.
You can play around with some markers here!
I also wrote a little script to place markers via drag and drop onto a map: check it out here!

Map

Last but not least I added some features to the map:
  • zooming out with a double rightclick
  • crosshair cursor when using rect-zoom with shift key pressed
  • suppress contextmenu when right-clicking on the map


Check out this map!



Final

As a final work, I did the JSdoc for the new classes (Marker and InfoWindow) of the map.

Project Completion

On August 22nd I completed this project successfully.

You can download the whole maplib library with this projects work including API-documentation and examples here

I want to thank OSM for the opportunity taking part in GSoC 2011 and my mentor Bernhard Zwischenbrugger for the support.