├── .gitignore ├── package.json ├── screenshot.jpg ├── wapp.ctrl.js ├── serve.js ├── control └── permalinkcontrol.css ├── README.md ├── georef.css ├── index.html ├── helmerttransform.js ├── georef.js └── wapp.img.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "live-server": "^1.2.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viglino/Map-georeferencer/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /wapp.ctrl.js: -------------------------------------------------------------------------------- 1 | /** A simple toggle control with a callback function 2 | */ 3 | 4 | var toggleCtrl = function(options) 5 | { var element = $("
").addClass(options['class'] + ' ol-unselectable ol-control'+ (options.on ? " ol-active":"")); 6 | 7 | function toggle(e) 8 | { element.toggleClass("ol-active"); 9 | e.preventDefault(); 10 | options.toggleFn(element.hasClass("ol-active")); 11 | }; 12 | 13 | $(" 81 |
82 |
83 | 84 | Click points on the left image and on the right map. 85 |
86 | Alt+click point on the map to remove it. 87 |
88 | Click+drag to move points. 89 |
90 | Save the map and drag and drop the file to add the georeferenced layer on the map. 91 |
92 |
93 | 94 | 95 | 104 | 105 | 113 | 114 | -------------------------------------------------------------------------------- /helmerttransform.js: -------------------------------------------------------------------------------- 1 | if (!ol.transform) ol.transform = {}; 2 | 3 | /** Helmert transformation is a transformation method within a three-dimensional space. 4 | * It is frequently used in geodesy to produce distortion-free transformations from one datum to another. 5 | * It is composed of scaling o rotation o translation 6 | * Least squares is used to solve the problem of determining the parameters. 7 | * [X] = [sx] . [cos -sin] + [tx] 8 | * [Y] [sy] [sin cos] [ty] 9 | * 10 | * With the similarity option the scale is the same along both axis ie. sx = sy 11 | */ 12 | ol.transform.Helmert = function (options) 13 | { if (!options) options={}; 14 | this.similarity = options.similarity; 15 | this.matrix = [1,0,0,0,1,0]; 16 | this.hasControlPoints = false; 17 | } 18 | 19 | /** Calculate the helmert transform with control points. 20 | * @return {Array.}: source coords 21 | * @return {Array.: projected coords 22 | */ 23 | ol.transform.Helmert.prototype.setControlPoints = function(xy, XY) 24 | { if (xy.length<2) 25 | { this.matrix = [1,0,0,0,1,0]; 26 | this.hasControlPoints = false; 27 | } 28 | else 29 | { if (this.similarity || xy.length<3) this.matrix = this._similarity ( xy, XY ); 30 | else this.matrix = this._helmert ( xy, XY ); 31 | this.hasControlPoints = true; 32 | } 33 | return this.hasControlPoints; 34 | } 35 | 36 | /** Get the rotation of the transform 37 | * @return {Number}: angle 38 | */ 39 | ol.transform.Helmert.prototype.getRotation = function() 40 | { return this.a_; 41 | } 42 | 43 | /** Get the scale of the transform 44 | * @return {ol.Coordinate}: scale along x and y axis 45 | */ 46 | ol.transform.Helmert.prototype.getScale = function() 47 | { return this.sc_ 48 | } 49 | 50 | /** Get the rotation of the translation 51 | * @return {ol.Coordinate}: translation 52 | */ 53 | ol.transform.Helmert.prototype.getTranslation = function() 54 | { return this.tr_; 55 | } 56 | 57 | /** Transform a point 58 | * @param {ol.Coordinate}: coordinate in the origin datum 59 | * @return {ol.Coordinate}: coordinate in the destination datum 60 | */ 61 | ol.transform.Helmert.prototype.transform = function(xy) 62 | { var m = this.matrix; 63 | return [ m[0]*xy[0] + m[1]*xy[1] +m[2], m[3]*xy[0] + m[4]*xy[1] +m[5] ]; 64 | } 65 | 66 | /** Revers transform of a point 67 | * @param {ol.Coordinate}: coordinate in the destination datum 68 | * @return {ol.Coordinate}: coordinate in the origin datum 69 | */ 70 | ol.transform.Helmert.prototype.revers = function(xy) 71 | { var a = this.matrix[0]; 72 | var b = this.matrix[1]; 73 | var c = this.matrix[3]; 74 | var d = this.matrix[4]; 75 | var p = this.matrix[2]; 76 | var q = this.matrix[5]; 77 | return [ 78 | (d*xy[0] - b*xy[1] +b*q - p*d) / (a*d-b*c), 79 | (-c*xy[0] + a*xy[1] + c*p - a*q) / (a*d-b*c), 80 | ]; 81 | } 82 | 83 | /** 84 | Transformee de Helmert au moindre carre : 85 | Somme ( carre (a*xy + b - XY) ) minimale 86 | avec A de la forme : 87 | [a -b] 88 | [b a] 89 | **/ 90 | ol.transform.Helmert.prototype._similarity = function( xy, XY ) 91 | { if ( !xy.length || xy.length != XY.length ) 92 | { console.log ("Helmert : Taille des tableaux de points incompatibles"); 93 | return false; 94 | } 95 | var i; // Variable de boucle 96 | var n = XY.length; // nb points de calage 97 | var a=1,b=0,p=0,q=0; 98 | 99 | // Barycentre 100 | var mxy = { x:0 , y:0 }; 101 | var mXY = { x:0 , y:0 }; 102 | for (i=0; i0) this.a_ *= -1; 144 | this.sc_ = [sc,sc]; 145 | this.tr_ = [p,q]; 146 | 147 | return this.matrix; 148 | } 149 | 150 | 151 | /** 152 | Transformee de Helmert-Etendue au moindre carre : 153 | Somme ( carre (a*xy + b - XY) ) minimale 154 | avec A de la forme : 155 | [a -b][k 0] 156 | [b a][0 h] 157 | **/ 158 | ol.transform.Helmert.prototype._helmert = function (xy, XY, poids, tol) 159 | { if ( !xy.length || xy.length != XY.length ) 160 | { console.log ("Helmert : Taille des tableaux de points incompatibles"); 161 | return false; 162 | } 163 | var i; // Variable de boucle 164 | var n = xy.length; // nb points de calage 165 | // Creation de poids par defaut 166 | if (!poids) poids = []; 167 | if (poids.length == 0 || n != poids.iGetTaille()) 168 | { for (i=0; i div) break; 241 | 242 | // Nouvelle approximation 243 | da = a * Math.cos(dt) - b * Math.sin(dt); 244 | db = b * Math.cos(dt) + a * Math.sin(dt); 245 | a = da; 246 | b = db; 247 | k += dk; 248 | h += dh; 249 | 250 | div = Math.abs(dk) + Math.abs(dh); 251 | } while (Math.abs(dk) + Math.abs(dh) > tol); 252 | 253 | // Retour du repere barycentrique 254 | tx = mXY.x - a*k * mxy.x + b*h * mxy.y; 255 | ty = mXY.y - b*k * mxy.x - a*h * mxy.y; 256 | 257 | this.a_ = Math.acos(a); 258 | if (b>0) this.a_ *= -1; 259 | if (Math.abs(this.a_) < Math.PI/8) 260 | { this.a_ = Math.asin(-b); 261 | if (a<0) this.a_ = Math.PI - this.a_; 262 | } 263 | this.sc_ = [k,h]; 264 | this.tr_ = [tx,ty]; 265 | 266 | // la Solution 267 | this.matrix = []; 268 | this.matrix[0] = a * k; 269 | this.matrix[1] = -b * h; 270 | this.matrix[2] = tx; 271 | this.matrix[3] = b * k; 272 | this.matrix[4] = a * h; 273 | this.matrix[5] = ty; 274 | return this.matrix; 275 | } 276 | -------------------------------------------------------------------------------- /georef.js: -------------------------------------------------------------------------------- 1 | /** HTML5 Georeferencing Image on a map 2 | */ 3 | 4 | 5 | /** jQuery plugin : load an image as dataURL 6 | */ 7 | (function($){ 8 | 9 | jQuery.fn.loadDataURL = function (callback, scope) { 10 | return this.on ('change', function(e) { 11 | // Loop through the FileList 12 | for (var i=0, f; f=e.target.files[i]; i++) { 13 | // Only process image files. 14 | if (!f.type.match('image.*')) continue; 15 | 16 | var reader = new FileReader(); 17 | 18 | // Closure to capture the file information. 19 | reader.onload = (function(file) { 20 | return function(e) { 21 | callback.call (scope, file.name, e.target.result); 22 | }; 23 | })(f); 24 | 25 | // Read in the image file as a data URL. 26 | reader.readAsDataURL(f); 27 | } 28 | }); 29 | }; 30 | })(jQuery) 31 | 32 | 33 | var pixelProjection = new ol.proj.Projection({ 34 | code: 'pixel', 35 | units: 'pixels', 36 | extent: [-100000, -100000, 100000, 100000] 37 | }); 38 | 39 | 40 | /** The webapp 41 | */ 42 | var wapp = { 43 | /** Synchronize maps 44 | */ 45 | synchro: true, 46 | 47 | /** Initialize webapp 48 | */ 49 | initialize: function() { 50 | // Initialize loader 51 | $("#loader input[type=file]").loadDataURL( wapp.load, wapp ); 52 | $("#loader button").click( function() { 53 | var f = $("#loader input[type=text]").val(); 54 | var n = f.split("/").pop(); 55 | n = n.substr(0, n.lastIndexOf('.')) || n; 56 | wapp.load(n, $("#loader input[type=text]").val()); 57 | }); 58 | 59 | // Set the maps 60 | this.setMap(); 61 | this.setImageMap(); 62 | 63 | // Decode source 64 | var p={}, hash = document.location.search; 65 | if (hash) { 66 | hash = hash.replace(/(^#|^\?)/,"").split("&"); 67 | for (var i=0; i", 77 | 'className': "ol-mask", 78 | toggleFn: function(b) 79 | { if (!self.maskFeature) 80 | { layers.imask.setActive(true); 81 | layers.iclick.setActive(false); 82 | } 83 | } 84 | }); 85 | map.addControl(mk); 86 | // New mask added 87 | vector.getSource().on('addfeature', function(e) 88 | { if (layers.imask.getActive()) 89 | { this.maskFeature = e.feature; 90 | layers.imask.setActive(false); 91 | map.removeControl(mk); 92 | if (!this.lastPoint.img) layers.iclick.setActive(true); 93 | this.calc(); 94 | } 95 | }.bind(this)); 96 | 97 | // Modification => calc new transform 98 | var modify = new ol.interaction.Modify( 99 | { features: vector.getSource().getFeaturesCollection(), 100 | deleteCondition: function(event) 101 | { return ol.events.condition.shiftKeyOnly(event) && 102 | ol.events.condition.singleClick(event); 103 | } 104 | }); 105 | map.addInteraction(modify); 106 | modify.on("modifyend", function() 107 | { self.calc(); 108 | }); 109 | 110 | return layers; 111 | } 112 | 113 | 114 | 115 | wapp.img.prototype.getStyle = function(feature) 116 | { if (!feature) return [ 117 | new ol.style.Style ({ 118 | image: new ol.style.Circle( 119 | { radius: 8, 120 | stroke: new ol.style.Stroke( 121 | { color: "#fff", 122 | width: 3 123 | }) 124 | }) 125 | }), 126 | new ol.style.Style ({ 127 | image: new ol.style.Circle( 128 | { radius: 8, 129 | stroke: new ol.style.Stroke( 130 | { color: "#000", 131 | width: 1 132 | }) 133 | }) 134 | })]; 135 | 136 | if (feature.get("isimg")) 137 | { return [ new ol.style.Style ({ 138 | image: new ol.style.RegularShape( 139 | { radius: 10, 140 | points: 4, 141 | stroke: new ol.style.Stroke( 142 | { color: "orange", 143 | width: 2 144 | }) 145 | }) 146 | })]; 147 | } 148 | 149 | return [ new ol.style.Style ({ 150 | image: new ol.style.Circle( 151 | { radius: 8, 152 | stroke: new ol.style.Stroke( 153 | { color: "red", 154 | width: 2 155 | }) 156 | }), 157 | stroke: new ol.style.Stroke( 158 | { color: "red", 159 | width: 2 160 | }) 161 | })]; 162 | } 163 | 164 | /** Add a destination image 165 | */ 166 | wapp.img.prototype.addDest = function(dataURL, map) 167 | { var self = this; 168 | var layers = {}; 169 | 170 | // Helmert ctrl 171 | var hc = new ol.control.Toggle( 172 | { on: !this.getSimilarity(), 173 | html: "H", 174 | 'className': "ol-helmert", 175 | toggleFn: function(b) { self.setSimilarity(!b) } 176 | }); 177 | map.addControl(hc); 178 | 179 | // Show/hide overlay 180 | var ov = new ol.control.Toggle( 181 | { html: "", 182 | 'className': "overview", 183 | toggleFn: function(b) 184 | { if (layers.image) layers.image.setVisible(!layers.image.getVisible()); 185 | } 186 | }); 187 | map.addControl(ov); 188 | map.getLayerGroup().on('change', function(e) 189 | { if (!layers.image) return; 190 | if (layers.image.getVisible()) $("i", ov.element).removeClass("fa-eye-slash"); 191 | else $("i", ov.element).addClass("fa-eye-slash"); 192 | }, this); 193 | 194 | 195 | // Controls points 196 | var vector = layers.vector = new ol.layer.Vector( 197 | { name: 'Vecteur', 198 | source: new ol.source.Vector({ features: new ol.Collection() }), 199 | style: this.getStyle, 200 | // render map control points first 201 | renderOrder: function(f1,f2) { if (f1.get('isimg')) return false; else return true; }, 202 | displayInLayerSwitcher: false 203 | }) 204 | map.addLayer(vector); 205 | 206 | // Draw links 207 | vector.on("precompose", function(e) 208 | { if (!self.transformation.hasControlPoints) return; 209 | var ctx = e.context; 210 | ctx.beginPath(); 211 | ctx.strokeStyle = "blue"; 212 | ctx.strokeWidth = 3; 213 | var ratio = e.frameState.pixelRatio; 214 | 215 | for (var i=0; i calc new transform 239 | var modify = new ol.interaction.Modify( 240 | { features: vector.getSource().getFeaturesCollection(), 241 | deleteCondition: function(event) 242 | { return ol.events.condition.shiftKeyOnly(event) && 243 | ol.events.condition.singleClick(event); 244 | } 245 | }); 246 | map.addInteraction(modify); 247 | 248 | modify.on("modifystart", function(e) 249 | { self.lastChanged_ = null; 250 | }); 251 | modify.on("modifyend", function(e) 252 | { if (self.lastChanged_) 253 | { for (var i=0; i 1); 282 | } 283 | } 284 | 285 | wapp.img.prototype.addControlPoint = function(feature, img) { 286 | if (feature.get('id')) return; 287 | var id = this.lastID_ || 1; 288 | 289 | if (img) { 290 | this.sourceLayer.iclick.setActive(false); 291 | this.lastPoint.img = feature; 292 | feature.set('id',id); 293 | var pt = this.transform (feature.getGeometry().getCoordinates()); 294 | if (pt) { 295 | wapp.map.getView().setCenter(pt); 296 | this.lastPoint.map = new ol.Feature({ id:id, geometry: new ol.geom.Point(pt) }); 297 | this.destLayer.vector.getSource().addFeature(this.lastPoint.map); 298 | } 299 | } else { 300 | this.destLayer.iclick.setActive(false); 301 | this.lastPoint.map = feature; 302 | feature.set('id',id); 303 | 304 | var pt = this.revers (feature.getGeometry().getCoordinates()); 305 | if (pt) 306 | { wapp.mapimg.getView().setCenter(pt); 307 | this.lastPoint.img = new ol.Feature({ id:id, geometry: new ol.geom.Point(pt) }); 308 | this.sourceLayer.vector.getSource().addFeature(this.lastPoint.img); 309 | } 310 | } 311 | 312 | // Add a new Point 313 | if (this.lastPoint.map && this.lastPoint.img) 314 | { this.lastPoint.img2 = new ol.Feature({ isimg:true, id:id, geometry: new ol.geom.Point([0,0]) }); 315 | this.destLayer.vector.getSource().addFeature(this.lastPoint.img2); 316 | var self = this; 317 | this.lastPoint.img2.on ("change", function(e) 318 | { self.lastChanged_ = e.target; 319 | }); 320 | 321 | this.lastPoint.id = id; 322 | this.controlPoints.push(this.lastPoint); 323 | this.lastID_ = ++id; 324 | 325 | this.lastPoint = {}; 326 | this.calc(); 327 | this.sourceLayer.iclick.setActive(true); 328 | this.destLayer.iclick.setActive(true); 329 | } 330 | } 331 | 332 | /** Calc a new transformation 333 | */ 334 | wapp.img.prototype.calc = function() 335 | { if (!this.controlPoints) return; 336 | 337 | if (this.controlPoints.length > 1) 338 | { 339 | var xy=[], XY=[]; 340 | for (var i=0; i