Leaflet.DistortableImage

A Leaflet extension to distort images -- "rubbersheeting" -- for the MapKnitter.org (src) image georectification service by Public Lab. Leaflet.DistortableImage allows for perspectival distortions of images, client-side, using CSS3 transformations in the DOM.
Advantages include:
- It can handle over 100 images smoothly, even on a smartphone.
- Images can be right-clicked and downloaded individually in their original state
- CSS3 transforms are GPU-accelerated in most (all?) browsers, for a very smooth UI
- No need to server-side generate raster GeoTiffs, tilesets, etc. in order to view distorted imagery layers
- Images use DOM event handling for real-time distortion
- Full resolution download option for large images, using WebGL acceleration
Download as zip or clone the repo to get a local copy.
Also available on NPM as leaflet-distortableimage
The recommended Google satellite base layer can be integrated using this Leaflet plugin: https://gitlab.com/IvanSanchez/Leaflet.GridLayer.GoogleMutant
Here's a screenshot:

Setup - Single Image Interface
-
From the root directory, run npm install
or sudo npm install
-
Open examples/index.html
in a browser
Demo
Check out this simple demo.
And watch this GIF demo:

To test the code, open index.html
in your browser and click and drag the markers on the edges of the image. The image will show perspectival distortions.
Basic Usage
The most simple implementation to get started:
map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/anishshah101.ipm9j6em/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
id: 'examples.map-i86knfo3'
}).addTo(map);
img = L.distortableImageOverlay(
'example.png', {
corners: [
L.latLng(51.52,-0.14),
L.latLng(51.52,-0.10),
L.latLng(51.50,-0.14),
L.latLng(51.50,-0.10)
],
}).addTo(map);
L.DomEvent.on(img._image, 'load', img.editing.enable, img.editing);
Options available to pass during L.DistortableImageOverlay
initialization:
Actions
actions
(optional, default: [ToggleTransparency, ToggleOutline, ToggleLock, ToggleRotateScale, ToggleOrder, EnableEXIF, Revert, Export, Delete], value: array)
If you would like to overrwrite the default toolbar actions available for an individual image's L.Popup
toolbar, pass an array with the actions you want. Reference the available values here.
For example, to overrwrite the toolbar to only include the ToggleTransparency
and Delete
actions:
img = L.distortableImageOverlay(
* 'example.png', {
corners: [
L.latLng(51.52,-0.14),
L.latLng(51.52,-0.10),
L.latLng(51.50,-0.14),
L.latLng(51.50,-0.10)
],
actions: [ToggleTransparency, Delete]
}).addTo(map);
Corners
The corners are stored as L.latLng
objects on the image, and can be accessed using
our getCorners()
method after the image is instantiated and added to the map.
They are provided in NW, NE, SW, SE
order (in a Z
shape).
Useful usage example:
img = L.distortableImageOverlay(...);
img.addTo(map);
L.DomEvent.on(img._image, 'load', img.editing.enable, img.editing);
JSON.stringify(img.getCorners())
=> "[{"lat":51.52,"lng":-0.14},{"lat":51.52,"lng":-0.1},{"lat":51.5,"lng":-0.14},{"lat":51.5,"lng":-0.1}]"
JSON.stringify(img.getCorners())
=> "[{"lat":51.50685099607552,"lng":-0.06058305501937867},{"lat":51.50685099607552,"lng":-0.02058595418930054},{"lat":51.486652692081925,"lng":-0.06058305501937867},{"lat":51.486652692081925,"lng":-0.02058595418930054}]"
We further added a getCorner(idx)
method used the same way as its plural counterpart but with an index passed to it.
Selected
selected
(optional, default: false, value: boolean)
By default, your image will initially appear on the screen as "unselected", meaning its toolbar and editing handles will not be visible. Interacting with the image, such as by clicking it, will make these components visible.
Some developers prefer that an image initially appears as "selected" instead of "unselected". In this case, we provide an option to pass selected: true
.
Note: when working with the multi image interface, only the last overlay you pass selected: true
to will appear with editing handles and a toolbar.
Mode
mode
(optional, default: "distort", value: string)
Each primary editing mode corresponds to a separate editing handle.
This option sets the image's initial editing mode, meaning the corresponding editing handle will always appear first when you interact with the image.
Values available to pass to mode
are:
-
distort (default): Distortion via individually draggable corners.
-
rotate: Rotation only.
-
scale: Resize only.
-
rotateScale: Free transform. Combines the rotate and scale modes into one.
-
lock: Prevents any image actions (including those triggered from the toolbar, user gestures, and hotkeys) until the toolbar action ToggleLock is explicitly triggered (or its hotkey l).
In the below example, the image will be initialiazed with "rotateScale" handles:
img = L.distortableImageOverlay("example.png", {
corners: [
L.latLng(51.52, -0.14),
L.latLng(51.52, -0.10),
L.latLng(51.50, -0.14),
L.latLng(51.50, -0.10)
],
mode: "rotateScale",
}).addTo(map);
L.DomEvent.on(img._image, 'load', img.editing.enable, img.editing);
Keymapper
keymapper
(optional, default: true, value: boolean)
By default, an image loads with a keymapper legend showing the available key bindings for different editing / interaction options. To suppress the keymapper, pass keymapper: false
as an additional option to the image.
Full-resolution download
We've added a GPU-accelerated means to generate a full resolution version of the distorted image; it requires two additional dependencies to enable; see how we've included them in the demo:
<script src="../node_modules/webgl-distort/dist/webgl-distort.js"></script>
<script src="../node_modules/glfx/glfx.js"></script>
When instantiating a Distortable Image, pass in a fullResolutionSrc
option set to the url of the higher resolution image. This image will be used in full-res exporting.
img = L.distortableImageOverlay(
'example.png', {
corners: [
L.latLng(51.52,-0.14),
L.latLng(51.52,-0.10),
L.latLng(51.50,-0.14),
L.latLng(51.50,-0.10)
],
fullResolutionSrc: 'large.jpg'
}).addTo(map);
L.DomEvent.on(img._image, 'load', img.editing.enable, img.editing);
Suppress Toolbar
suppressToolbar
(optional, default: false, value: boolean)
To initialize an image without its toolbar, pass it suppressToolbar: true
.
Typically, editing actions are triggered through our toolbar interface or our predefined keybindings. If disabling the toolbar, the developer will need to implement their own toolbar UI or just use the keybindings. (WIP API for doing this)
This option will override other options related to the toolbar, such as selected: true
Setup - Multiple Image Interface
-
From the root directory, run npm install
or sudo npm install
-
Open examples/select.html
in a browser (todo -- add gh pages demo)
Our DistortableCollection
class allows working with multiple images simultaneously. This interface builds on the single image interface.
The setup is relatively similar - here is an example with two images:
img = L.distortableImageOverlay(
'example.png', {
keymapper: false,
corners: [
L.latLng(51.52, -0.14),
L.latLng(51.52,-0.10),
L.latLng(51.50, -0.14),
L.latLng(51.50,-0.10)
],
});
img2 = L.distortableImageOverlay(
'example.png', {
keymapper: false,
corners: [
L.latLng(51.51, -0.20),
L.latLng(51.51,-0.16),
L.latLng(51.49, -0.21),
L.latLng(51.49,-0.17)
],
});
imgGroup = L.distortableCollection().addTo(map);
imgGroup.addLayer(img);
imgGroup.addLayer(img2);
Note: notice how we didn't enable
the image editing above as we had done for the single image interface. This is because our DistortableCollection
class uses event listeners internally (layeradd
) to enable editing on every image as it's added. This event is only triggered if we add the layers to the group dynamically. I.e. you must add the group to the map initially empty.
Options available to pass during L.DistortableCollection
initialization:
✤ Actions
actions
(optional, default: [Exports, Deletes, Locks, Unlocks], value: array)
Overrwrite the default toolbar actions for an image collection's L.Control
toolbar. Reference the available values here.
For example, to overrwrite the toolbar to only include the Deletes
action:
imgGroup = L.distortableCollection({
actions: [Deletes]
}).addTo(map);
To add / remove a tool from the toolbar at runtime, we have also added the methods addTool(action)
and removeTool(action)
.
UI and functionalities
Currently it supports multiple image selection and translations, and WIP we are working on porting all editing tools to work for it, such as transparency, etc. Image distortions still use the single-image interface.
How to multi-select:
- Multi-selection works with cmd +
click
to toggle an individual image's inclusion in this interface. - Or shift +
drag
to use our BoxSelector
handler to select multiple at once. - Or for touch devices,
touch
and hold
(aka longpress
).
How to un-multi-select:
- A single toolbar instance (using
L.control
) renders the set of tools available to use on collections of images. - In order to return to the single-image interface, where each
L.popup
toolbar only applies actions on the image it's attached to, you must toggle all images out of multi-select or... - ...Click on the map or hit the esc key to quickly deselect all images.
- For the aforementioned 3 mutli-select methods, the
BoxSelector
method is the only one that doesn't also toggle out of multi-select mode.
Toolbar Actions (& Keybindings)
Single Image Interface:
Defaults:
Addons:
Multiple Image Interface:
Defaults:
Quick API Reference
L.DistortableImageOverlay
L.DistortableCollection
-
removeTool(action)
- Removes the passed tool from the control toolbar in runtime.
- Ex:
imgGroup.removeTool(Deletes)
-
addTool(action)
- Adds the passed tool to the control toolbar in runtime. Returns false if the tool is not available or is already present.
-
hasTool(action)
- Checks if the tool is already present in the currently rendered control toolbar.
Contributing
This plugin has basic functionality, and is in production as part of MapKnitter, but there are plenty of outstanding issues to resolve. Please consider helping out!
- This project uses
grunt
to do a lot of things, including concatenate source files from /src/
to /DistortableImageOverlay.js
:
$ npm install -g grunt-cli
$ grunt
- To build all files from
/src/
into the /dist/
folder, run:
$ grunt concat:dist
- Optional: We use SVG for our icon system. Please visit our wiki SVG Icon System if you are interested in making updates to them or just simply learning about our workflow.
Contributors
Many more at https://github.com/publiclab/Leaflet.DistortableImage/graphs/contributors