Annotating the map

Will Breitkreutz
Feb 25, 2017 · 9 min read

Trying to get OpenLayers 3 and React to play nice and share the DOM (Document Object Model) turns out to be a little harder than we thought it was going to be, well, that’s not exactly true, it’s actually the event ecosystem that makes it interesting when trying to get the two frameworks to play nice.

Anyway, We’re getting ahead of ourselves.

CorpsMap Xenon (the internal name for our new mapping platform) is a web mapping application built as a React application with an integrated OpenLayers map canvas. Until now, React is has been in charge of the user interface (UI) components that wrap around the map canvas and OpenLayers takes over managing the DOM inside the map.

We’ve worked out a way to make React and OpenLayers work together to add a React-based component to an OpenLayers managed overlay element and leverage the best of both frameworks to get nicely interactive tools for our users. Let’s explore how we make this work, it’s a little bit of a kludge in places, but it works well and will hopefully lead to more elegant interaction later on.

Our Case Study

Our users want the ability to add their own annotation to a map, stick it to a place (but be able to move it around as needed), oh, and let’s add integrated style editing just to round it out.

We’re going to start out by building a little plugin for CorpsMap, the plugin architecture is subject of another post, but the basics are that we create a React component for the tools button, write all the plugin logic and then register the plugin with the main application which handles putting the button in the right place on the toolbar.

Our basic plugin, when activated by clicking the annotation tool button sets our cursor to cross-hairs and awaits the coming click event when it will do it’s thing.

Annotation tool in the main toolbar

In order to listen to the click event on the map, we just need to add our addComment function to the listener queue of the map:

app.ol2d.on(‘singleclick’, this.addComment);

When the user clicks our frameworks need to start working together, binding a react component to a DOM node managed by OpenLayers (to keep it in place when a user manipulates the map view).

Our addComment function looks a little something like this:

addComment(evt){      
var _this = this;
var annoCoord = evt.coordinate;
var annoId = 'comment-' + overlayId++;
var anno = document.createElement('div');
anno.className = 'ol-popup anno-popup';
var popup = new ol.Overlay({
id: annoId,
element: anno,
autoPan: true,
autoPanAnimation: {
duration: 250
}
})
app.ol2d.addOverlay(popup);
popup.setPosition(annoCoord);
overlaysAdded.push(annoId);
ReactDOM.render(<PopupContent popup={popup} />, anno);
}

We’re creating a DOM node called anno and handing that off to OpenLayers as a new ol.Overlay. At the same time we’re using React to render a component called PopupContent to the same DOM node and passing in a reference to the OpenLayers overlay for use at a later date inside the React component.

Easy enough so far, say PopupContent just returns <div>Click to edit text</div> you will get the popup shown below, not too bad, but not super functional either.

Not a bad start

Time to start walking through our requirements.

Editing the text

Annotation that just tells the user to edit the text, without letting them edit the text would be pretty boring. The contentEditable property provided by HTML lets us easily add editing to the popup. All we need to do is add it to the div returned by our PopupContent component:

<div contentEditable={true} >Click to edit text</div>

Side Note: You can just add contentEditable as an HTML attribute, but we like to add it with a boolean flag to follow the React way of adding attribution to DOM elements, this also makes it easy to manage the edit-ability of a component based on state down the road if that’s something we want to do.

Closing the overlay

We don’t want overlays to just pile up on the map until the user refreshes the page, so an easy way to get rid of the overlay is critical.

You might think we could just add a button to the PopupContent component, listen to the onClick event using out of the box React and then act accordingly, at least that’s what we thought when we started. After some hand waving and eventual research we figured out that Reacts event ecosystem is separate from the normal DOM event pathway.

The simple explanation is that React anticipates that all click events will bubble up to the main page body, where React will intercept them and act accordingly. This makes sense when you start thinking about how React manages DOM changes. OpenLayers, however stops the event bubble when a user clicks on an overlay so that the click isn’t passed on to the parent map element, which also makes sense. How do you make these two jive though?

Well the way we approached the problem is probably more verbose than it needs to be, but works well in this application.

We abstract the close button into it’s own component that is added to the PopupContent just after the editable div mentioned above.

Our CloseButton component looks like this:

var CloseButton = React.createClass({
componentDidMount: function(){
var _this = this;
ReactDOM.findDOMNode(this).addEventListener(‘click’, (e) => {
_this.handleClick();
}, false);
},
handleClick: function(e){
app.ol2d.removeOverlay(this.props.popup);
},
render: function(){
return (
<div className=”ol-popup-closer”></div>
)
}
})

When the component is added, the render function just renders a simple div and our css takes care of adding the X icon. After the component mounts, we look up the DOM node using ReactDOM.findDOMNode and attach a vanilla JS click event listener to that node, telling it to use our handleClick function as the callback.

While this isn’t as nice as using the React onClick property, it works relatively well for the interactions we need in this tool.

All of the other functions I’m going to mention below use the same event listener format to add interaction to the React components without having to let the event bubble up to the top level of the application.

Moving the overlay

Making the overlay draggable is kind of an interesting problem to solve, we have to stack a number of event listeners on top of each other, turning off the pan functionality of the map while we drag the overlay and turning it back on when the user is done.

We decided to add a drag handle to the overlay rather than make the entire panel draggable to make the interaction more precise and not feel like the overlay jumps to the mouse location when the drag action starts.

Inside our DragHandle component, we step through these steps when the component is mounted in our overlay:

  1. Get a reference to the dragPan interaction from the OpenLayers map so we can turn it off later.
  2. Add a listener to the mousedown event on the drag handle itself, when triggered it will.

The callback to mousedown then has to do these actions:

  1. Attach the dragMe function from the component as the callback to the pointermove event of the map, dragMe handles the updates to the popup location based on changes to the mouse pointers location on the map.
  2. Turn off the dragPan interaction captured above so that moving the mouse doesn’t pan the underlying map.
  3. Set the dragging property of the overlay to true so that it our other listeners know that it is actually supposed to be dragging and therefore they should respond accordingly.
  4. Attach the placeMe function from the component as the callback to the mouseup event of the map, placeMe takes care of resetting the original state of the dragPan interaction and the dragging property of the overlay in additon to removing the listeners to pointermove and mouseup events.

All of this adds up to the overlay being draggable by the user all over the map.

Editing Style

Style editing added another wrinkle to the process. Now we need to store the style of the text in the overlay as state in our PopupContent component, and add a reference to the style state to the JSX div rendering our content:

<div style={this.state.style} contentEditable={true}>Click to edit text</div>

That’s easy enough. Now we need to add a style editor button like our drag handle and close button to PopupContent.

The style editor button needs a way to send the updated style information back up to our PopupContent so we attach a custom property we’re calling changeHandler and a reference to a function on PopupContent that will receive the updated style and set the state accordingly.

<EditorButton changeHandler={this.setStyle} />

In order to have a style editor panel open up when the user clicks the edit button, we can add the editor panel as a child to our editor button, that way the positioning cascades down and we can be assured that the panel will show up where we want it to relative to the button element.

The button then handles the visible state of the editor panel, listening to the click event as we did above, and sending a boolean show value down in to the editor panel component that is used to toggle display:none which shows the editor panel on click.

Our editor button looks something like this:

<div className="ol-popup-edit">
<StyleEditor
changeHandler={this.props.changeHandler}
show={this.state.showEditor}
onClose={this.hideEditor}
/>
</div>

As you can see above, we’re passing the changeHandler function from our PopupContent component down in to the editor so that it can handle any changes and send the state back up the tree.

Annoyingly, each of the pieces of functionality in the style editor require their own React component due to the click event bubbling issue mentioned earlier so we won’t walk through all of them right now. You can see the component below showing that we can adjust the size of the text, toggle bold and italic, and pick from 5 different colors (we’re surely going to have to add more colors later on).

Style Editor Deployed

As the user clicks the elements of the style editor, the new style is sent upstream to the PopupContent element which sets it’s own style state accordingly, cascading the style to the displayed text.

Just for fun

After all that, wouldn’t it be nice if we could play around a little bit with our overlays and make them a little more fun? How about adding emoji support!?

This turned out to be less simple than originally thought, but it was a good learning experience for sure, and the final solution turned out to be much more simple than the first or second attempt at adding the functionality.

We’re able to display emoji by turning emoji text, the name of the emoji surrounded by :’s such as :smile: into their image equivalent.

There is a nice little library that parses text looking for the emoji text and replacing them with image tags pointing to the emoji images where they live on the server.

Doing the transformation from emoji text to emoji image tag in one direction worked great, but what about when you want to edit the text? We originally started out trying to parse the image tag back into the emoji text, wasted a few hours buried in regular expressions before we realized that there was an easier way.

The solution starts by storing both the raw text entered into the editable component including any emoji text like :boom: as well as the transformed text including the emoji image tags in the component state.

By listening to the focus and blur events of the PopupContent text element, we can swap out the rendered text with the original emoji text so that the user can edit at will and every time she clicks out of the popup the content is re-transformed into emoji images and the appropriate inane image shows up.

So what you end up with is a fun little Easter egg that people can use to liven up their maps with any of the emoji from here.

Will Breitkreutz

Written by

geographer, coder, biker, beer lover

corpsmap

corpsmap

Ramblings of the CorpsMap Development Team

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade