/* * imgViewer2 * * * Copyright (c) 2013 Wayne Mogg * Licensed under the MIT license. */ var waitForFinalEvent = (function () { var timers = {}; return function (callback, ms, uniqueId) { if (!uniqueId) { uniqueId = "Don't call this twice without a uniqueId"; } if (timers[uniqueId]) { clearTimeout (timers[uniqueId]); } timers[uniqueId] = setTimeout(callback, ms); }; })(); /* * imgViewer2 plugin starts here */ ;(function($) { $.widget("wgm.imgViewer2", { options: { zoomStep: 0.5, zoomMax: undefined, zoomable: true, dragable: true, onClick: $.noop, onReady: $.noop }, _create: function() { var self = this; if (!$(this.element).is("img")) { var elem = this.element.children()[0]; if (!$(elem).is("img")) { $.error('imgviewer plugin can only be applied to img elements'); } else { self.img = self.element.children()[0]; } } else { self.img = self.element[0]; } // the original img element var $img = $(self.img); /* * a copy of the original image to be positioned over it and manipulated to * provide zoom and pan */ self.view = $("
").uniqueId().appendTo("body"); var $view = $(self.view); self.map = {}; self.bounds = {}; // a flag used to check the target image has loaded self.ready = false; self.resize = false; $img.one("load",function() { // get and some geometry information about the image self.ready = true; var width = $img.width(), height = $img.height(), offset = $img.offset(); // cache the image padding information self.offsetPadding = { top: parseInt($img.css('padding-top'),10), left: parseInt($img.css('padding-left'),10), right: parseInt($img.css('padding-right'),10), bottom: parseInt($img.css('padding-bottom'),10) }; /* * cache the image margin/border size information * because of IE8 limitations left and right borders are assumed to be the same width * and likewise top and bottom borders */ self.offsetBorder = { x: Math.round(($img.outerWidth()-$img.innerWidth())/2), y: Math.round(($img.outerHeight()-$img.innerHeight())/2) }; /* * define the css style for the view container using absolute positioning to * put it directly over the original image */ var vTop = offset.top + self.offsetBorder.y + self.offsetPadding.top, vLeft = offset.left + self.offsetBorder.x + self.offsetPadding.left; $view.css({ position: "absolute", overflow: "hidden", top: vTop+"px", left: vLeft+"px", width: width+"px", height: height+"px" }); // add the leaflet map self.bounds = L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth)); self.map = L.map($view.attr('id'), {crs:L.CRS.Simple, minZoom: -10, trackresize: false, maxBoundsViscosity: 1.0, attributionControl: false, inertia: true, zoomSnap: 0, wheelPxPerZoomLevel: Math.round(36/self.options.zoomStep), zoomDelta: self.options.zoomStep }); self.zimg = L.imageOverlay(self.img.src, self.bounds).addTo(self.map); self.map.options.minZoom = self.map.getBoundsZoom(self.bounds,false); self.map.fitBounds(self.bounds); self.bounds = self.map.getBounds(); self.map.setMaxBounds(self.bounds); if (self.options.zoomMax !== null) { var lzoom = self.leafletZoom(self.options.zoomMax); if (lzoom < self.map.getZoom()) { self.map.setZoom(lzoom); } self.map.options.maxZoom = lzoom; } if (!self.options.dragable) { self.map.dragging.disable(); } if (!self.options.zoomable) { self.map.zoomControl.disable(); self.map.boxZoom.disable(); self.map.touchZoom.disable(); self.map.doubleClickZoom.disable(); self.map.scrollWheelZoom.disable(); } self.map.on('click', function(ev) { if (self.options.onClick !== null) { self.options.onClick.call(self, ev.originalEvent, self.eventToImg(ev)); } }); self.map.on('zoomend', function() { if (self.options.zoomMax >= 1 && this.getZoom() > this.options.zoomMax) { this.setZoom(this.options.zoomMax); } if (!self.resize) { self.bounds = self.map.getBounds(); } }); self.map.on('moveend', function() { if (!self.resize) { self.bounds = self.map.getBounds(); } }); self.map.on('resize', function() { self.map.options.minZoom = -10; self.map.fitBounds(self.bounds,{animate:false}); self.map.options.minZoom = self.map.getBoundsZoom(L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth)),true); self.map.options.maxZoom = self.leafletZoom(self.options.zoomMax); waitForFinalEvent(function(){ self.resize = false; self._view_resize(); self.map.options.minZoom = -10; self.map.fitBounds(self.bounds,{animate:false}); self.map.options.minZoom = self.map.getBoundsZoom(L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth)),true); self.map.options.maxZoom = self.leafletZoom(self.options.zoomMax); }, 300, $img[0].id); }); self.options.onReady.call(self); }).each(function() { if (this.complete) { $(this).trigger("load"); } }); /* /* * Window resize handler */ $(window).resize(function() { if (self.ready) { self.resize = true; self._view_resize(); self.map.invalidateSize({animate: false}); } }); }, /* * View resize - the aim is to keep the view centered on the same location in the original image */ _view_resize: function() { if (this.ready) { var $view = $(this.view), $img = $(this.img), width = $img.width(), height = $img.height(), offset = $img.offset(), vTop = Math.round(offset.top + this.offsetBorder.y + this.offsetPadding.top), vLeft = Math.round(offset.left + this.offsetBorder.x + this.offsetPadding.left); $view.css({ top: vTop+"px", left: vLeft+"px", width: width+"px", height: height+"px" }); } }, /* * Remove the plugin */ destroy: function() { $(window).unbind("resize"); this.map.remove(); $(this.view).remove(); $.Widget.prototype.destroy.call(this); }, _setOption: function(key, value) { switch(key) { case 'zoomStep': if (parseFloat(value) <= 0 || isNaN(parseFloat(value))) { return; } break; case 'zoomMax': if (parseFloat(value) < 1 || isNaN(parseFloat(value))) { return; } break; } var version = $.ui.version.split('.'); if (version[0] > 1 || version[1] > 8) { this._super(key, value); } else { $.Widget.prototype._setOption.apply(this, arguments); } switch(key) { case 'zoomStep': if (this.ready) { this.map.options.zoomDelta = this.options.zoomStep; this.map.options.wheelPxPerZoomLevel = Math.round(60/this.options.zoomStep); } break; case 'zoomMax': if (this.ready) { lzoom = this.leafletZoom(this.options.zoomMax); if (lzoom < this.map.getZoom()) { this.map.setZoom(lzoom); } this.map.options.maxZoom = lzoom; this.map.fire('zoomend'); } break; case 'zoomable': if (this.options.zoomable) { this.map.zoomControl.enable(); this.map.boxZoom.enable(); this.map.touchZoom.enable(); this.map.doubleClickZoom.enable(); this.map.scrollWheelZoom.enable(); } else { this.map.zoomControl.disable(); this.map.boxZoom.disable(); this.map.touchZoom.disable(); this.map.doubleClickZoom.disable(); this.map.scrollWheelZoom.disable(); } break; case 'dragable': if (this.options.dragable) { this.map.dragging.enable(); } else { this.map.dragging.disable(); } break; } }, /* * Test if a relative image coordinate is visible in the current view */ isVisible: function(relx, rely) { var view = this.getView(); if (view) { return (relx >= view.left && relx <= view.right && rely >= view.top && rely <= view.bottom); } else { return false; } }, /* * Convert a user supplied zoom to a Leaflet zoom */ leafletZoom: function(zoom) { if (this.ready && zoom !== undefined) { var img = this.img, map = this.map, lzoom = map.getZoom() || 0, size = map.getSize(), width = img.naturalWidth, height = img.naturalHeight, nw = L.latLng(height/zoom,width/zoom), se = L.latLng(0,0), boundsSize = map.project(nw, lzoom).subtract(map.project(se, lzoom)); var scale = Math.min(size.x / boundsSize.x, -size.y / boundsSize.y); return map.getScaleZoom(scale, lzoom); } else { return undefined; } }, /* * Get the Leaflet map object */ getMap: function() { if (this.ready) { return this.map; } else { return null; } }, /* * Get current zoom level * Returned zoom will always be >=1 * a zoom of 1 means the entire image is just visible within the viewport * a zoom of 2 means half the image is visible in the viewport etc */ getZoom: function() { if (this.ready) { var img = this.img, map = this.map, width = img.naturalWidth, height = img.naturalHeight, constraint = this.options.constraint, bounds = map.getBounds(); if (constraint == 'width' ) { return Math.max(1, width/(bounds.getEast()-bounds.getWest())); } else if (constraint == 'height') { return Math.max(1,height/(bounds.getNorth()-bounds.getSouth())); } else { return Math.max(1, (width/(bounds.getEast()-bounds.getWest()) + height/(bounds.getNorth()-bounds.getSouth()))/2); } } else { return null; } }, /* * Set the zoom level * Zoom must be >=1 * a zoom of 1 means the entire image is just visible within the viewport * a zoom of 2 means half the image is visible in the viewport etc */ setZoom: function( zoom ) { if (this.ready) { zoom = Math.max(1, zoom); if (this.options.zoomMax === undefined) { } else { zoom = Math.min(zoom, this.options.zoomMax); } var img = this.img, map = this.map, width = img.naturalWidth, height = img.naturalHeight, constraint = this.options.constraint, center = map.getCenter(), bounds = map.getBounds(); var hvw, hvh; if (constraint == 'width') { hvw = width/zoom/2; hvh = hvw * (bounds.getNorth()-bounds.getSouth())/(bounds.getEast()-bounds.getWest()); } else if (constraint == 'height') { hvh = height/zoom/2; hvw = hvh * (bounds.getEast()-bounds.getWest())/(bounds.getNorth()-bounds.getSouth()); } else { hvw = width/zoom/2; hvh = height/zoom/2; } var east = center.lng + hvw, west = center.lng - hvw, north = center.lat + hvh, south = center.lat - hvh; if (west<0) { east += west; west = 0; } else if (east > width) { west -= east-width; east = width; } if (south<0) { north += south; south = 0; } else if (north > height) { south -= north-height; north = height; } map.fitBounds(L.latLngBounds(L.latLng(south,west), L.latLng(north,east)),{animate:false}); } return this; }, /* * Get relative image coordinates of current view */ getView: function() { if (this.ready) { var img = this.img, width = img.naturalWidth, height = img.naturalHeight, bnds = this.map.getBounds(); return { top: 1 - bnds.getNorth()/height, left: bnds.getWest()/width, bottom: 1 - bnds.getSouth()/height, right: bnds.getEast()/width }; } else { return null; } }, /* * Pan the view to be centred at the given relative image location */ panTo: function(relx, rely) { if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) { var img = this.img, map = this.map, bounds = this.bounds, // bounds = map.getBounds(), east = bounds.getEast(), west = bounds.getWest(), north = bounds.getNorth(), south = bounds.getSouth(), centerX = (east+west)/2, centerY = (north+south)/2, width = img.naturalWidth, height = img.naturalHeight, newY = (1-rely)*height, newX = relx*width; east += newX - centerX; west += newX - centerX; north += newY - centerY; south += newY - centerY; if (west<0) { east -= west; west = 0; } if (east > width) { west -= east-width; east = width; } if (south<0) { north -= south; south = 0; } if (north > height) { south -= north-height; north = height; } map.fitBounds(L.latLngBounds(L.latLng(south,west), L.latLng(north,east)),{animate:false}); } return this; }, /* * Return the relative image coordinate for a Leaflet event */ eventToImg: function(ev) { if (this.ready) { var img = this.img, width = img.naturalWidth, height = img.naturalHeight; relx = ev.latlng.lng/width; rely = 1 - ev.latlng.lat/height; if (relx>=0 && relx<=1 && rely>=0 && rely<=1) { return {x: relx, y: rely}; } else { return null; } } else { return null; } }, /* * Convert relative image coordinate to Leaflet LatLng point */ relposToLatLng: function(x,y) { if (this.ready) { var img = this.img, width = img.naturalWidth, height = img.naturalHeight; return L.latLng((1-y)*height, x*width); } else { return null; } }, /* * Convert relative image coordinate to Image pixel */ relposToImage: function(pos) { if (this.ready) { var img = this.img, width = img.naturalWidth, height = img.naturalHeight; return {x: Math.round(pos.x*width), y: Math.round(pos.y*height)}; } else { return null; } } }); })(jQuery);