├── .babelrc
├── .gitignore
├── screenshots
├── camera.jpg
├── example.png
└── crosshair.jpg
├── images
├── camera.svg
├── crosshair-icon.svg
├── crosshair.svg
├── marker.svg
└── camera-icon.svg
├── rollup.config.browser.js
├── rollup.config.browser.min.js
├── css
└── Leaflet.GeotagPhoto.css
├── LICENSE.md
├── package.json
├── index.js
├── src
├── Leaflet.GeotagPhoto.Crosshair.js
├── Leaflet.GeotagPhoto.CameraControl.js
└── Leaflet.GeotagPhoto.Camera.js
├── examples
├── crosshair.html
└── camera.html
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015-rollup"]
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _examples
2 | node_modules
3 | dist
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/screenshots/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nypl-spacetime/Leaflet.GeotagPhoto/HEAD/screenshots/camera.jpg
--------------------------------------------------------------------------------
/screenshots/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nypl-spacetime/Leaflet.GeotagPhoto/HEAD/screenshots/example.png
--------------------------------------------------------------------------------
/screenshots/crosshair.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nypl-spacetime/Leaflet.GeotagPhoto/HEAD/screenshots/crosshair.jpg
--------------------------------------------------------------------------------
/images/camera.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/images/crosshair-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/rollup.config.browser.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import nodeResolve from 'rollup-plugin-node-resolve'
3 | import commonjs from 'rollup-plugin-commonjs'
4 |
5 | export default {
6 | entry: 'index.js',
7 | dest: 'dist/Leaflet.GeotagPhoto.js',
8 | format: 'iife',
9 | moduleName: 'leaflet-geotag-photo',
10 | globals: {
11 | Leaflet: 'L'
12 | },
13 | plugins: [
14 | nodeResolve({
15 | jsnext: true,
16 | skip: [
17 | 'Leaflet'
18 | ]
19 | }),
20 | commonjs(),
21 | babel({
22 | exclude: 'node_modules/**',
23 | presets: 'es2015-rollup'
24 | })
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/images/crosshair.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/images/marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/rollup.config.browser.min.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import uglify from 'rollup-plugin-uglify'
3 | import nodeResolve from 'rollup-plugin-node-resolve'
4 | import commonjs from 'rollup-plugin-commonjs'
5 |
6 | export default {
7 | entry: 'index.js',
8 | dest: 'dist/Leaflet.GeotagPhoto.min.js',
9 | format: 'iife',
10 | moduleName: 'leaflet-geotag-photo',
11 | sourceMap: true,
12 | globals: {
13 | Leaflet: 'L'
14 | },
15 | plugins: [
16 | nodeResolve({
17 | jsnext: true,
18 | skip: [
19 | 'Leaflet'
20 | ]
21 | }),
22 | commonjs(),
23 | babel({
24 | exclude: 'node_modules/**',
25 | presets: 'es2015-rollup'
26 | }),
27 | uglify()
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/css/Leaflet.GeotagPhoto.css:
--------------------------------------------------------------------------------
1 | .leaflet-geotag-photo-crosshair {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 | pointer-events: none;
8 | overflow: hidden;
9 | z-index: 999;
10 |
11 | display: -webkit-box;
12 | display: -moz-box;
13 | display: -ms-flexbox;
14 | display: -webkit-flex;
15 | display: flex;
16 | -webkit-box-align: center;
17 | -webkit-flex-align: center;
18 | -ms-flex-align: center;
19 | -webkit-align-items: center;
20 | align-items: center;
21 | }
22 |
23 | .leaflet-geotag-photo-crosshair > * {
24 | position: relative;
25 | z-index: 1001;
26 | margin: auto;
27 | }
28 |
29 | .leaflet-control-geotag-photo-camera,
30 | .leaflet-control-geotag-photo-crosshair {
31 | width: 100%;
32 | padding: 3px;
33 | box-sizing: border-box;
34 | }
--------------------------------------------------------------------------------
/images/camera-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 The New York Public Library
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leaflet-geotag-photo",
3 | "version": "0.6.2",
4 | "description": "Leaflet plugin for photo geotagging",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/nypl-spacetime/Leaflet.GeotagPhoto"
8 | },
9 | "files": [
10 | "dist",
11 | "src",
12 | "index.js",
13 | "images"
14 | ],
15 | "main": "dist/Leaflet.GeotagPhoto.js",
16 | "browser": "dist/Leaflet.GeotagPhoto.min.js",
17 | "module": "index",
18 | "jsnext:main": "index",
19 | "scripts": {
20 | "prepublish": "npm run test",
21 | "pretest": "rimraf dist && mkdir dist && npm run build",
22 | "build": "cp css/* dist && ./node_modules/.bin/rollup -c rollup.config.browser.js && ./node_modules/.bin/rollup -c rollup.config.browser.min.js",
23 | "test": "npm run lint",
24 | "lint": "standard *.js src/*"
25 | },
26 | "author": "Bert Spaan",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "babel-preset-es2015-rollup": "^1.2.0",
30 | "rimraf": "^2.5.4",
31 | "rollup": "^0.36.4",
32 | "rollup-plugin-babel": "^2.6.1",
33 | "rollup-plugin-commonjs": "^5.0.5",
34 | "rollup-plugin-node-resolve": "^2.0.0",
35 | "rollup-plugin-uglify": "^1.0.1",
36 | "standard": "^8.6.0",
37 | "tap": "^8.0.1"
38 | },
39 | "dependencies": {
40 | "field-of-view": "^0.2.2",
41 | "leaflet": "^1.0.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import L from 'Leaflet'
4 | import GeotagPhotoCrosshair from './src/Leaflet.GeotagPhoto.Crosshair'
5 | import GeotagPhotoCamera from './src/Leaflet.GeotagPhoto.Camera'
6 |
7 | // Object.assign polyfill, for IE<=11. From:
8 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
9 | // TODO: I'm sure Babel can add this polyfill, too.
10 | if (typeof Object.assign !== 'function') {
11 | Object.assign = function (target, varArgs) {
12 | if (target == null) {
13 | throw new TypeError('Cannot convert undefined or null to object')
14 | }
15 |
16 | var to = Object(target)
17 |
18 | for (var index = 1; index < arguments.length; index++) {
19 | var nextSource = arguments[index]
20 |
21 | if (nextSource != null) {
22 | for (var nextKey in nextSource) {
23 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
24 | to[nextKey] = nextSource[nextKey]
25 | }
26 | }
27 | }
28 | }
29 | return to
30 | }
31 | }
32 |
33 | L.geotagPhoto = {
34 | crosshair: function (options) {
35 | return new GeotagPhotoCrosshair(options)
36 | },
37 | camera: function (feature, options) {
38 | return new GeotagPhotoCamera(feature, options)
39 | }
40 | }
41 |
42 | L.GeotagPhoto = {
43 | Crosshair: GeotagPhotoCrosshair,
44 | Camera: GeotagPhotoCamera
45 | }
46 |
--------------------------------------------------------------------------------
/src/Leaflet.GeotagPhoto.Crosshair.js:
--------------------------------------------------------------------------------
1 | import L from 'leaflet'
2 |
3 | export default L.Evented.extend({
4 | options: {
5 | controlCrosshairImg: '../images/crosshair-icon.svg',
6 | crosshairHTML: '
'
7 | },
8 |
9 | initialize: function (options) {
10 | L.setOptions(this, options)
11 | },
12 |
13 | addTo: function (map) {
14 | this._map = map
15 | var container = map.getContainer()
16 | this._element = L.DomUtil.create('div', 'leaflet-geotag-photo-crosshair', container)
17 | this._element.innerHTML = this.options.crosshairHTML
18 |
19 | this._boundOnInput = this._onInput.bind(this)
20 | this._boundOnChange = this._onChange.bind(this)
21 |
22 | this._map.on('move', this._boundOnInput)
23 | this._map.on('moveend', this._boundOnChange)
24 |
25 | return this
26 | },
27 |
28 | removeFrom: function (map) {
29 | if (this._map && this._boundOnInput && this._boundOnChange) {
30 | this._map.off('move', this._boundOnInput)
31 | this._map.off('moveend', this._boundOnChange)
32 | }
33 |
34 | if (this._element) {
35 | L.DomUtil.remove(this._element)
36 | }
37 |
38 | return this
39 | },
40 |
41 | _onInput: function () {
42 | this.fire('input')
43 | },
44 |
45 | _onChange: function () {
46 | this.fire('change')
47 | },
48 |
49 | getCrosshairLatLng: function () {
50 | return this._map.getCenter()
51 | },
52 |
53 | getCrosshairPoint: function () {
54 | if (this._map) {
55 | var center = this.getCrosshairLatLng()
56 | return {
57 | type: 'Point',
58 | coordinates: [
59 | center.lng,
60 | center.lat
61 | ]
62 | }
63 | }
64 | }
65 |
66 | })
67 |
--------------------------------------------------------------------------------
/src/Leaflet.GeotagPhoto.CameraControl.js:
--------------------------------------------------------------------------------
1 | import L from 'leaflet'
2 |
3 | export default L.Control.extend({
4 | options: {
5 | position: 'topleft'
6 | },
7 |
8 | initialize: function (geotagPhotoCamera, options) {
9 | this._geotagPhotoCamera = geotagPhotoCamera
10 | L.setOptions(this, options)
11 | },
12 |
13 | onAdd: function (map) {
14 | this._map = map
15 |
16 | var controlName = 'leaflet-control-geotag-photo-'
17 | var container = L.DomUtil.create('div', controlName + ' leaflet-bar')
18 |
19 | var cameraImg = '
'
20 | var crosshairImg = '
'
21 |
22 | this._cameraButton = this._createButton(cameraImg, 'Move camera back to map (C)',
23 | controlName + 'camera', container, this._centerCamera)
24 |
25 | this._crosshairButton = this._createButton(crosshairImg, 'Move map back to camera (M)',
26 | controlName + 'crosshair', container, this._centerMap)
27 |
28 | this._boundMapKeyPress = this._mapKeyPress.bind(this)
29 | this._map.on('keypress', this._boundMapKeyPress)
30 |
31 | return container
32 | },
33 |
34 | _createButton: function (html, title, className, container, fn) {
35 | var link = L.DomUtil.create('a', className, container)
36 | link.innerHTML = html
37 | link.href = '#'
38 | link.title = title
39 |
40 | /*
41 | * Will force screen readers like VoiceOver to read this as "Zoom in - button"
42 | */
43 | link.setAttribute('role', 'button')
44 | link.setAttribute('aria-label', title)
45 |
46 | L.DomEvent
47 | .on(link, 'mousedown dblclick', L.DomEvent.stopPropagation)
48 | .on(link, 'click', L.DomEvent.stop)
49 | .on(link, 'click', fn, this)
50 | .on(link, 'click', this._refocusOnMap, this)
51 |
52 | return link
53 | },
54 |
55 | onRemove: function (map) {
56 | L.DomUtil.remove(this._element)
57 | map.off('keypress', this._boundMapKeyPress)
58 | },
59 |
60 | _mapKeyPress: function (evt) {
61 | if (evt.originalEvent.charCode === 99) {
62 | // C key
63 | this._centerCamera()
64 | } else if (evt.originalEvent.charCode === 109) {
65 | // M key
66 | this._centerMap()
67 | }
68 | },
69 |
70 | _centerCamera: function () {
71 | if (this._map && this._geotagPhotoCamera) {
72 | this._geotagPhotoCamera.centerBounds(this._map.getBounds())
73 | }
74 | },
75 |
76 | _centerMap: function () {
77 | if (this._map && this._geotagPhotoCamera) {
78 | this._map.fitBounds(this._geotagPhotoCamera.getBounds())
79 | }
80 | }
81 |
82 | })
83 |
--------------------------------------------------------------------------------
/examples/crosshair.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Crosshair - Leaflet.GeotagPhoto
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
67 |
68 |
69 |
73 |
83 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/examples/camera.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Camera - Leaflet.GeotagPhoto
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
115 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Leaflet.GeotagPhoto
2 |
3 | Leaflet plugin for photo geotagging.
4 |
5 | Examples:
6 |
7 | - [Crosshair mode](http://spacetime.nypl.org/Leaflet.GeotagPhoto/examples/crosshair.html)
8 | - [Camera mode](http://spacetime.nypl.org/Leaflet.GeotagPhoto/examples/camera.html)
9 |
10 | [](http://spacetime.nypl.org/Leaflet.GeotagPhoto/examples/camera.html)
11 |
12 | Leaflet.GeotagPhoto is part of The New York Public Library's [NYC Space/Time Directory](http://spacetime.nypl.org/).
13 |
14 | You can also find Leaflet.GeotagPhoto on [Leaflet's plugin page](http://leafletjs.com/plugins.html).
15 |
16 | ## Usage
17 |
18 | Include the following HTML in your page's `` tag:
19 |
20 | ```html
21 |
22 |
23 | ```
24 |
25 | The HTML above links to the latest version of Leaflet.GeotagPhoto. In production, you should link to a specific version, to prevent newer versions breaking your application:
26 |
27 | ```html
28 |
29 |
30 | ```
31 |
32 | ## Modes
33 |
34 | ### `L.GeotagPhoto.Crosshair`
35 |
36 | 
37 |
38 | #### Example
39 |
40 | ```js
41 | L.geotagPhoto.crosshair().addTo(map)
42 | .on('input', function (event) {
43 | var point = this.getCrosshairPoint()
44 | })
45 | ```
46 |
47 | #### API
48 |
49 | `L.GeotagPhoto.Crosshair` extends [L.Evented](http://leafletjs.com/reference-1.0.0.html#evented).
50 |
51 | | Function | Description |
52 | |:------------------------------------|:------------------------------------------------|
53 | | `L.geotagPhoto.crosshair(options?)` | Creation |
54 | | `addTo (map)` | Add `L.GeotagPhoto.Crosshair` to `map` |
55 | | `removeFrom (map)` | Remove `L.GeotagPhoto.Crosshair` from `map` |
56 | | `getCrosshairLatLng ()` | Returns crosshair's [`LatLng`](http://leafletjs.com/reference-1.0.0.html#latlng) |
57 | | `getCrosshairPoint ()` | Returns crosshair's GeoJSON Point |
58 |
59 | #### Options
60 |
61 | | Option | Type | Default | Description
62 | |:----------------|:--------------|:------------------------------------------------------|:-------------|
63 | | `crosshairHTML` | `HTML String` | `
` | HTML string of crosshair element |
64 | | `controlCrosshairImg` | `url` | `../images/crosshair.svg` | Crosshair image URL used by the default `crosshairHTML` |
65 |
66 | ### `L.GeotagPhoto.Camera`
67 |
68 | 
69 |
70 | #### Example
71 |
72 | ```js
73 | var cameraPoint = [6.83442, 52.43369]
74 | var targetPoint = [6.83342, 52.43469]
75 |
76 | var points = {
77 | type: 'Feature',
78 | properties: {
79 | angle: 20
80 | },
81 | geometry: {
82 | type: 'GeometryCollection',
83 | geometries: [
84 | {
85 | type: 'Point',
86 | coordinates: cameraPoint
87 | },
88 | {
89 | type: 'Point',
90 | coordinates: targetPoint
91 | }
92 | ]
93 | }
94 | }
95 |
96 | var options = {
97 | draggable: true
98 | }
99 |
100 | L.geotagPhoto.camera(points, options).addTo(map)
101 | .on('change', function (event) {
102 | // Get camera field of view
103 | // See:
104 | // https://github.com/nypl-spacetime/field-of-view#output
105 | var fieldOfView = this.getFieldOfView()
106 | })
107 | ```
108 |
109 | #### API
110 |
111 | `L.GeotagPhoto.Camera` extends [L.FeatureGroup](http://leafletjs.com/reference-1.0.0.html#featuregroup).
112 |
113 | | Function | Description |
114 | |:------------------------------------------|:------------------------------------------------|
115 | | `L.geotagPhoto.camera(feature, options?)` | Creation, `feature` is [input for field-of-view](https://github.com/nypl-spacetime/field-of-view#input) |
116 | | `getFieldOfView ()` | Returns [field of view of camera](https://github.com/nypl-spacetime/field-of-view#output) |
117 | | `getCameraLatLng ()` | Returns camera's [`LatLng`](http://leafletjs.com/reference-1.0.0.html#latlng) |
118 | | `getTargetLatLng ()` | Returns target's [`LatLng`](http://leafletjs.com/reference-1.0.0.html#latlng) |
119 | | `getCameraPoint ()` | Returns camera's GeoJSON Point |
120 | | `getTargetPoint ()` | Returns target's GeoJSON Point |
121 | | `getCenter ()` | Returns [`LatLng`](http://leafletjs.com/reference-1.0.0.html#latlng) of point halfway camera and target |
122 | | `getBounds ()` | Returns [`LatLngBounds`](http://leafletjs.com/reference-1.0.0.html#latlngbounds) of field of view triangle |
123 | | `centerBounds (bounds)` | Moves camera and target so their center lies in the middle of `bounds` |
124 | | `setAngle (angle)` | Set angle of view |
125 | | `setCameraLatLng (latLng)` | Set `LatLng` of camera |
126 | | `setTargetLatLng (latLng)` | Set `LatLng` of target |
127 | | `setCameraAndTargetLatLng (cameraLatLng, targetLatLng)` | Set `LatLng` of camera and `LatLng` of target |
128 | | `setDraggable (boolean)` | Toggle between static or draggable camera |
129 |
130 | #### Options
131 |
132 | | Option | Type | Default | Description
133 | |:----------------------|:----------|--------:|:----------------------------------------------------------|
134 | | `draggable` | `Boolean` | `true` | Whether the camera is draggable with mouse/touch or not |
135 | | `angleMarker` | `Boolean` | `true` | Whether the angle of the field-of-view can be changed with a draggable marker
136 | | `minAngle` | `Number` | 5 | Minimum angle of field-of-view
137 | | `maxAngle` | `Number` | 120 | Maximum angle of field-of-view
138 | | `cameraIcon` | [`L.Icon`](http://leafletjs.com/reference-1.0.2.html#icon) | See below | Camera icon
139 | | `targetIcon` | [`L.Icon`](http://leafletjs.com/reference-1.0.2.html#icon) | See below | Target icon
140 | | `angleIcon` | [`L.Icon`](http://leafletjs.com/reference-1.0.2.html#icon) | See below | Angle icon
141 | | `outlineStyle` | [`L.Path options`](http://leafletjs.com/reference-1.0.2.html#path-option) | See below | Style of field-of-view triangle's outline
142 | | `fillStyle` | [`L.Path options`](http://leafletjs.com/reference-1.0.2.html#path-option) | See below | Style of field-of-view triangle's fill polygon
143 | | `control` | `Boolean` | `true` | Whether to show camera control buttons |
144 | | `controlCameraImg` | `String` | `../images/camera-icon.svg` | URL to icon displayed in camera control button
145 | | `controlCrosshairImg` | `String` | `../images/crosshair-icon.svg` | URL to icon displayed in crosshair control button
146 |
147 | ##### Defaults
148 |
149 | ```js
150 | const defaults = {
151 | cameraIcon: L.icon({
152 | iconUrl: '../images/camera.svg',
153 | iconSize: [38, 38],
154 | iconAnchor: [19, 19]
155 | }),
156 |
157 | targetIcon: L.icon({
158 | iconUrl: '../images/marker.svg',
159 | iconSize: [32, 32],
160 | iconAnchor: [16, 16]
161 | }),
162 |
163 | angleIcon: L.icon({
164 | iconUrl: '../images/marker.svg',
165 | iconSize: [32, 32],
166 | iconAnchor: [16, 16]
167 | }),
168 |
169 | outlineStyle: {
170 | color: 'black',
171 | opacity: 0.5,
172 | weight: 2,
173 | dashArray: '5, 7',
174 | lineCap: 'round',
175 | lineJoin: 'round'
176 | },
177 |
178 | fillStyle: {
179 | weight: 0,
180 | fillOpacity: 0.2,
181 | fillColor: '#3388ff'
182 | }
183 | }
184 | ```
185 |
186 | #### Keyboard navigation
187 |
188 | - Use `tab` to switch between map, camera and target
189 | - Use arrow keys to move map, camera or target
190 | - Press `C` to move camera to center of current map view
191 | - Press `M` to center map on current camera position
192 |
193 | ## Building & Publishing
194 |
195 | To build the plugin, run:
196 |
197 | npm run build
198 |
199 | The resulting files will be available in the `dist` directory.
200 |
201 | To publish the plugin to [npm](https://www.npmjs.com/package/leaflet-geotag-photo) and [unpkg.com](https://unpkg.com/leaflet-geotag-photo), run:
202 |
203 | npm publish
204 |
205 | ## See also
206 |
207 | - http://spacetime.nypl.org/
208 | - https://github.com/nypl-spacetime/field-of-view
209 | - https://github.com/nypl-spacetime/surveyor
210 | - http://turfjs.org/
211 |
--------------------------------------------------------------------------------
/src/Leaflet.GeotagPhoto.Camera.js:
--------------------------------------------------------------------------------
1 | import L from 'leaflet'
2 | import { fromFeature } from 'field-of-view'
3 |
4 | import GeotagPhotoCameraControl from './Leaflet.GeotagPhoto.CameraControl'
5 |
6 | L.geotagPhotoCameraControl = function (geotagPhotoCamera, options) {
7 | return new GeotagPhotoCameraControl(geotagPhotoCamera, options)
8 | }
9 |
10 | export default L.FeatureGroup.extend({
11 |
12 | options: {
13 | // Whether the camera is draggable with mouse/touch or not
14 | draggable: true,
15 |
16 | // Whether to show camera control buttons
17 | control: true,
18 |
19 | // Whether the angle of the field-of-view can be changed with a draggable marker
20 | angleMarker: true,
21 |
22 | minAngle: 5,
23 | maxAngle: 120,
24 |
25 | // Control button images
26 | controlCameraImg: '../images/camera-icon.svg',
27 | controlCrosshairImg: '../images/crosshair-icon.svg',
28 |
29 | cameraIcon: L.icon({
30 | iconUrl: '../images/camera.svg',
31 | iconSize: [38, 38],
32 | iconAnchor: [19, 19]
33 | }),
34 |
35 | targetIcon: L.icon({
36 | iconUrl: '../images/marker.svg',
37 | iconSize: [32, 32],
38 | iconAnchor: [16, 16]
39 | }),
40 |
41 | angleIcon: L.icon({
42 | iconUrl: '../images/marker.svg',
43 | iconSize: [32, 32],
44 | iconAnchor: [16, 16]
45 | }),
46 |
47 | outlineStyle: {
48 | color: 'black',
49 | opacity: 0.5,
50 | weight: 2,
51 | dashArray: '5, 7',
52 | lineCap: 'round',
53 | lineJoin: 'round'
54 | },
55 |
56 | fillStyle: {
57 | weight: 0,
58 | fillOpacity: 0.2,
59 | fillColor: '#3388ff'
60 | }
61 | },
62 |
63 | initialize: function (feature, options) {
64 | L.setOptions(this, options)
65 |
66 | this.options.minAngle = Math.max(this.options.minAngle, 1)
67 | this.options.maxAngle = Math.min(this.options.maxAngle, 179)
68 |
69 | this._fieldOfView = fromFeature(feature)
70 | this._angle = this._fieldOfView.properties.angle
71 |
72 | var layers = this._createLayers()
73 | L.LayerGroup.prototype.initialize.call(this, layers)
74 |
75 | this.setDraggable(this.options.draggable)
76 | },
77 |
78 | _createLayers: function () {
79 | this._cameraIcon = this.options.cameraIcon
80 | this._targetIcon = this.options.targetIcon
81 | this._angleIcon = this.options.angleIcon
82 |
83 | var pointList = this._getPointList(this._fieldOfView)
84 | var cameraLatLng = this._getCameraFromPointList(pointList)
85 | var targetLatLng = this._getTargetFromPointList(pointList)
86 | var angleLatLng = this._getAngleFromPointList(pointList)
87 |
88 | this._polyline = L.polyline(pointList, this.options.outlineStyle)
89 | this._polygon = L.polygon(pointList, Object.assign(this.options.fillStyle, {
90 | className: 'field-of-view'
91 | }))
92 |
93 | this._control = L.geotagPhotoCameraControl(this, {
94 | cameraImg: this.options.controlCameraImg,
95 | crosshairImg: this.options.controlCrosshairImg
96 | })
97 |
98 | this._cameraMarker = L.marker(cameraLatLng, {
99 | icon: this._cameraIcon,
100 | draggable: this.options.draggable,
101 | zIndexOffset: 600,
102 | title: 'Camera',
103 | alt: 'Location of the camera'
104 | }).on('drag', this._onMarkerDrag, this)
105 | .on('dragend', this._onMarkerDragEnd, this)
106 |
107 | this._targetMarker = L.marker(targetLatLng, {
108 | icon: this._targetIcon,
109 | draggable: this.options.draggable,
110 | zIndexOffset: 200,
111 | title: 'Target',
112 | alt: 'Location of the target'
113 | }).on('drag', this._onMarkerDrag, this)
114 | .on('dragend', this._onMarkerDragEnd, this)
115 |
116 | this._angleMarker = L.marker(angleLatLng, {
117 | icon: this._angleIcon,
118 | draggable: this.options.draggable,
119 | zIndexOffset: 400,
120 | title: 'Angle',
121 | alt: 'Field of view angle'
122 | }).on('drag', this._onAngleMarkerDrag, this)
123 | .on('dragend', this._onMarkerDragEnd, this)
124 |
125 | var boundUpdateMarkerBearings = this._updateMarkerBearings.bind(this)
126 | var markerSetPos = function (pos) {
127 | var protoMarkerSetPos = L.Marker.prototype._setPos
128 | protoMarkerSetPos.call(this, pos)
129 | boundUpdateMarkerBearings()
130 | }
131 |
132 | this._cameraMarker._setPos = this._targetMarker._setPos = markerSetPos
133 |
134 | return [
135 | this._polygon,
136 | this._polyline,
137 | this._targetMarker,
138 | this._angleMarker,
139 | this._cameraMarker
140 | ]
141 | },
142 |
143 | addTo: function (map) {
144 | this._map = map
145 |
146 | L.FeatureGroup.prototype.addTo.call(this, map)
147 |
148 | if (this.options.control) {
149 | this._control.addTo(map)
150 | }
151 |
152 | this._boundOnDocumentKeyDown = this._onDocumentKeyDown.bind(this)
153 | document.addEventListener('keydown', this._boundOnDocumentKeyDown)
154 |
155 | this.setDraggable(this.options.draggable)
156 | this._updateMarkerBearings(this._fieldOfView)
157 |
158 | return this
159 | },
160 |
161 | removeFrom: function (map) {
162 | this._map = undefined
163 |
164 | L.FeatureGroup.prototype.removeFrom.call(this, map)
165 |
166 | if (this._boundOnDocumentKeyDown) {
167 | document.removeEventListener('keydown', this._boundOnDocumentKeyDown)
168 | }
169 |
170 | return this
171 | },
172 |
173 | _getPointList: function (fieldOfView) {
174 | return [
175 | [
176 | fieldOfView.geometry.geometries[1].coordinates[0][1],
177 | fieldOfView.geometry.geometries[1].coordinates[0][0]
178 | ],
179 | [
180 | fieldOfView.geometry.geometries[0].coordinates[1],
181 | fieldOfView.geometry.geometries[0].coordinates[0]
182 | ],
183 | [
184 | fieldOfView.geometry.geometries[1].coordinates[1][1],
185 | fieldOfView.geometry.geometries[1].coordinates[1][0]
186 | ]
187 | ]
188 | },
189 |
190 | _getCameraFromPointList: function (pointList) {
191 | return pointList[1]
192 | },
193 |
194 | _getTargetFromPointList: function (pointList) {
195 | return [
196 | (pointList[0][0] + pointList[2][0]) / 2,
197 | (pointList[0][1] + pointList[2][1]) / 2
198 | ]
199 | },
200 |
201 | _getAngleFromPointList: function (pointList) {
202 | return pointList[2]
203 | },
204 |
205 | _addRotateTransform: function (element, rotation) {
206 | if (!element) {
207 | return
208 | }
209 |
210 | var transform = element.style[L.DomUtil.TRANSFORM]
211 | var rotate = 'rotate(' + rotation + ')'
212 |
213 | element.style.transformOrigin = 'center center'
214 |
215 | if (transform.indexOf('rotate') !== -1) {
216 | element.style[L.DomUtil.TRANSFORM] = transform.replace(/rotate\(.*?\)/, rotate)
217 | } else {
218 | element.style[L.DomUtil.TRANSFORM] = transform + ' ' + rotate
219 | }
220 | },
221 |
222 | _updateMarkerBearings: function (fieldOfView) {
223 | fieldOfView = fieldOfView || this._fieldOfView
224 |
225 | var bearing = fieldOfView.properties.bearing
226 | var angle = fieldOfView.properties.angle
227 | this._addRotateTransform(this._cameraMarker._icon, bearing + 'deg')
228 | this._addRotateTransform(this._targetMarker._icon, bearing + 'deg')
229 | this._addRotateTransform(this._angleMarker._icon, (bearing + angle / 2) + 'deg')
230 | },
231 |
232 | _drawFieldOfView: function (fieldOfView) {
233 | fieldOfView = fieldOfView || this._fieldOfView
234 |
235 | var pointList = this._getPointList(fieldOfView)
236 | this._polyline.setLatLngs(pointList)
237 | this._polygon.setLatLngs(pointList)
238 | },
239 |
240 | _updateFieldOfView: function () {
241 | var angle = this._angle
242 | var cameraLatLng = this._cameraMarker.getLatLng()
243 | var targetLatLng = this._targetMarker.getLatLng()
244 |
245 | var cameraTarget = {
246 | type: 'Feature',
247 | properties: {
248 | angle: angle
249 | },
250 | geometry: {
251 | type: 'GeometryCollection',
252 | geometries: [
253 | this._geoJsonPoint(cameraLatLng),
254 | this._geoJsonPoint(targetLatLng)
255 | ]
256 | }
257 | }
258 |
259 | this._fieldOfView = fromFeature(cameraTarget)
260 |
261 | var angleLatLng = this._getAngleFromPointList(this._getPointList(this._fieldOfView))
262 | this._angleMarker.setLatLng(angleLatLng)
263 |
264 | this._updateMarkerBearings(this._fieldOfView)
265 | this._drawFieldOfView(this._fieldOfView)
266 | },
267 |
268 | _onAngleMarkerDrag: function (evt) {
269 | var cameraLatLng = this._cameraMarker.getLatLng()
270 | var targetLatLng = this._targetMarker.getLatLng()
271 | var angleLatLng = this._angleMarker.getLatLng()
272 |
273 | var points = {
274 | type: 'Feature',
275 | geometry: {
276 | type: 'GeometryCollection',
277 | geometries: [
278 | this._geoJsonPoint(cameraLatLng),
279 | this._geoJsonPoint(targetLatLng),
280 | this._geoJsonPoint(angleLatLng)
281 | ]
282 | }
283 | }
284 |
285 | this._fieldOfView = fromFeature(points)
286 | this.setAngle(this._fieldOfView.properties.angle)
287 | },
288 |
289 | _onMarkerDrag: function (evt) {
290 | this._updateFieldOfView()
291 | this.fire('input')
292 | },
293 |
294 | _onMarkerDragEnd: function (evt) {
295 | this.fire('change')
296 | },
297 |
298 | _moveMarker: function (marker, offset) {
299 | var point = this._map.latLngToContainerPoint(marker.getLatLng())
300 | point = point.add(offset)
301 | var latLng = this._map.containerPointToLatLng(point)
302 | marker.setLatLng(latLng)
303 |
304 | this._updateFieldOfView()
305 | this.fire('change')
306 | },
307 |
308 | _onMarkerKeyDown: function (marker, evt) {
309 | var moveDelta = 20
310 | if (evt.shiftKey) {
311 | moveDelta = moveDelta * 4
312 | }
313 |
314 | if (evt.keyCode === 37) {
315 | // left
316 | this._moveMarker(marker, L.point(-moveDelta, 0))
317 | } else if (evt.keyCode === 38) {
318 | // up
319 | this._moveMarker(marker, L.point(0, -moveDelta))
320 | } else if (evt.keyCode === 39) {
321 | // right
322 | this._moveMarker(marker, L.point(moveDelta, 0))
323 | } else if (evt.keyCode === 40) {
324 | // down
325 | this._moveMarker(marker, L.point(0, moveDelta))
326 | }
327 | },
328 |
329 | _onAngleMarkerKeyDown: function (evt) {
330 | var angleDelta = 2.5
331 | if (evt.shiftKey) {
332 | angleDelta = angleDelta * 4
333 | }
334 |
335 | if (evt.keyCode === 37) {
336 | // left
337 | this.setAngle(this._angle - angleDelta)
338 | } else if (evt.keyCode === 39) {
339 | // right
340 | this.setAngle(this._angle + angleDelta)
341 | }
342 | },
343 |
344 | _onDocumentKeyDown: function (evt) {
345 | if (document.activeElement === this._cameraMarker._icon) {
346 | this._onMarkerKeyDown(this._cameraMarker, evt)
347 | } else if (document.activeElement === this._targetMarker._icon) {
348 | this._onMarkerKeyDown(this._targetMarker, evt)
349 | } else if (document.activeElement === this._angleMarker._icon) {
350 | this._onAngleMarkerKeyDown(evt)
351 | }
352 | },
353 |
354 | _setMarkerVisible: function (marker, visible) {
355 | marker._icon.style.display = visible ? 'inherit' : 'none'
356 | },
357 |
358 | _geoJsonPoint: function (latLng) {
359 | return {
360 | type: 'Point',
361 | coordinates: [latLng.lng, latLng.lat]
362 | }
363 | },
364 |
365 | getFieldOfView: function () {
366 | return this._fieldOfView
367 | },
368 |
369 | getCameraLatLng: function () {
370 | return this._cameraMarker.getLatLng()
371 | },
372 |
373 | getTargetLatLng: function () {
374 | return this._targetMarker.getLatLng()
375 | },
376 |
377 | getCameraPoint: function () {
378 | return this._geoJsonPoint(this.getCameraLatLng())
379 | },
380 |
381 | getTargetPoint: function () {
382 | return this._geoJsonPoint(this.getTargetLatLng())
383 | },
384 |
385 | getCenter: function () {
386 | if (!this._map) {
387 | return
388 | }
389 |
390 | return L.latLngBounds([
391 | this.getCameraLatLng(),
392 | this.getTargetLatLng()
393 | ]).getCenter()
394 | },
395 |
396 | centerBounds: function (bounds) {
397 | var cameraBounds = this.getBounds()
398 |
399 | if (!bounds.contains(cameraBounds)) {
400 | var center = this.getCenter()
401 | var cameraLatLng = this.getCameraLatLng()
402 | var targetLatLng = this.getTargetLatLng()
403 |
404 | var boundsCenter = bounds.getCenter()
405 |
406 | var newCameraLatLng = [
407 | boundsCenter.lat - (center.lat - cameraLatLng.lat),
408 | boundsCenter.lng - (center.lng - cameraLatLng.lng)
409 | ]
410 |
411 | var newTargetLatLng = [
412 | boundsCenter.lat - (center.lat - targetLatLng.lat),
413 | boundsCenter.lng - (center.lng - targetLatLng.lng)
414 | ]
415 |
416 | this.setCameraAndTargetLatLng(newCameraLatLng, newTargetLatLng)
417 | }
418 | },
419 |
420 | setCameraLatLng: function (latLng) {
421 | if (!this._map) {
422 | return
423 | }
424 |
425 | this._cameraMarker.setLatLng(latLng)
426 | this._updateFieldOfView()
427 | this.fire('change')
428 | },
429 |
430 | setTargetLatLng: function (latLng) {
431 | if (!this._map) {
432 | return
433 | }
434 |
435 | this._targetMarker.setLatLng(latLng)
436 | this._updateFieldOfView()
437 | this.fire('change')
438 | },
439 |
440 | setCameraAndTargetLatLng: function (cameraLatLng, targetLatLng) {
441 | if (!this._map) {
442 | return
443 | }
444 |
445 | this._cameraMarker.setLatLng(cameraLatLng)
446 | this._targetMarker.setLatLng(targetLatLng)
447 | this._updateFieldOfView()
448 | this.fire('change')
449 | },
450 |
451 | getBounds: function () {
452 | if (!this._fieldOfView) {
453 | return
454 | }
455 |
456 | var pointList = this._getPointList(this._fieldOfView)
457 | return L.latLngBounds(pointList)
458 | },
459 |
460 | setAngle: function (angle) {
461 | this._angle = Math.max(Math.min(angle, this.options.maxAngle), this.options.minAngle)
462 | this._updateFieldOfView()
463 | this.fire('input')
464 | },
465 |
466 | setDraggable: function (draggable) {
467 | if (!this._map) {
468 | return
469 | }
470 |
471 | if (draggable) {
472 | this._cameraMarker.dragging.enable()
473 | this._targetMarker.dragging.enable()
474 | this._angleMarker.dragging.enable()
475 | } else {
476 | this._cameraMarker.dragging.disable()
477 | this._targetMarker.dragging.disable()
478 | this._angleMarker.dragging.disable()
479 | }
480 |
481 | this._setMarkerVisible(this._targetMarker, draggable)
482 | this._setMarkerVisible(this._angleMarker, draggable && this.options.angleMarker)
483 | }
484 |
485 | })
486 |
--------------------------------------------------------------------------------