├── .gitignore ├── README.md ├── control └── permalinkcontrol.css ├── georef.css ├── georef.js ├── helmerttransform.js ├── index.html ├── package.json ├── screenshot.jpg ├── serve.js ├── wapp.ctrl.js └── wapp.img.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Map-georeferencer 2 | An Proof of concept to georeference maps with Openlayers (ol). 3 | 4 | > [!WARNING] 5 | > Map-georefenrecer is now available in [IGNF-Ma-carte](https://github.com/IGNF-Ma-carte/mcgeoimage). See it [online](https://ignf-ma-carte.github.io/mcgeoimage/). 6 | 7 | ## Description 8 | 9 | [View it online](http://viglino.github.io/Map-georeferencer/) 10 | 11 | [![screenshot](screenshot.jpg)](http://viglino.github.io/Map-georeferencer/) 12 | 13 | You can download an image or give a valid link to get it online. 14 | The image will appear on the left side and on the right side is the reference map. 15 | Find same places on both maps. 16 | A minimum of two control points are required to calculate the geographical position of the map. 17 | The more control points that are added, the more precise the georeference is. 18 | 19 | The transformation model is an [Helmert](https://en.wikipedia.org/wiki/Helmert_transformation) or an affine transform. 20 | 21 | The image map can appear as an overlay on the reference maps. 22 | Side-by-side view is available as well, to compare any two maps next to each other. 23 | 24 | You can crop the image by defining a crop polygon directly on the image. 25 | 26 | You can get parameters via the tranformation button on the bottom of the screen. 27 | This transformation can be used with an [ol.source.geoimagesource](https://github.com/Viglino/ol3-ext/blob/gh-pages/layer/geoimagesource.js) in an ol map. 28 | 29 | ## Dependencies 30 | 31 | Map-georeferencer use [ol](https://github.com/openlayers/ol3), [jQuery](https://jquery.com/) and [ol-ext](https://github.com/Viglino/ol-ext). 32 | 33 | ## Licence 34 | 35 | Map-georeferencer is licenced under the French Opensource BSD like CeCILL-B FREE SOFTWARE LICENSE. 36 | (c) 2016 - Jean-Marc Viglino 37 | 38 | Some resources (mapping services and API) used in this sofware may have a specific license. 39 | You must check before use. 40 | 41 | Full text license in English: (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt) 42 | Full text license in French: (http://www.cecill.info/licences/Licence_CeCILL-C_V1-fr.txt) 43 | -------------------------------------------------------------------------------- /control/permalinkcontrol.css: -------------------------------------------------------------------------------- 1 | .ol-permalink 2 | { position: absolute; 3 | top:0.5em; 4 | right: 2.5em; 5 | } 6 | .ol-touch .ol-permalink 7 | { right: 3em; 8 | } 9 | 10 | .ol-permalink button 11 | { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AcFBjYE1ZK03gAAAUlJREFUOMuVk71KA1EQhc/NaiP+gCRpFHwGBSFlCrFVfAsbwSJCBMv06QIGJOBziI3EYAgkjU8gIloIAasIn4WzMqx34zrN7J6de+6ZmbNSgQDSfADcATPgHbgCyvonSYv8KEzWdofegH3gwmG9Ikq67sAESFzNueHThTyiEIKAmr2OJCUhhO30Aou+5aUQU2Ik65K2JC1KegohPGfUBkmvksqShnntHEcGOs60NXHfjmKz6czZTsNqbhzW+muwY2ATWAWawCOwBgxcTfvnvCPxKx4Cy5bPgBWgauRpdL2ImNlGhp3MabETm8mh94nDk4yCNE5/KTGg7xxbyhYAG0AN2AEqURIDZ0a0Fxn+LXAPXDpzRqMk6cOedz1ubdYl1b6NHgZRJe72nuu/CdSBl+yKi/zZlTnbaeXOJIesClwDU+ATeEhtX5TkCwAWUyAsHH1QAAAAAElFTkSuQmCC'); 12 | background-position: center; 13 | background-repeat: no-repeat; 14 | } -------------------------------------------------------------------------------- /georef.css: -------------------------------------------------------------------------------- 1 | html 2 | { font-family: "Trebuchet MS",Helvetica,sans-serif; 3 | } 4 | #map, 5 | #img 6 | { position:fixed; 7 | width:50%; 8 | height:100%; 9 | top:0; bottom:0; 10 | cursor: crosshair; 11 | } 12 | #map 13 | { right:0; 14 | } 15 | #img 16 | { left:0; 17 | } 18 | 19 | #loading img 20 | { max-height: 200px; 21 | display: block; 22 | margin: auto; 23 | } 24 | 25 | .fullpage #img 26 | { top:auto; 27 | width:200px; 28 | height:200px; 29 | z-index:2; 30 | border-radius:0 10px 0 0; 31 | background:#fff; 32 | padding:5px; 33 | } 34 | .fullpage #img .ol-control 35 | { display: none; 36 | } 37 | .fullpage #img .ol-fullpage 38 | { display: block; 39 | } 40 | .fullpage #img .ol-viewport 41 | { border-radius:0 10px 0 0; 42 | } 43 | 44 | .fullpage #map 45 | { left:0; 46 | width:100%; 47 | } 48 | 49 | .ol-mask, 50 | .overview, 51 | .ol-helmert 52 | { top:0.5em; 53 | right:0.5em; 54 | } 55 | .ol-helmert 56 | { top:0.5em; 57 | right:2.5em; 58 | } 59 | 60 | .ol-helmert.ol-active button 61 | { background-color: rgba(0, 136, 60, 0.5); 62 | } 63 | .ol-helmert.ol-active button:focus, 64 | .ol-helmert.ol-active button:hover 65 | { background-color: rgba(0, 136, 60, 0.9); 66 | } 67 | 68 | .ol-fullpage 69 | { bottom:0.5em; 70 | left:0.5em; 71 | } 72 | .ol-fullpage button:before 73 | { content: "\00ab" 74 | } 75 | .fullpage .ol-fullpage button:before 76 | { content: "\00bb" 77 | } 78 | 79 | .fullpage .ol-fullpage 80 | { position:fixed; 81 | z-index:10; 82 | } 83 | 84 | .dialog .modal 85 | { position:fixed; 86 | width:100%; 87 | height:100%; 88 | left:0; right:0; 89 | top:0; bottom:0; 90 | background-color:rgba(0,0,0,0.5); 91 | } 92 | .dialog .inner 93 | { position:absolute; 94 | left:50%; 95 | top: 20%; 96 | margin-left:-250px; 97 | width:500px; 98 | background:#fff; 99 | border:3px solid #369; 100 | border-radius:5px; 101 | padding:1em; 102 | box-sizing: border-box; 103 | overflow: hidden; 104 | transition:all 0.5s; 105 | -webkit-transition:all 0.5s; 106 | } 107 | .dialog.hidden .inner 108 | { transform: scale(0); 109 | -webkit-transform: scale(0); 110 | } 111 | .dialog.hidden .modal 112 | { display:none; 113 | } 114 | 115 | 116 | .btn 117 | { color:#fff; 118 | background:#369; 119 | padding:0.5em; 120 | text-decoration:none; 121 | cursor:pointer; 122 | display:inline-block; 123 | box-shadow: 4px 4px 8px rgba(0,0,0,0.5); 124 | } 125 | 126 | h1 127 | { color:#369; 128 | margin: 0.25em 0; 129 | } 130 | 131 | .ol-mouse-position 132 | { background: rgba(255,255,255,0.5); 133 | top:auto; 134 | right:auto; 135 | bottom:8px; 136 | left: 3em; 137 | padding:0 0.5em; 138 | } 139 | 140 | .ol-info 141 | { right: 0.5em; 142 | bottom:0.5em; 143 | } 144 | .ol-info button 145 | { padding-bottom:0.3em; 146 | } 147 | -------------------------------------------------------------------------------- /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}: 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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 8 | Map-georeferencer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 57 | 58 | 59 | 60 |
61 | 62 |
63 | 64 | Fork me on Github 65 | 66 |
67 | 68 | 69 |
70 |

Choose an image

71 | Upload a file: 72 | 73 |
74 | ...or choose a remote image URL (http/s) 75 |
76 | 80 | 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "live-server": "^1.2.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viglino/Map-georeferencer/b59bbfc10a55a0fd45a7edf5c6b50565050f55e2/screenshot.jpg -------------------------------------------------------------------------------- /serve.js: -------------------------------------------------------------------------------- 1 |  2 | var liveServer = require("live-server"); 3 | 4 | var params = { 5 | port: 8181, // Set the server port. Defaults to 8080. 6 | host: "0.0.0.0", // Set the address to bind to. Defaults to 0.0.0.0 or process.env.IP. 7 | open: false, // When false, it won't load your browser by default. 8 | root: "./", // Set root directory that's being served. Defaults to cwd. 9 | watch: ['./'], // comma-separated string for paths to watch 10 | // ignore: '', // comma-separated string for paths to ignore 11 | file: "index.html", // When set, serve this file (server root relative) for every 404 (useful for single-page applications) 12 | wait: 1000, // Waits for all changes, before reloading. Defaults to 0 sec. 13 | logLevel: 2 // 0 = errors only, 1 = some, 2 = lots 14 | }; 15 | liveServer.start(params); 16 | -------------------------------------------------------------------------------- /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 | $("