/*
TileViewer HTML5 client
Version: 3.0.2
This plugin is tested with following dependencies
* JQuery 1.7+
* JQuery.HotKeys
* Brandon Aaron's (http://brandonaaron.net) mousewheel jquery plugin 3.0.3
* Circular-Slider (https://github.com/princejwesley/circular-slider)
* HammerJS (http://hammerjs.github.io) [In progress]
The MIT License
Copyright (c) 2011 Soichi Hayashi (https://sites.google.com/site/soichih/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*****************
MODIFIED TO WORK WITH CollectiveAccess and Tilepic format February 2012 by SK
ANNOTATIONS SUPPORT ADDED April 2013 by SK
MORE ANNOTATIONS SUPPORT ADDED October-December 2013 by SK
HAPPY HAPPY IMAGE ROTATION SUPPORT ADDED April 2014 by SK
USER INTERFACE OVERHAUL January-February 2015 by SK
*****************
*/
(function($){
var methods = {
///////////////////////////////////////////////////////////////////////////////////
// Initializes if it's not already initialized
init: function (options) {
jQuery.widget.bridge('uitooltip', $.ui.tooltip);
methods.tileCounts = undefined;
var defaults = {
// Setup and display options
id: 'tileviewer',
src: null,
empty: "#cccccc", //color of empty (loading) tile - if no subtile is available
zoomSensitivity: 16,
thumbnail: false,//display thumbnail
debug: false,
maximumPixelsize: 4,//set this to >1 if you want to let user to zoom image after reaching its original resolution
toolbar: ['zoomIn', 'zoomOut', 'pan', 'toggleAnnotations', 'rect', 'point', 'polygon', 'measure', 'lock', 'separator', 'overview', 'rotation', 'expand', 'separator', 'list', 'download', 'help', 'key'],
toolbarIcons: {
'pan': '',
'toggleAnnotations': '',
'rect': '',
'point': '',
'polygon': '',
'measure': '',
'lock': '',
'overview': '',
'rotation': '',
'expand': '',
'list': '',
'download': '',
'help': '',
'key': '',
'zoomIn': '',
'zoomOut': ''
}, // HTML for icons to use for toolbar in place of Font-Awesome icons; can be tags, text, Etc.
tooltips: {
'pan': 'Pan around image', 'toggleAnnotations': 'Show/hide annotations',
'rect': 'Rectangle annotation tool', 'point': 'Point annotation tool', 'polygon': 'Polygon annotation tool', 'measure': 'Measure features',
'lock': 'Lock annotations', 'overview': 'Show/hide image overview', 'rotation': 'Rotate image', 'expand': 'Fit image to window',
'list': 'Show/hide annotation sidebar', 'download': 'Download image', 'help': 'Help', 'key': 'Color key', 'zoomIn': 'Zoom in', 'zoomOut': 'Zoom out',
'rotationReset': 'Reset rotation'
},
tooltipClass: 'tileviewerTooltipFormat',
uiIcons: {
'zoomIn': '',
'zoomOut': '',
'lock': '',
'delete': '',
'close': ''
},
annotationLoadUrl: null,
annotationSaveUrl: null,
helpLoadUrl:null,
// Annotations
useAnnotations: true, // display annotation tools + annotations
displayAnnotations: true, // display annotations on load
lockAnnotations: false, // lock annotations on load - will display but cannot add, remove or drag existing annotations
lockAnnotationText: false, // lock annotation text on load - will display text but not be editable
showAnnotationTools: true, // show annotation tools on load
annotationTextDisplayMode: 'mouseover', // how to display annotation text: 'simultaneous' = show all annotation text all the time; 'mouseover' = show annotation text only when mouse is over the annotation or it is selected; 'selected' = show annotation text only when it is selected
annotationColor: "#000000", //"EE7B19",
annotationColorSelected: "#CC0000",
highlightPointsWithCircles: true, // draw circles around point label locations?
allowDraggableTextBoxesForRects: true, // allow draggable text boxes for rectangular annotations?
addPointAnnotationMode: false,
addRectAnnotationMode: false,
addPolygonAnnotationMode: false,
addMeasureAnnotationMode: false,
panMode: true,
annotationEditorPanel: null, // instance of ca.panel to open full annotation editor in
annotationEditorUrl: null, // url to load full annotation editor form
annotationEditorLink: 'More', // content of full annotation editor link
annotationDisplayMode: 'center', // perimeter, center
annotationDisplayModeCenterColor: "rgba(175, 0, 0, 0.4)", // when perimeter is "center", the color/opacity of the dot used to mark the center, as an rgba() string
allowAnnotationList: true, // use annotation list
annotationList: false, // is annotation list currently displayed?
allowAnnotationSearch: true, // allow annotation search option in annotation list; only available if annotation list is allowed
annotationPrefixText: '',
defaultAnnotationText: '', // initial value for newly created annotations
emptyAnnotationLabelText: "Enter your annotation", // text to display when there is no annotation text in label
emptyAnnotationEditorText: "Type text. Drag to position.",
showEmptyAnnotationLabelTextInTextBoxes: true,
useKey: false, // annotation color key
showKey: false,
// Image zooming
toolbarZooming: false,
sliderZooming: true,
// Image rotation
allowRotation: true,
rotation: false, // show rotation slider?
// Image download
mediaDownloadUrl: null, // url to download of media
// Measurements
enableMeasurements: true, // show measurement tool and prompt to set image scale
scale: null, // measurement scale factor
measurementUnits: null, // measurement units to display
imageScaleControlFirstSetText: "
A scale must be set for this image before measurements can be evaluated.
Enter the length with units (mm, cm, m, km, in, ft, miles, etc.) of the currently selected measurement below.
",
imageScaleControlChangeSettingText: "
This image is scaled at %1.
To change scale enter the length with units (mm, cm, m, km, in, ft, miles, etc.) of the currently selected measurement below.
"
};
if (options.annotationLoadUrl && (options.annotationLoadUrl.substr(0, 1) === '#')) {
options.enableMeasurements = false; // no measurements allowed when saving annotations locally
}
return this.each(function() {
var $this = $(this);
//
// Convert string to boolean
//
var stringToBoolean = function(s){
switch(s.toLowerCase().trim()){
case "true": case "yes": case "1": return true;
case "false": case "no": case "0": case null: return false;
default: return Boolean(s);
}
}
// force user-provided options to boolean when required
jQuery.each(defaults, function(k, v) {
if ((typeof v == 'boolean') && (options[k] !== undefined) && (typeof options[k] !== 'boolean')) {
options[k] = stringToBoolean(options[k]);
}
});
options = $.extend(defaults, options);//override defaults with options
$this.data("options", options);
///////////////////////////////////////////////////////////////////////////////////
// Now we can start initializing
// If the plugin hasn't been initialized yet..
var view = $this.data("view");
if(!view) {
var layer = {
info: null,
//current view offset - not absolute pixel offset
xpos: 0,
ypos: 0,
//number of tiles on the current level
xtilenum: null,
ytilenum: null,
//current tile level/size (size is usually 128-256)
level: null,
tilesize: null,
thumb: null, //thumbnail image
loader: {
loading: 0, //actual number of images that are currently loaded
max_loading: 6, //max number of image that can be loaded simultaneously
tile_count: 0, //number of tiles in tile dictionary (not all of them are actually loaded)
max_tiles: 200 //max number of images that can be stored in tiles dictionary
},
tiles: [] //tiles dictionary
}; //layer definition
$this.data("layer", layer);
var view = {
canvas: document.createElement("canvas"), // main canvas where image is drawn
thumbCanvas: document.createElement("canvas"), // small canvas where optional thumbnail view (aka "navigator") is drawn
controls: document.createElement("div"),
annotationContainer: document.createElement("div"),
annotationTextBlocks: [],
annotationTextEditor: document.createElement("div"),
mode: null, //current mouse left button mode (pan, sel2d, sel1d, etc..)
pan: {
//pan destination
xdest: null,//(pixel pos)
ydest: null,//(pixel pos)
leveldest: null,
},
select: {
x: null,
y: null,
width: null,
height: null
},
hammer: null, // touch event detection (if Hammer.js is available)
rotation: 0, // degrees rotated
annotations: [], // annotations list
changedAnnotations: [], // indices of annotations that need to be saved
annotationAreas: [],
annotationsToSave: [],
annotationsToDelete: [],
isSavingAnnotations: false, // flag indicating save is pending
//current mouse position (client pos)
xnow: null,
ynow: null,
mousedown: false,
dragAnnotation: null, // index of annotation currently being dragged
selectedAnnotation: null, // index of annotation currently selected
mouseOverAnnotation: null, // index of annotation mouse is currently over
framerate: null,//current framerate (1000 msec / drawtime msec)
needdraw: false, //flag used to request for frameredraw
diagonalWidth: null, // diagonal width of viewer area
canvasOverscanX: null,
canvasOverscanY: null,
polygonInProgressAnnotationIndex: null, // index of polygone being built; null if no polygon is being built currently
///////////////////////////////////////////////////////////////////////////////////
// Internal functions
draw: function() {
view.needdraw = false;
if(layer.info == null) { return; }
var start = new Date().getTime();
var ctx = view.canvas.getContext("2d");
view.canvas.width = view.diagonalWidth;
view.canvas.height = view.diagonalWidth;
view.draw_tiles(ctx);
switch(view.mode) {
case "pan":
if(options.thumbnail) {
jQuery(view.thumbCanvas).show();
view.draw_thumb(ctx);
} else {
jQuery(view.thumbCanvas).hide();
}
break;
}
if (options.rotation) {
jQuery($this).find(".tileviewerToolbarRotation").show();
} else {
jQuery($this).find(".tileviewerToolbarRotation").hide();
}
if (options.annotationList) {
jQuery($this).find(".tileviewerAnnotationList").show();
} else {
jQuery($this).find(".tileviewerAnnotationList").hide();
}
if (options.showKey) {
jQuery($this).find(".tileviewerKey").show();
} else {
jQuery($this).find(".tileviewerKey").hide();
}
view.update_controls();
if (options.useAnnotations) {
view.draw_annotations();
}
},
load_annotations: function() {
if (!options.useAnnotations || !options.annotationLoadUrl || !options.annotationLoadUrl.trim()) { return; }
if (options.annotationLoadUrl.substr(0,1) !== '#') {
jQuery.getJSON(options.annotationLoadUrl, function(data) {
view.load_annotation_data(data);
});
} else {
try {
var data = jQuery(options.annotationLoadUrl).val();
if (data) view.load_annotation_data(JSON.parse(data));
} catch(e) {
view.load_annotation_data('');
}
}
},
load_annotation_data: function(data) {
view.annotations = [];
view.annotationTextBlocks = [];
jQuery.each(data, function(k, v) {
if (!v) { return; }
if (v['scale'] && v['measurementUnits']) {
options.scale = v['scale'];
options.measurementUnits = v['measurementUnits'];
jQuery(".tileviewerImageScaleControls div.tileviewerImageScaleControlText").html(options.imageScaleControlChangeSettingText.replace("%1", "1" + options.measurementUnits + " = " + (options.scale.toFixed(2) * 100) + "% of width"));
return;
}
if (!v['annotation_id']) { return; }
v['index'] = k;
v['x'] = parseFloat(v['x']);
v['y'] = parseFloat(v['y']);
v['w'] = parseFloat(v['w']);
v['h'] = parseFloat(v['h']);
v['tx'] = parseFloat(v['tx']);
v['ty'] = parseFloat(v['ty']);
v['tw'] = parseFloat(v['tw']);
v['th'] = parseFloat(v['th']);
if (v['label'] == '[BLANK]') { v['label'] = ''; }
// create text block
var textBlock = document.createElement("div");
jQuery(textBlock).attr('id', 'tileviewerAnnotationTextBlock_' + k).addClass("tileviewerAnnotationTextBlock").data("annotationIndex", k).html(options.annotationPrefixText + (v['label'] ? v['label'] : (options.showEmptyAnnotationLabelTextInTextBoxes ? options.emptyAnnotationLabelText : '')));
jQuery('#tileviewerAnnotationTextBlock_' + k).remove();
jQuery(view.annotationContainer).append(textBlock)
if (options.annotationTextDisplayMode == 'simultaneous') {
view._make_annotation_text_block_draggable('#tileviewerAnnotationTextBlock_' + k);
}
v['textBlock'] = textBlock;
view.annotations.push(v);
});
view.draw_annotations();
if (options.allowAnnotationList) { view.update_annotation_list(); }
jQuery(view.canvas).parent().trigger('tileviewer:loadAnnotations', {'viewer': jQuery("#" + options.id)});
},
/**
* Record annotation changes for subsequent commit
*/
save_annotations: function(toSave, toDelete) {
if (!options.useAnnotations) { return; }
for(var i in toSave) {
if (!jQuery.isNumeric(i)) { continue; }
i = parseInt(i);
view.annotationsToSave.push(view.annotations[parseInt(toSave[i])]);
}
for(var i in toDelete) {
if (!jQuery.isNumeric(i)) { continue; }
i = parseInt(i);
view.annotationsToDelete.push(view.annotations[parseInt(toDelete[i])].annotation_id);
}
view.commit_annotation_changes();
},
/**
* Write annotations to database
*/
commit_annotation_changes: function() {
if (!options.useAnnotations) { return; }
if (view.isSavingAnnotations) {
if (options.debug) { console.log("Cannot commit now; save is pending"); console.trace(); }
return false;
}
if ((view.annotationsToSave.length == 0) && (view.annotationsToDelete.length == 0)) {
if (options.debug) { console.log("Cannot commit now; nothing to save"); }
return false;
}
view.isSavingAnnotations = true;
if (options.debug) { console.log("Commit " + view.annotationsToSave.length + " annotations to " + (options.annotationSaveUrl ? options.annotationSaveUrl : "LOCAL"), view.annotationsToSave, view.annotationsToDelete); }
// strip out textBlock pointer because it causes jQuery errors with getJSON
var annotationsToSave = [];
jQuery.each(view.annotationsToSave, function(k, v) {
var a = jQuery.extend({}, v);
a['textBlock'] = null;
annotationsToSave.push(a);
});
if (options.annotationSaveUrl.substr(0,1) !== '#') {
jQuery.post(options.annotationSaveUrl, { save: annotationsToSave, delete: view.annotationsToDelete }, function(data) {
view._update_annotations_after_commit(data['annotation_ids'], annotationsToSave);
jQuery.each(view.annotationsToDelete, function(k, v) {
view.annotations[v] = null;
});
view.isSavingAnnotations = false;
}, 'json');
} else {
var ids = [];
jQuery.each(view.annotationsToDelete, function(k, v) {
view.annotations[v-1] = null;
});
jQuery.each(view.annotations, function(k, v) {
if (!v) { return; }
if(!v['annotation_id']) { v['annotation_id'] = k + 1; }
ids.push(v['annotation_id']);
});
view._update_annotations_after_commit(ids, annotationsToSave);
if (options.annotationSaveUrl.substr(0,1) == '#') {
var filteredAnnotations = [];
var i = 0;
jQuery.each(view.annotations, function(k, v) {
if (v) {
v['index'] = i;
v['annotation_id'] = i + 1;
filteredAnnotations.push(v);
i++;
}
});
jQuery(options.annotationSaveUrl).val(JSON.stringify(filteredAnnotations));
}
view.isSavingAnnotations = false;
}
jQuery(view.canvas).parent().trigger('tileviewer:saveAnnotations', {'viewer': jQuery(view.canvas).parent()});
},
_update_annotations_after_commit: function(annotation_ids, annotationsToSave) {
for(var index in annotation_ids) {
if (!jQuery.isNumeric(index)) { continue; }
if (!view.annotations[index]) { continue; }
view.annotations[index]['annotation_id'] = annotation_ids[index];
var i = view.changedAnnotations.indexOf(index);
if (i !== -1) {
view.changedAnnotations.splice(i, 1);
}
view.annotationsToSave = [];
view.annotationsToDelete = [];
view.needdraw = true;
if (options.allowAnnotationList) { view.update_annotation_list(); } // reload annotation list because annotations have changes
}
// put new text into overlays
for(var i in annotationsToSave) {
var index = annotationsToSave[i]['index'];
if (!jQuery.isNumeric(i)) { continue; }
if (!jQuery.isNumeric(index)) { continue; }
if (annotation_ids[index]) {
jQuery("#tileviewerAnnotationTextBlock_" + index).html(options.annotationPrefixText + (annotationsToSave[i]['label'] ? annotationsToSave[i]['label'] : (options.showEmptyAnnotationLabelTextInTextBoxes ? options.emptyAnnotationLabelText : '')));
}
}
view.isSavingAnnotations = false;
},
_get_annotation_by_index: function(index, returnArrayIndex) {
var annotationsToCheck = jQuery.extend(true, [], view.annotations);
index = parseInt(index);
for(var i in annotationsToCheck) {
if (!annotationsToCheck[i]) { continue; }
if (parseInt(annotationsToCheck[i]['index']) === index) { return returnArrayIndex ? i : annotationsToCheck[i]; }
}
return null;
},
_make_annotation_text_block_draggable: function(id) {
var index = jQuery(this).data('annotationIndex');
jQuery(id).show().draggable({ drag: function(e) {
var index = jQuery(this).data('annotationIndex');
if ((options.lockAnnotationText) || (parseInt(view.annotations[index]['locked']) == 1)) {
e.preventDefault();
return false;
}
if(
index != null
&&
view.annotations[index]
&&
(
(view.annotations[index].type == 'point')
||
(view.annotations[index].type == 'poly')
||
(view.annotations[index].type == 'measure')
||
((view.annotations[index].type == 'rect') && options.allowDraggableTextBoxesForRects)
)
) {
var pos = jQuery('#tileviewerAnnotationTextBlock_' +index).position();
var factor = Math.pow(2,layer.level);
view.annotations[index].tx = ((pos.left + view.canvasOverscanX - layer.xpos)/((layer.info.width/factor) * (layer.tilesize/256))) * 100;
view.annotations[index].ty = ((pos.top + view.canvasOverscanY - layer.ypos)/((layer.info.height/factor) * (layer.tilesize/256))) * 100;
view.draw();
}
}, stop: function(e) {
var index = jQuery(this).data('annotationIndex');
var pos = jQuery('#tileviewerAnnotationTextBlock_' + index).position();
var factor = Math.pow(2,layer.level);
view.annotations[index].tx = ((pos.left + view.canvasOverscanX - layer.xpos)/((layer.info.width/factor) * (layer.tilesize/256))) * 100;
view.annotations[index].ty = ((pos.top + view.canvasOverscanY - layer.ypos)/((layer.info.height/factor) * (layer.tilesize/256))) * 100;
view.save_annotations([index], []);
view.draw();
}}).mouseup(function(e) {
view.isAnnotationResize = view.isAnnotationTransformation = view.mousedown = view.dragAnnotation = null;
});
},
init_context_properties: function(ctx) {
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.shadowBlur = 2;
ctx.shadowColor = 'rgba(255,255, 255, 1.0)';
ctx.lineWidth = 1;
ctx.fillStyle = '#0c0';
},
//
// Update annotations list with current content
//
update_annotation_list: function() {
var annotationList = document.createElement("ol");
jQuery(annotationList).addClass('tileviewerAnnotationList');
var annotationsToDraw = jQuery.extend(true, [], view.annotations);
var selectedAnnotation = view.selectedAnnotation;
for(var i in annotationsToDraw) {
if (!jQuery.isNumeric(i)) { continue; }
var annotation = annotationsToDraw[i];
if (!annotation) { continue; }
jQuery(annotationList).append("