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). I just started coding this week, on Monday. The purpose of this research note is to provide an update on the approach that I've chosen (see my last research note) for implementing basic annotation functionality.
Implementation Update
Last week, I was trying to decide whether to implement visual annotation for Leaflet using SVG or HTML canvas. After meeting with Jeff, we decided to go with SVG. This was for a number of reasons:
- Leaflet core has built-in classes for handling SVG drawing (L.Rectangle, L.Polyline, L.Circle, etc), and there is already a high-quality drawing plugin, Leaflet.draw, which extends Leaflet core SVG functionality with an excellent UI.
- SVG performance profile is more appropriate. If a user started at a high zoom level, annotations could potentially cover a very large pixel area. Canvas performance degrades quickly with pixel area, while SVG performance is constant with respect to pixel area (SVG performance degrades in proportion to the number of objects).
- SVG text looks better, and text annotation is one of the most fundamental visual annotation features that the MapKnitter community is looking for
- We have more options for zooming/scaling behavior with SVG
Jeff and I talked about developing a Leaflet plugin containing the rich annotation functionality that we are looking for - functionality going beyond what is offered in Leaflet.draw. To this end, I've begun development on a new Leaflet plugin, which I've called Leaflet.Illustrate. Jeff and I threw around a bunch of names: Illuminate, Annotate, Explain, Uncover, Storyteller, Cartographer... We wanted a name that would communicate succinctly that the plugin is designed to allow and encourage users to mark up maps in ways that clarify the purpose and background of the map. Leaflet.Illustrate is a working name.
The first feature I've begun to implement is text drawing. This functionality is contained in the classes Leaflet.Illustrate.Create.Textbox and Leaflet.Illustrate.Edit.Textbox, which, respectively, extend Leaflet.Edit.Rectangle and Leaflet.Draw.Rectangle (both exported by Leaflet.draw).
So far, I have added a 'Textbox' button to the drawing toolbar which users can select to draw a textbox on the map. This will enable text annotation in the style of Powerpoint or Photoshop, in which a user designates a rectangular area in which to enable text entry. The textbox is composed of two elements: an SVG rectangle, drawn using L.Draw.Rectangle which displays the outline of the textbox, and an HTML <textarea>
element inside of an L.DivIcon, where the user can enter text. Right now, users can draw a textbox and enter text in it. What remains to be done to round out basic textbox functionality is to:
- Allow users to select text inside of the
<textarea>
. Currently, trying to select text inside the<textarea>
only results in dragging the map. - Determine how the
<textarea>
will behave on zoom. Should it shrink along with the map, or maintain constant size on the screen? - Correct that initial latlng position of the
<textarea>
. - Make sure that the
<textarea>
resizes properly along with the SVG rectangle.
Some higher-level questions that have occurred to me at this point:
If I continue to implement textboxes using HTML <textarea>
elements, then the L.Draw.Rectangle outline is not really necessary. If I want an outline to show up around the <textarea>
when it is focused, I can do that easily using CSS. The weakness of using <textarea>
elements is that, as far as I am aware, HTML elements cannot be rotated arbitrarily, which means that any text annotations on the map would always have to be oriented horizontally. I don't think this is necessarily a bad thing, since horizontally-oriented text is most readable. However, if there is a need in the community for text that can be rotated, then we will need to implement textboxes using SVG text elements. This will be more complicated, as it will most likely involve extending Leaflet core, which currently does not support SVG text, only SVG graphics.
Finally, I'm not sure yet if it's really necessary to encapsulate all of this functionality in a separate Leaflet plugin. It may be sufficient to simply add functionality to Leaflet.draw. Leaflet.draw is conveniently designed in that the drawing functionality is only loosely coupled to the toolbar. The more that I think about it, the more it seems to me that, for MapKnitter, we don't really need that much new functionality - we just need existing functionality presented in a specific way (i.e. it's more of a UI problem than a plugin design problem). Accordingly, it may make the most sense to add a few classes to Leaflet.draw and then design a completely new and separate toolbar UI for MapKnitter. This will become clearer as I get further with my coding.
Weigh in! Let me know what you think of all this.
5 Comments
The choice of SVG sounds like a good one. I'm excited to be able to export high res annotations too.
Horizontally oriented text is the most readable in block form, but using it exclusively on a map will reduce readability. I turned all the labels horizontal on your header image to illustrate:
A horizontal label on a vertical geographic feature is illegible by itself. Which street is which? we don't know. Every annotated feature will have a horizontal label and a shape describing or pointing it out, that will be visually cluttered and effect readability for the whole map.
I think rotatable text is a part of the core specification for this project, not an optional feature.
Is this a question? Click here to post it to the Questions page.
Reply to this comment...
Log in to comment
Ah, ok. That's really good to know. Looking at the map above - right: horizontal labels are individually more readable, but filling the map with horizontal labels reduces readability for the whole map.
Time for me to dive into the SVG text element spec! This will involve adding to Leaflet core. Leaflet core currently support a bunch of SVG types, all at Leaflet/src/layer/vector - but not text.
Continuing to work on the implementation with L.Draw.Rectangle and L.DivIcon will take me down another path, so I'm going to put that aside in order to focus on the full-functionality SVG text implementation.
Reply to this comment...
Log in to comment
Cool! nice pivot! that's pretty neat that you're extending basic Leaflet functionality.
Reply to this comment...
Log in to comment
@justin - Many thanks for the great efforts you made in getting leaflet.illustrate to this point. I have 2 implementation questions, if you don't mind?
I'm using L.Illustrate.Create.Textbox and L.Illustrate.textbox in a command chain in order to implement my own maptool. Once I persist the Textbox created by L.Illustrate.textbox, I would like to be able to reconstruct an entire L.Illustrate.Textbox (from a database), and redisplay it on a map -- I'm close, but no cigar yet.
In reconstructing the textbox, I can: 1) successfully call L.Illustrate.textbox to create a L.Illustrate.Textbox (let's call it tb), 2) create a divIcon and set it to tb["options"]["icon"]
the but when I call L.Illustrate.Textbox.setContent(), the call stack fails because the icon has no children: (leaflet.illustrate.js:455): Uncaught TypeError: Cannot read property 'children' of undefined
My first question - is there any other way to have the div icon correctly initalised? My second question - have you made any progress in being able to shrink the textarea when zooming out?
My huge thanks in advance, Donovan
Is this a question? Click here to post it to the Questions page.
Reply to this comment...
Log in to comment
Follow-up:
I was able to solve the first problem by allowing the framework to instantiate the icon for me - i.e. only worrying about creating a L.Illiustrate.Mapbox:
private createTextbox(position: L.LatLng, options: {}, text: string): L.Illustrate.Textbox { var divOptions = { minSize: new L.Point(options["textboxSize"].x, options["textboxSize"].y), textEditable: false, textContent: text} return L.Illustrate.textbox(position, divOptions) }
Reply to this comment...
Log in to comment
Login to comment.