Background
I am working to add rich annotation functionality to MapKnitter as part of Google Summer of Code (read about my project here: http://publiclab.org/notes/justinmanley/03-18-2014/mapknitter-annotations-using-fabric-js-gsoc-2014-proposal). This is my fourth week of coding. The purpose of this research note is to provide an update on my progress with implementing text annotation for Leaflet.
To trace the discussions that have led me up to this point, you can look back through the following research notes, listed in chronological order.
March 18, 2014 - MapKnitter Annotations Using Fabric.js (GSoC 2014 Proposal)
June 17, 2014 - MapKnitter Annotations Plugin: Preliminary Specification
June 25, 2014 - MapKnitter Annotations Update: L.Illustrate.Textbox
July 2, 2014 - MapKnitter Annotations: Textbox Rotation using CSS Transforms
You can view the code for Leaflet.Illustrate on GitHub and you can view a demo using the version of Leaflet.Illustrate (0.0.1) released along with this research note on GitHub at http://manleyjster.github.io/Leaflet.Illustrate/examples/0.0.1/simple/:
Progress
Since I last posted a research note, I have successfully implemented rotatable textboxes (using CSS transforms as I proposed in a previous research note) which behave correctly and consistently during map zoom and handles for rotating, resizing, and moving textboxes.
In order to implement these features, I have implemented the following classes, many of which are general-purpose extensions of Leaflet core functionality.
L.RotatableMarker
(extendsL.Marker
)Since textboxes (
L.Illustrate.Texbox
) are created as instances ofL.Marker
with HTML icons supplied byL.DivIcon
, this is the class which makes textboxes rotatable. The editing handles rotate along with their companion textbox, and so also derive from L.RotatableMarker.Leaflet.Illustrate.EditHandle
(extendsL.RotatableMarker
)A handle is a marker attached to a companion shape. A handle:
- Is positioned with respect to its companion shape. The position of the handle is given as an offset in pixel coordinates from the center of its companion shape. The offset is given as an
L.Point
object, and its geographic position (latlng) is managed internally. Note that the geographic position (latlng) of the handle usually changes during zooming, since the textbox maintains constant pixel size during zoom. - Responds to drag events to alter the state of the textbox (classes which extend
Leaflet.Illustrate.EditHandle
must implement_onHandleDrag
(for example, inLeaflet.Illustrate.ResizeHandle
).
The key purpose of the
Leaflet.Illustrate.EditHandle
class is to allow applications to create markers which must maintain a fixed pixel position with respect to a givenlatlng
.Child classes
Leaflet.Illustrate.RotateHandle
,Leaflet.Illustrate.ResizeHandle
, andLeaflet.Illustrate.MoveHandle
all inherit fromLeaflet.Illustrate.EditHandle
.- Is positioned with respect to its companion shape. The position of the handle is given as an offset in pixel coordinates from the center of its companion shape. The offset is given as an
I've also implemented several utility methods extending Leaflet core which are worth noting:
L.DomUtil.getRotateString(angle, units)
Returns a CSS transform string corresponding to the given angle. The units (radians, degrees, turns, or gradians) must be given explicitly. This complements the method
L.DomUtil.getTranslateString
already defined inL.DomUtil
.L.DomUtil.setTransform(el, point, angle, disable3D)
Sets the
transform
property on the given HTML elementel
for the given offset (point
), and rotation (angle
). If disable3D is set, then there is no rotation (rotation only works in browsers which support CSS transforms).Leaflet core provides only the method
L.DomUtil.setPosition
, which sets the absolute position of the given element using either the css-transformtranslate
function or, ifdisable3D
is false, setting thetop
andleft
properties on the element. Whendisable3D
is set,L.DomUtil.setPosition
just sets the css-transform property to the newly calculated translate string, and, in the process, overwrites any rotation also set using the css-transform property. This new methodL.DomUtil.setTransform
solves this by setting both the translate and rotate property as appropriate.There are undoubtedly ways to optimize this. Ultimately, we think about translation and rotation as separate operations, and so it would be nice if the abstraction provided by Leaflet supported this. This could be done by implementing
L.DomUtil.setTranslation
andL.DomUtil.setRotation
, each of which would parse the css-transform string and reset only the parameters of the appropriate translate function. Further (relatively simple) optimizations would be to delete the transform functions when unnecessary (i.e.rotate(0,0,0,0)
could be deleted).
Current Challenges and TODO
I've completed the bulk of the work that has to be done to implement textboxes. There are quite a number of small, but crucial tasks that remain before these textboxes are truly usable.
- Add remove hooks for edit handles (implement
onRemove
orremoveHooks
) - Coordinate textbox focus, dragging, and text selection (it is not currently possible to select text in the textbox because of how Leaflet overrides default drag behavior within the map container)
- Fix irritating flicker-on-zoom of the pointer indicating the position of the rotate handle
- Add tooltip text (
L.Illustrate.tooltipText
- the analogue ofL.drawLocal
inLeaflet.draw
) - Allow
L.Illustrate.Textbox
to be modified via styles given in theoptions
object, including default text - Finish implementing
L.Illustrate.Toolbar
. This may involve refactoring parts ofL.Toolbar
.
I've been working for the past few days on eliminating the annoying flickering of the rotate handle pointer on zoom. It's turning out to be quite a thorny problem. Markers (L.Marker
) and polylines (L.Path
) are implemented very differently. The main difficulty is that each marker has an _animateZoom
method which is called when the zoomanim
event is fired. In writing L.Illustrate.EditHandle
, I simply overrode the default _animateZoom
method and reset the position of the handle during zoom in order to maintain constant pixel position with respect to the companion shape of the handle.
Notice how the pointer for the rotate handle (perpendicular blue line on top) flickers on zoom.
Whether the polylines are drawn using Canvas, SVG, or VML (backwards-compatibility with IE), they are scaled during animation using _animatePathZoom
, which is a method of the L.Map
object, not a method of the polyline. The _animatePathZoom
method resets the css-transform of the root <svg>
element, translating and scaling all vector objects on the map by the same amount.
This is not what we want for a pointer.
Pointers should extend paths in the same way that handles extend markers. That is, a pointer is a path whose vertices are defined in pixel coordinates, given with respect to an origin which has a fixed geographic position.
After spending a fair amount of time on this, I think that the best way to approach the rotate handle pointer is to define a new class (L.Illustrate.Pointer
) to encapsulate this functionality. Since each pointer will be scaled and translated differently during map zooming, each will need to be given its own <svg>
root element.
I like this approach because it animates the paths during map zoom in the same way as Leaflet core (no need for SMIL
or javascript animations). I'm a bit concerned that adding many <svg>
(one for each pointer) to the page will negatively impact performance, although this thread (How many SVGs before performance issues?) suggests that, as long as there are tens to hundreds of pointers, that shouldn't be a big issue.
Implementing rotate handle pointers in this way will mean that L.Illustrate.Pointer
can also be used for any kind of pointer meant to call out a single location or identify an annotation (like a textbox) with a single point on the map.
Next Steps
Once the Leaflet.Illustrate plugin is usable, I can begin integrating it into MapKnitter. This will involve designing a toolbar (extending either L.Draw.Toolbar
or L.Toolbar
) to expose the annotation functionality of Leaflet.draw
and Leaflet.Illustrate
to users and then integrating this toolbar into MapKnitter, among other things.
2 Comments
Hi, Justin - looks great, thanks! I like the attention to detail and appreciate your working to get this right. One thought was that it's a little hard to see the handles -- maybe they could be a little bigger? And while editing the text, could we show a slightly darkened background so you can see how large the area is?
Apologies if this is stuff you just haven't gotten to yet and seems obvious, just trying to be helpful!
Is this a question? Click here to post it to the Questions page.
Reply to this comment...
Log in to comment
Looking good! nice to see all the little to dos documented along the way. great update!
Reply to this comment...
Log in to comment
Login to comment.