# Leaflet.DistortableImage
**Repository Path**: icuit/Leaflet.DistortableImage
## Basic Information
- **Project Name**: Leaflet.DistortableImage
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: BSD-2-Clause
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-11-09
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Leaflet.DistortableImage
[](https://travis-ci.org/publiclab/Leaflet.DistortableImage)
[](https://publiclab.org/conduct)
[](https://github.com/publiclab/Leaflet.DistortableImage/issues)
[](https://badge.fury.io/js/leaflet-distortableimage)
A Leaflet extension to distort images -- "rubbersheeting" -- for the [MapKnitter.org](http://mapknitter.org) ([src](https://github.com/publiclab/mapknitter)) image georectification service by [Public Lab](http://publiclab.org). 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](https://github.com/publiclab/Leaflet.DistortableImage/pull/100) for large images, using WebGL acceleration
[Download as zip](https://github.com/publiclab/Leaflet.DistortableImage/releases) or clone the repo to get a local copy.
Also available on NPM as [leaflet-distortableimage](https://www.npmjs.com/package/leaflet-distortableimage):
```Bash
npm i leaflet-distortableimage
```
## Compatibility with Leaflet versions
Compatible with Leaflet 1.0.0 and greater
## Demo
Check out this [simple demo](https://publiclab.github.io/Leaflet.DistortableImage/examples/index.html).
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.
For the additional features in the [multiple image interface](#Multiple-Image-Interface), open `select.html` and use shift + click on an image or shift + drag on the map to "multi-select" (collect) images. For touch screens, touch + hold the image.
## Single Image Interface
The simplest implementation is to create a map with our recommended `TileLayer`, then create an `L.distortableImageOverlay` instance and add it onto the map.
```js
// set the initial map center and zoom level
map = L.map('map').setView([51.505, -0.09], 13);
// adds a Google Satellite layer with a toner label overlay
map.addGoogleMutant();
map.whenReady(function() {
// By default, 'img' will be placed centered on the map view specified above
img = L.distortableImageOverlay('example.jpg').addTo(map);
});
```
Note: map.addGoogleMutant() is a convenience function for adding our recommended layer to the map. If you want a different baselayer, skip this line and add your preferred setup instead.
**Options** available to pass during `L.DistortableImageOverlay` initialization:
* [actions](#Actions)
* [corners](#corners)
* [editable](#editable)
* [fullResolutionSrc](#Full-resolution%20download)
* [mode](#mode)
* [rotation](#rotation)
* [selected](#selected)
* [suppressToolbar](#Suppress-Toolbar)
### Actions
* `actions` (*optional*, default: [`L.DragAction`, `L.ScaleAction`, `L.DistortAction`, `L.RotateAction`, `L.FreeRotateAction`, `L.LockAction`, `L.OpacityAction`, `L.BorderAction`, `L.ExportAction`, `L.DeleteAction`], 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](#Single-Image-Interface).
For example, to overrwrite the toolbar to only include `L.OpacityAction` and `L.DeleteAction` , and also add on an additional non-default like `L.RestoreAction`:
```js
img = L.distortableImageOverlay('example.jpg', {
actions: [L.OpacityAction, L.DeleteAction, L.RestoreAction],
}).addTo(map);
```
### Corners
* `corners` (*optional*, default: an array of `LatLang`s that position the image on the center of the map, value: *array*)
Allows you to set an image's position on the map manually (somewhere other than the center default).
Note that this can manipulate the shape and dimensions of your image.
The corners should be passed as an array of `L.latLng` objects in NW, NE, SW, SE order (in a "Z" shape).
They will be stored on the image. See the [Quick API Reference](#Quick-API-Reference) for their getter and setter methods.
Example:
```js
img = L.distortableImageOverlay('example.jpg', {
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);
// you can grab the initial corner positions
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}]"
// ...move the image around...
// you can check the new corner positions.
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}]"
// note there is an added level of precision after dragging the image
```
### Editable
`editable` (*optional*, default: true, value: *boolean*)
Internally, we use the image `load` event to trigger a call to `img.editing.enable()`, which sets up the editing interface (makes the image interactive, adds markers and toolbar).
If you want to enable editing based on custom logic instead, you can pass `editable: false` and then write your own function with a call to `img.editing.enable()`. Other passed options such as `selected: true` and `mode` will still be applicable and applied then.
Note: when using the multiple image interface (### Full-resolution download `fullResolutionSrc` (*optional*) We've added a GPU-accelerated means to generate a full resolution version of the distorted image. 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. ```js img = L.distortableImageOverlay('example.jpg', { fullResolutionSrc: 'large.jpg', }).addTo(map); ``` Our project includes two additional dependencies to enable this feature, [glfx.js](https://github.com/evanw/glfx.js) and [webgl-distort](https://github.com/jywarren/webgl-distort), both of which you can find in our [package.json](./package.json). ### Mode `mode` (*optional*, default: "distort", value: *string*) This option sets the image's initial editing mode, meaning the corresponding editing handles will always appear first when you interact with the image. Values available to pass to `mode` are: * **distort** (*default*): Distortion via individually draggable corners. * **drag**: Translation via individually draggable corners. * **rotate**: Rotation only. * **scale**: Resize only. * **freeRotate**: Combines the rotate and scale modes into one. * **lock**: Locks the image in place. Disables any user gestures, toolbar actions, or hotkeys that are not associated with mode. Exception: `L.ExportAction` will still be enabled. In the below example, the image will be initialiazed with "freeRotate" handles: ```js img = L.distortableImageOverlay('example.jpg', { mode: 'freeRotate', }).addTo(map); ``` If you select aL.DistortableCollection) this option will be ignored on individualL.DistortableImageOverlayinstances and should instead be passed to the collection instance.
mode that is removed or unavailable, your image will just be assigned the first available mode on initialization.
EachIn the below example, the image will be initialiazed with `'freeRotate'` handles, and limit its available modes to `'freeRotate'` and `'scale'`. * We also remember to add the normal toolbar actions we will want: ```js img = L.distortableImageOverlay('example.jpg', { mode: 'freeRotate', actions: [L.FreeRotateAction, L.ScaleAction, L.BorderAction, L.OpacityAction], }).addTo(map); ``` Likewise, it is possible to remove or add `actions` during runtime (`addTool`, `removeTool`), and if those actions are modes it will remove / add the `mode`. ### Rotation `rotation` (*optional*, default: {deg: 0, rad: 0}, value: *hash*) Set the initial rotation angle of your image, in degrees or radians. Set the unit as the key, and the angle as the value. ```js img = L.distortableImageOverlay('example.jpg', { rotation: { deg: 180, }, }).addTo(map); ``` ### Selected `selected` (*optional*, default: false, value: *boolean*) By default, your image will initially appear on the screen as unselected (no toolbar or markers). Interacting with it will make them visible. If you prefer that an image initially appears as selected instead, 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. ### Suppress Toolbar `suppressToolbar` (*optional*, default: false, value: *boolean*) To initialize an image without its `L.Popup` instance toolbar, pass it `suppressToolbar: true`. Typically, editing actions are triggered through our toolbar interface. If disabling the toolbar, the developer will need to implement their own toolbar UI connected to our actions (WIP API for doing this) ## Multiple Image Interface Our `DistortableCollection` class builds on the single image interface to allow working with multiple images simultaneously. The setup is relatively similar. Although not required, you will probably want to pass `corners` to individual images when adding multiple or they will be positioned on top of eachother. Here is an example with two images: ```js // 1. Instantiate map // 2. Instantiate images but this time *dont* add them directly to the map img = L.distortableImageOverlay('example.jpg', { 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.jpg', { 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), ], }); // 3. Instantiate an empty `DistortableCollection` group imgGroup = L.distortableCollection().addTo(map); // 4. Add the images to the group imgGroup.addLayer(img); imgGroup.addLayer(img2); ```modeis just a special type of action, so to ensure that these are always in sync themodesavailable on an image instance can be limited by theactionsavailable on it. To remove a mode, limit its corresponding action via theactionsoption during initialization. This holds true even whensuppressToolbar: trueis passed.
Note: You must instantiate a blank collection, then dynamically add layers to it like above. This is becauseOptions available to pass during `L.DistortableCollection` initialization: * [actions](#Actions-1) * [editable](#Editable-1) * [supressToolbar](#Suppress-Toolbar-1) ### Actions * `actions` (*optional*, default: [`L.ExportAction`, `L.DeleteAction`, `L.LockAction`, `L.UnlockAction`], value: *array*) Overrwrite the default toolbar actions for an image collection's `L.Control` toolbar. Reference the available values [here](#Multiple-Image-Interface). For example, to overrwrite the toolbar to only include the `L.DeleteAction`: ```JS imgGroup = L.distortableCollection({ actions: [L.DeleteAction], }).addTo(map); ``` To add / remove a tool from the toolbar at runtime, we have also added the methods `addTool(action)` and `removeTool(action)`. ### Editable `editable` (*optional*, default: true, value: *boolean*) See [editable](#editable). ### Suppress Toolbar `suppressToolbar` (*optional*, default: false, value: *boolean*) Same usage as [suppressToolbar](#Suppress-Toolbar), but for the collection group's `L.Control` toolbar instance. This provides the developer with the flexibility to keep the popup toolbars, the control toolbar, both, or neither. For ex. ```js // suppress this images personal toolbar img = L.distortableImageOverlay('example.jpg', { suppressToolbar: true, 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), ], }); // suppress the other images personal toolbar img2 = L.distortableImageOverlay('example.jpg', { suppressToolbar: true, }); // suppress collection toolbar accessed during multi-image selection imgGroup = L.distortableCollection({ supressToolbar: true, }).addTo(map); ``` ### 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 opacity, etc. Image distortions (via modes) still use the single-image interface. A single toolbar instance (using `L.control`) renders the set of tools available to use on collections of images. **collect**: 1. Collect an indvidiual image with shift + `click`. 2. Or for touch devices, `touch` + `hold` (aka `longpress`). 3. Collect multiple images at once with shift + `drag` (Uses our `L.Map.BoxCollector`). **decollect:** * 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 collection with `shift` + click / `touch` + `hold`, or... * ...Click on the map or hit the esc key to quickly decollect all. --- ## Toolbar Actions (& Keybindings) --- ### Single Image Interface --- #### Default tools * **L.BorderAction** (b) * Toggles a thin border around the overlay. * **L.DeleteAction** (backscpace, delete) * Permanently deletes the image from the map. Uses a `confirm()` modal dialog. * windows `backspace` / mac `delete` * **L.DistortAction** (d) * Sets `distort` mode. * **L.DragAction** * Sets `drag` mode. * **L.ExportAction** (e) * **L.FreeRotateAction** (f) * Sets `freeRotate` mode. * **L.LockAction** (l, u) * Toggles between `lock` mode and the initially set default mode (`distort` by default). * **L.OpacityAction** (o) * **L.RotateAction** (r): * Sets `rotate` mode. * **L.ScaleAction** (s): * Sets `scale` mode. #### Add-on tools These may be added using `addTool()`, like this: ```js distortableImageLayer.editing.addTool(L.StackAction); ``` * **L.RestoreAction** * Restores the image to its natural dimensions, scale, rotation, and location on the map. * **L.StackAction** (q, a) * Switch an image's overlap compared to neighboring images back and forth into view. Employs [`bringToFront()`](https://leafletjs.com/reference-1.5.0.html\#imageoverlay-bringtofront) and [`bringToBack()`](https://leafletjs.com/reference-1.5.0.html#imageoverlay-bringtoback) from the Leaflet API. * **L.GeolocateAction (WIP)** --- ### Multiple Image Interface --- Defaults: * **L.ExportAction** (e) * **L.DeleteAction** (backscpace, delete) * Permanently deletes a collection of images from the map. * **L.LockAction** (l) * Sets `lock` mode for a collection of images. * **L.UnlockAction** (u) * Unsets `lock` mode for a collection of images. ## Quick API Reference --- `L.Map` --- We have extended Leaflet's `L.Map` to include a convenience method for this library:DistortableCollectioninternally uses thelayeraddevent to enable additional editing features on images as they are added, and it is only triggered when they are added dynamically.
addGoogleMutant(opts? <Mutant options>): thisMutant options: {[mutantOpacity][, maxZoom][, minZoom][, labels][, labelOpacity][, doubleClickLabels]}
mutantOpacity (default 0.8, value: number 0..1)L.TileLayer opacity option.maxZoom (default: 18, value: number 0..21)L.TileLayer maxZoom option, except has a maximum value of 21 because higher zoom levels on the mutant layer will result in an error being thrown.minZoom (default: 0, value: number 0..maxZoom)L.TileLayer minZoom option.labels (default: true, value: boolean)false, the mutant layer will not have location labels.labelOpacity (default: 1, value: number 0, 1)0, labels will be initially invisible.undefined if labels: false is also passed.doubleClickLabels (default: true, value: boolean)dblclick. To turn this functionality off, set this option to false.undefined if labels: false is also passed.map.mutantOptions.doubleClickLabels: thisdblclick.#addGoogleMutant unless the options labels: false or doubleClickLabels: false are passed to it.labels: false passed, removed from map altogether.doubleClickLabels: false was passed, just disabled and can always be enabled during runtime via Leaflet's Handler API.doubleClickZoom handler when enabled.boxCollector: thisboxZoom handler. To use boxZoom instead, pass the options { boxCollector: false, boxZoom: true } to the map on initialization.
draging on the map for the multiple image interface.doubleClickZoom: thisenabled (and will return false) while the doubleClickLabels handler is enabled.doubleClickLabels time and fire a custom singleclick event on map click.Our "doubleClick" handlers mentioned above use a custom--- `L.DistortableImageOverlay` --- An individual image instance that can have transformation methods called on it and can be "selected".singleclickevent to run logic on mapdblclickwhile allowing the images on the map to remainselected. You can read more about the implications of this and how to disable it on our wiki "singleclick event".
getCorner(idx <number 0..3>): LatLnggetCorners(): 4 [LatLng, LatLng, LatLng, LatLng]setCorner(idx <number 0..3>, LatLng): thisdistort mode.setCorners(LatLngCorners): this#setCorner, but takes in a "corners" object made up of LatLngs to update all 4 corners with only one UI update at the end.
var scaledCorners = {};
var i;
var p;
for (i = 0; i < 4; i++) {
p = map
.project(img.getCorner(i))
.subtract(center)
.multiplyBy(scale)
.add(center);
scaledCorners[i] = map.unproject(p);
}
img.setCorners(scaledCorners);
setCornersFromPoints(PointCorners): this#setCorners, but takes in a "corners" object made up of Points instead of LatLngs.getCenter(): LatLnggetAngle([unit = 'deg'] <string>): Numberunits, or in degrees by default.
Number will always be >= 0.unit (optional, default: 'deg', value: string 'deg'|'rad')
img.getAngle();
img.getAngle('deg');
img.getAngle('rad');
setAngle(angle <number>, [unit = 'deg'] <string>): thisangle in units, or in degrees by default.unit (optional, default: 'deg', value: string 'deg'|'rad') img.setAngle(180); img.setAngle(180, 'deg'); img.setAngle(Math.PI, 'rad');
rotateBy(angle <number>, [unit = 'deg'] <string>): thisangle in units, or in degrees by default.unit (optional, default: 'deg', value: string 'deg'|'rad') img.rotateBy(180); img.rotateBy(180, 'deg'); img.rotateBy(Math.PI, 'rad');
scaleBy(factor <number>): this#setCorners.img.scaleBy(0.5)restore(): thisisSelected(): Booleanselect(): thisimgGroup.anyCollected() === true), does not select and instead just returns undefined.click.deselect(): thisclick and image collect (shift + click).Note: The main difference between theenable/disableruntime API and using theeditableoption during initialization is in runtime, neither individual image instaces nor the collection group get precedence over the other.
enable(): thisimg.editing.enable() after imgGroup.editing.disable() is valid. In this case, the single image interface will be available on this image but not the multi-image interface.disable(): thisenabled(): Booleanimg.editing.enabled()hasMode(mode <string>): BooleangetMode(): Stringmode of the image.getModes(): HashnextMode(): thismode of the image to the next one in the modes array by passing it to #setMode.modes only has 1 mode, it will instead return undefined and not update the image's mode.dblclick, but you can call it programmatically if you find a need. Note that dblclick also selects the image (given it's not disabled and the collection interface is not on).setMode(mode <string>): thismode of the image to the passed one given that it is in the modesarray, it is not already the current mode, and the image editing interface is enabled. Otherwise, does not set the mode and instead just returns undefined.isCollected(img <DistortableImageOverlay>): BooleanL.DistortableImageOverlay instance is collected, i.e. its underlying HTMLImageElement has a class containing "selected".anyCollected(): BooleanL.DistortableImageOverlay instances are collected.enable(): this#enable method and then enables the multi-image interface.disable(): this#disable method and disables the multi-image interface.enabled(): BooleanimgGroup.editing.enabled()removeTool(action <EditAction>): thisimgGroup.removeTool(Deletes)addTool(action <EditAction>): thisreplaceTool(old <EditAction>), next <EditAction>)hasTool(action <EditAction>): Boolean