diff --git a/summerHTMLImageMapCreator.js b/summerHTMLImageMapCreator.js index b3ca1d0..5d886e5 100644 --- a/summerHTMLImageMapCreator.js +++ b/summerHTMLImageMapCreator.js @@ -632,1051 +632,896 @@ var summerHtmlImageMapCreator = (function() { } }; })(); - - /* Help block */ - var help = (function() { - var block = utils.id('help'), - overlay = utils.id('overlay'), - close_button = block.querySelector('.close_button'); - - function hide() { - utils.hide(block); - utils.hide(overlay); - } + + + /** + * The constructor for dom events (for simple deleting of event) + * + * @constructor + * @param {DOMElement} target - DOM-element + * @param {String} eventType - e.g. 'click' or 'mousemove' + * @param {Function} func - handler for this event + */ + function AppEvent(target, eventType, func) { + this.target = target; + this.eventType = eventType; + this.func = func; - function show() { - utils.show(block); - utils.show(overlay); - } - - overlay.addEventListener('click', hide, false); - - close_button.addEventListener('click', hide, false); - - return { - show : show, - hide : hide - }; - })(); - - /* For html code of created map */ - var code = (function(){ - var block = utils.id('code'), - content = utils.id('code_content'), - close_button = block.querySelector('.close_button'); - - close_button.addEventListener('click', function(e) { - utils.hide(block); - e.preventDefault(); - }, false); - - return { - print: function() { - content.innerHTML = app.getHTMLCode(true); - utils.show(block); - }, - hide: function() { - utils.hide(block); - } - }; - })(); + target.addEventListener(eventType, func, false); + } + /** + * Remove this event listener from target + */ + AppEvent.prototype.remove = function() { + this.target.removeEventListener(this.eventType, this.func, false); + }; - /* Edit selected area info */ - var info = (function() { - var form = utils.id('edit_details'), - header = form.querySelector('h5'), - href_attr = utils.id('href_attr'), - alt_attr = utils.id('alt_attr'), - title_attr = utils.id('title_attr'), - save_button = utils.id('save_details'), - close_button = form.querySelector('.close_button'), - sections = form.querySelectorAll('p'), - obj, - x, - y, - temp_x, - temp_y; + + /** + * The constructor of helpers points + * Helper is small svg-rectangle with some actions + * + * @constructor + * @param node {DOMElement} - a node for inserting helper + * @param x {number} - x-coordinate of helper + * @param y {number} - y-coordinate of helper + * @param action {string} - an action by click of this helper (e.g. 'move') + */ + function Helper(node, x, y, action) { + this._el = document.createElementNS(Area.SVG_NS, 'rect'); - function changedReset() { - form.classList.remove('changed'); - utils.foreach(sections, function(x) { - x.classList.remove('changed'); - }); - } + this._el.classList.add(Helper.CLASS_NAME); + this._el.setAttribute('height', Helper.SIZE); + this._el.setAttribute('width', Helper.SIZE); + this._el.setAttribute('x', x + Helper.OFFSET); + this._el.setAttribute('y', y + Helper.OFFSET); - function save(e) { - obj.setInfoAttributes({ - href : href_attr.value, - alt : alt_attr.value, - title : title_attr.value, - }); - - obj[obj.href ? 'setStyleOfElementWithHref' : 'unsetStyleOfElementWithHref'](); - - changedReset(); - unload(); - - e.preventDefault(); - } + node.appendChild(this._el); - function unload() { - obj = null; - changedReset(); - utils.hide(form); - } + this._el.action = action; // TODO: move 'action' from dom el to data-attr + this._el.classList.add(Helper.ACTIONS_TO_CURSORS[action]); + } + + Helper.SIZE = 5; + Helper.OFFSET = -Math.ceil(Helper.SIZE / 2); + Helper.CLASS_NAME = 'helper'; + Helper.ACTIONS_TO_CURSORS = { + 'move' : 'move', + 'editLeft' : 'e-resize', + 'editRight' : 'w-resize', + 'editTop' : 'n-resize', + 'editBottom' : 's-resize', + 'editTopLeft' : 'nw-resize', + 'editTopRight' : 'ne-resize', + 'editBottomLeft' : 'sw-resize', + 'editBottomRight' : 'se-resize', + 'movePoint' : 'pointer' + }; + + /** + * Set coordinates for this helper + * + * @param x {number} - x-coordinate + * @param y {number} - y-coordinate + * @returns {Helper} + */ + Helper.prototype.setCoords = function(x, y) { + this._el.setAttribute('x', x + Helper.OFFSET); + this._el.setAttribute('y', y + Helper.OFFSET); - function setCoords(x, y) { - form.style.left = (x + 5) + 'px'; - form.style.top = (y + 5) + 'px'; - } + return this; + }; + + /** + * Set id of this helper in list of parent's helpers + * + * @param id {number} + * @returns {Helper} + */ + Helper.prototype.setId = function(id) { + // TODO: move n-field from DOM-element to data-attribute + this._el.n = id; - function moveEditBlock(e) { - setCoords(x + e.pageX - temp_x, y + e.pageY - temp_y); + return this; + }; + + /** + * The abstract constructor for area + * + * @constructor + * @abstract + * @param type {string} - type of area ('rectangle', 'circle' or 'polygon') + * @param coords {Object} - coordinates of area (e.g. x, y, width, height) + * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) + */ + function Area(type, coords, attributes) { + if (this.constructor === Area) { + throw new Error('This is abstract class'); } - function stopMoveEditBlock(e) { - x = x + e.pageX - temp_x; - y = y + e.pageY - temp_y; - setCoords(x, y); - - app.removeAllEvents(); - } + this._type = type; - function change() { - form.classList.add('changed'); - this.parentNode.classList.add('changed'); + /** + * @namespace + * @property href {string} - href-attribute of area + * @property alt {string} - alt-attribute of area + * @property title {string} - title-attribute of area + */ + this._attributes = { + href : '', + alt : '', + title : '' + }; + + if (attributes) { + this.setInfoAttributes(attributes); } - save_button.addEventListener('click', save, false); + this._coords = coords; - href_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); - alt_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); - title_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); + // the g-element, it contains this area and helpers elements + this._groupEl = document.createElementNS(Area.SVG_NS, 'g'); + app.addNodeToSvg(this._groupEl); - href_attr.addEventListener('change', change, false); - alt_attr.addEventListener('change', change, false); - title_attr.addEventListener('change', change, false); + // TODO: remove this field from DOM-element + // Link to parent object + this._groupEl.obj = this; - close_button.addEventListener('click', unload, false); + // svg-dom-element of area + this._el = null; - header.addEventListener('mousedown', function(e) { - temp_x = e.pageX, - temp_y = e.pageY; - - app.addEvent(document, 'mousemove', moveEditBlock); - app.addEvent(header, 'mouseup', stopMoveEditBlock); - - e.preventDefault(); - }, false); + // Object with all helpers of area + this._helpers = {}; - return { - load : function(object, new_x, new_y) { - obj = object; - href_attr.value = object.href ? object.href : ''; - alt_attr.value = object.alt ? object.alt : ''; - title_attr.value = object.title ? object.title : ''; - utils.show(form); - if (new_x && new_y) { - x = new_x; - y = new_y; - setCoords(x, y); - } - }, - unload : unload - }; - })(); - + // Add this new area to list of all areas + app.addObject(this); + } + Area.SVG_NS = 'http://www.w3.org/2000/svg'; // TODO: move to main editor constructor + Area.CLASS_NAMES = { + SELECTED : 'selected', + WITH_HREF : 'with_href' + }; + Area.CONSTRUCTORS = { + rectangle : Rectangle, + circle : Circle, + polygon : Polygon + }; + Area.REGEXP = { + AREA : //gmi, + HREF : / href="([\S\s]+?)"/, + ALT : / alt="([\S\s]+?)"/, + TITLE : / title="([\S\s]+?)"/, + DELIMETER : / ?, ?/ + }; + Area.HTML_NAMES_TO_AREA_NAMES = { + rect : 'rectangle', + circle : 'circle', + poly : 'polygon' + }; + Area.ATTRIBUTES_NAMES = ['HREF', 'ALT', 'TITLE']; + + /** + * This method should be implemented for child-classes + * + * @throws {AbstractMethodCall} + */ + Area.prototype.ABSTRACT_METHOD = function() { + throw new Error('This is abstract method'); + }; + + /** + * All these methods are abstract + * + * @throws {AbstractMethodCall} + */ + Area.prototype.setSVGCoords = + Area.prototype.setCoords = + Area.prototype.dynamicDraw = + Area.prototype.onProcessDrawing = + Area.prototype.onStopDrawing = + Area.prototype.edit = + Area.prototype.dynamicEdit = + Area.prototype.onProcessEditing = + Area.prototype.onStopEditing = + Area.prototype.toString = + Area.prototype.toHTMLMapElementString = + Area.prototype.getCoordsForDisplayingInfo = + Area.prototype.ABSTRACT_METHOD; + + /** + * Redraw this area with own or custom coordinates + * + * @param coords {Object} [coords=undefined] + * @returns {Area} - this area + */ + Area.prototype.redraw = function(coords) { + this.setSVGCoords(coords ? coords : this._coords); + + return this; + }; + + /** + * Remove this area from DOM-tree + */ + Area.prototype.remove = function() { + app.removeNodeFromSvg(this._groupEl); + }; - /* Load areas from html code */ - var from_html_form = (function() { - var form = utils.id('from_html_wrapper'), - code_input = utils.id('code_input'), - load_button = utils.id('load_code_button'), - close_button = form.querySelector('.close_button'); + /** + * Move this area by dx, dy + * + * @returns {Area} - this area + */ + Area.prototype.move = function(dx, dy) { + this.setCoords(this.edit('move', dx, dy)).redraw(); + return this; + }; + + /** + * Add class name for selected areas to this area + * + * @returns {Area} - this area + */ + Area.prototype.select = function() { + this._el.classList.add(Area.CLASS_NAMES.SELECTED); + console.info(this.toString() + ' is selected now'); - function load(e) { - if (Area.createAreasFromHTMLOfMap(code_input.value)) { - hide(); - } - - e.preventDefault(); + return this; + }; + + /** + * Remove class name for selected areas from this area + * + * @returns {Area} - this area + */ + Area.prototype.deselect = function() { + this._el.classList.remove(Area.CLASS_NAMES.SELECTED); + + return this; + }; + + /** + * Set style of element with href attribute for this area + * + * @returns {Area} - this area + */ + Area.prototype.setStyleOfElementWithHref = function() { + this._el.classList.add(Area.CLASS_NAMES.WITH_HREF); + + return this; + }; + + /** + * Unset style of element with href attribute for this area + * + * @returns {Area} - this area + */ + Area.prototype.unsetStyleOfElementWithHref = function() { + this._el.classList.remove(Area.CLASS_NAMES.WITH_HREF); + + return this; + }; + + /** + * Set attributes (href, alt and title) for this area + * + * @param attributes {Object} - Object with attributes for area + */ + Area.prototype.setInfoAttributes = function(attributes) { + this._attributes.href = attributes.href; + this._attributes.alt = attributes.alt; + this._attributes.title = attributes.title; + }; + + /** + * Returns json-representation of this area + * + * @returns {Object} + */ + Area.prototype.toJSON = function() { + /** + * @namespace + * @property type {string} - type of this area (e.g. 'rectangle', 'circle') + * @property coords {Object} - coordinates of this area (e.g. 'x', 'width') + * @property attributes {Object} - attributes of this area (e.g. 'href', 'title') + */ + return { + type : this._type, + coords : this._coords, + attributes : this._attributes + }; + }; + + /** + * Returns new area object created with params from json-object + * + * @static + * @param params {Object} - params of area, incl. type, coords and attributes + * @returns {Rectangle|Circle|Polygon} + */ + Area.fromJSON = function(params) { + var AreaConstructor = Area.CONSTRUCTORS[params.type]; + + if (!AreaConstructor) { + throw new Error('This area type is not valid'); } - function hide() { - utils.hide(form); + if (!AreaConstructor.testCoords(params.coords)) { + throw new Error('This coords is not valid for ' + params.type); } - load_button.addEventListener('click', load, false); + app.setIsDraw(true); - close_button.addEventListener('click', hide, false); + var area = new AreaConstructor(params.coords, params.attributes); - return { - show : function() { - code_input.value = ''; - utils.show(form); - }, - hide : hide - }; - })(); + app.setIsDraw(false) + .resetNewArea(); + + return area; + }; + /** + * Creates new areas from html-string with elements + * + * @param htmlStr {string} + * @returns {Array} - array with areas + */ + Area.createAreasFromHTMLOfMap = function(htmlStr) { + if (!htmlStr) { + return false; + } - /* Get image form */ - var get_image = (function() { - var block = utils.id('get_image_wrapper'), - close_button = block.querySelector('.close_button'), - loading_indicator = utils.id('loading'), - button = utils.id('button'), - filename = null, - last_changed = null; + while (true) { + var findedResult = Area.REGEXP.AREA.exec(htmlStr); // + if (!findedResult) { + break; + } + + var htmlAreaFinded = findedResult[0], // + type = findedResult[1], // $1 + coords = findedResult[2].split(Area.REGEXP.DELIMETER), // $2 + attributes = {}; - // Drag'n'drop - the first way to loading an image - var drag_n_drop = (function() { - var dropzone = utils.id('dropzone'), - dropzone_clear_button = dropzone.querySelector('.clear_button'), - sm_img = utils.id('sm_img'); + Area.ATTRIBUTES_NAMES.forEach(function(item, i) { + var result = Area.REGEXP[item].exec(htmlAreaFinded); + + if (result) { + attributes[name] = result[1]; + } + }); - function testFile(type) { - switch (type) { - case 'image/jpeg': - case 'image/gif': - case 'image/png': - return true; - } - return false; - } - - dropzone.addEventListener('dragover', function(e){ - utils.stopEvent(e); - }, false); - - dropzone.addEventListener('dragleave', function(e){ - utils.stopEvent(e); - }, false); - - dropzone.addEventListener('drop', function(e){ - utils.stopEvent(e); - - var reader = new FileReader(), - file = e.dataTransfer.files[0]; - - if (testFile(file.type)) { - dropzone.classList.remove('error'); - - reader.readAsDataURL(file); - - reader.onload = function(e) { - sm_img.src = e.target.result; - sm_img.style.display = 'inline-block'; - filename = file.name; - utils.show(dropzone_clear_button); - last_changed = drag_n_drop; - }; - } else { - clearDropzone(); - dropzone.classList.add('error'); - } - - }, false); - - function clearDropzone() { - sm_img.src = ''; - - utils.hide(sm_img) - .hide(dropzone_clear_button); - - dropzone.classList.remove('error'); - - last_changed = url_input; - } - - dropzone_clear_button.addEventListener('click', clearDropzone, false); - - return { - clear : clearDropzone, - init : function() { - dropzone.draggable = true; - this.clear(); - utils.hide(sm_img) - .hide(dropzone_clear_button); - }, - test : function() { - return Boolean(sm_img.src); - }, - getImage : function() { - return sm_img.src; - } - }; - })(); - - /* Set a url - the second way to loading an image */ - var url_input = (function() { - var url = utils.id('url'), - url_clear_button = url.parentNode.querySelector('.clear_button'); - - function testUrl(str) { - var url_str = str.trim(), - temp_array = url_str.split('.'), - ext; - - if(temp_array.length > 1) { - ext = temp_array[temp_array.length-1].toLowerCase(); - switch (ext) { - case 'jpg': - case 'jpeg': - case 'gif': - case 'png': - return true; - } - } - - return false; - } - - function onUrlChange() { - setTimeout(function(){ - if(url.value.length) { - utils.show(url_clear_button); - last_changed = url_input; - } else { - utils.hide(url_clear_button); - last_changed = drag_n_drop; - } - }, 0); - } - - url.addEventListener('keypress', onUrlChange, false); - url.addEventListener('change', onUrlChange, false); - url.addEventListener('paste', onUrlChange, false); - - function clearUrl() { - url.value = ''; - utils.hide(url_clear_button); - url.classList.remove('error'); - last_changed = url_input; - } - - url_clear_button.addEventListener('click', clearUrl, false); - - return { - clear : clearUrl, - init : function() { - this.clear(); - utils.hide(url_clear_button); - }, - test : function() { - if(testUrl(url.value)) { - url.classList.remove('error'); - return true; - } else { - url.classList.add('error'); - } - return false; - }, - getImage : function() { - var tmp_arr = url.value.split('/'); - filename = tmp_arr[tmp_arr.length - 1]; - - return url.value.trim(); - } - }; - })(); - - - /* Block init */ - function init() { - utils.hide(loading_indicator); - drag_n_drop.init(); - url_input.init(); - } - init(); - - /* Block clear */ - function clear() { - drag_n_drop.clear(); - url_input.clear(); - last_changed = null; - } - - /* Selected image loading */ - function onButtonClick(e) { - if (last_changed === url_input && url_input.test()) { - app.loadImage(url_input.getImage()).setFilename(filename); - } else if (last_changed === drag_n_drop && drag_n_drop.test()) { - app.loadImage(drag_n_drop.getImage()).setFilename(filename); - } - - e.preventDefault(); - } - - button.addEventListener('click', onButtonClick, false); - - close_button.addEventListener('click', hide, false); - - function show() { - clear(); - utils.show(block); - } - - function hide() { - utils.hide(block); - } - - /* Returned object */ - return { - show : function() { - app.hide(); - show(); - - return this; - }, - hide : function() { - app.show(); - hide(); - - return this; - }, - showLoadIndicator : function() { - utils.show(loading_indicator); - - return this; - }, - hideLoadIndicator : function() { - utils.hide(loading_indicator); - - return this; - } - }; - })(); - get_image.show(); - - - /* Buttons and actions */ - var buttons = (function() { - var all = utils.id('nav').getElementsByTagName('li'), - save = utils.id('save'), - load = utils.id('load'), - rectangle = utils.id('rectangle'), - circle = utils.id('circle'), - polygon = utils.id('polygon'), - edit = utils.id('edit'), - clear = utils.id('clear'), - from_html = utils.id('from_html'), - to_html = utils.id('to_html'), - preview = utils.id('preview'), - new_image = utils.id('new_image'), - show_help = utils.id('show_help'); - - function deselectAll() { - utils.foreach(all, function(x) { - x.classList.remove(Area.CLASS_NAMES.SELECTED); - }); - } - - function selectOne(button) { - deselectAll(); - button.classList.add(Area.CLASS_NAMES.SELECTED); - } - - function onSaveButtonClick(e) { - // Save in localStorage - app.saveInLocalStorage(); - - e.preventDefault(); - } - - function onLoadButtonClick(e) { - // Load from localStorage - app.clear() - .loadFromLocalStorage(); - - e.preventDefault(); - } - - function onShapeButtonClick(e) { - // shape = rect || circle || polygon - app.setMode('drawing') - .setDrawClass() - .setShape(this.id) - .deselectAll() - .hidePreview(); - info.unload(); - selectOne(this); - - e.preventDefault(); - } - - function onClearButtonClick(e) { - // Clear all - if (confirm('Clear all?')) { - app.setMode(null) - .setDefaultClass() - .setShape(null) - .clear() - .hidePreview(); - deselectAll(); - } - - e.preventDefault(); - } - - function onFromHtmlButtonClick(e) { - // Load areas from html - from_html_form.show(); - - e.preventDefault(); - } - - function onToHtmlButtonClick(e) { - // Generate html code only - info.unload(); - code.print(); - - e.preventDefault(); - } - - function onPreviewButtonClick(e) { - if (app.getMode() === 'preview') { - app.setMode(null) - .hidePreview(); - deselectAll(); - } else { - app.deselectAll() - .setMode('preview') - .setDefaultClass() - .preview(); - selectOne(this); - } - - e.preventDefault(); - } - - function onEditButtonClick(e) { - if (app.getMode() === 'editing') { - app.setMode(null) - .setDefaultClass() - .deselectAll(); - deselectAll(); - utils.show(domElements.svg); - } else { - app.setShape(null) - .setMode('editing') - .setEditClass(); - selectOne(this); - } - app.hidePreview(); - e.preventDefault(); - } - - function onNewImageButtonClick(e) { - // New image - clear all and back to loading image screen - if(confirm('Discard all changes?')) { - app.setMode(null) - .setDefaultClass() - .setShape(null) - .setIsDraw(false) - .clear() - .hide() - .hidePreview(); - deselectAll(); - get_image.show(); - } - - e.preventDefault(); - } - - function onShowHelpButtonClick(e) { - help.show(); - - e.preventDefault(); + coords = coords.map(function(item) { + return Number(item); + }); + + type = Area.HTML_NAMES_TO_AREA_NAMES[type]; + + Area.fromJSON({ + type : type, + coords : Area.CONSTRUCTORS[type].getCoordsFromHTMLArray(coords), + attributes : attributes + }); + } - - save.addEventListener('click', onSaveButtonClick, false); - load.addEventListener('click', onLoadButtonClick, false); - rectangle.addEventListener('click', onShapeButtonClick, false); - circle.addEventListener('click', onShapeButtonClick, false); - polygon.addEventListener('click', onShapeButtonClick, false); - clear.addEventListener('click', onClearButtonClick, false); - from_html.addEventListener('click', onFromHtmlButtonClick, false); - to_html.addEventListener('click', onToHtmlButtonClick, false); - preview.addEventListener('click', onPreviewButtonClick, false); - edit.addEventListener('click', onEditButtonClick, false); - new_image.addEventListener('click', onNewImageButtonClick, false); - show_help.addEventListener('click', onShowHelpButtonClick, false); - })(); - + + return Boolean(htmlAreaFinded); + }; + /** - * The constructor for dom events (for simple deleting of event) + * Returns copy of original area, selected and moved by (10,10) from it * - * @constructor - * @param {DOMElement} target - DOM-element - * @param {String} eventType - e.g. 'click' or 'mousemove' - * @param {Function} func - handler for this event - */ - function AppEvent(target, eventType, func) { - this.target = target; - this.eventType = eventType; - this.func = func; - - target.addEventListener(eventType, func, false); - } - - /** - * Remove this event listener from target + * @param originalArea {Area} + * @returns {Area} - a copy of original area */ - AppEvent.prototype.remove = function() { - this.target.removeEventListener(this.eventType, this.func, false); + Area.copy = function(originalArea) { + return Area.fromJSON(originalArea.toJSON()).move(10, 10).select(); }; + /* ---------- Constructors for real areas ---------- */ /** - * The constructor of helpers points - * Helper is small svg-rectangle with some actions + * The constructor for rectangles * + * (x, y) ----- + * | | height + * ------------ + * width + * * @constructor - * @param node {DOMElement} - a node for inserting helper - * @param x {number} - x-coordinate of helper - * @param y {number} - y-coordinate of helper - * @param action {string} - an action by click of this helper (e.g. 'move') + * @param coords {Object} - object with parameters of new area (x, y, width, height) + * if some parameter is undefined, it will set 0 + * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) */ - function Helper(node, x, y, action) { + function Rectangle(coords, attributes) { + Area.call(this, 'rectangle', coords, attributes); + + /** + * @namespace + * @property {number} x - Distance from the left edge of the image to the left side of the rectangle + * @property {number} y - Distance from the top edge of the image to the top side of the rectangle + * @property {number} width - Width of rectangle + * @property {number} height - Height of rectangle + */ + this._coords = { + x : coords.x || 0, + y : coords.y || 0, + width : coords.width || 0, + height : coords.height || 0 + }; + this._el = document.createElementNS(Area.SVG_NS, 'rect'); + this._groupEl.appendChild(this._el); - this._el.classList.add(Helper.CLASS_NAME); - this._el.setAttribute('height', Helper.SIZE); - this._el.setAttribute('width', Helper.SIZE); - this._el.setAttribute('x', x + Helper.OFFSET); - this._el.setAttribute('y', y + Helper.OFFSET); + var x = coords.x - this._coords.width / 2, + y = coords.y - this._coords.height / 2; - node.appendChild(this._el); + this._helpers = { + center : new Helper(this._groupEl, x, y, 'move'), + top : new Helper(this._groupEl, x, y, 'editTop'), + bottom : new Helper(this._groupEl, x, y, 'editBottom'), + left : new Helper(this._groupEl, x, y, 'editLeft'), + right : new Helper(this._groupEl, x, y, 'editRight'), + topLeft : new Helper(this._groupEl, x, y, 'editTopLeft'), + topRight : new Helper(this._groupEl, x, y, 'editTopRight'), + bottomLeft : new Helper(this._groupEl, x, y, 'editBottomLeft'), + bottomRight : new Helper(this._groupEl, x, y, 'editBottomRight') + }; - this._el.action = action; // TODO: move 'action' from dom el to data-attr - this._el.classList.add(Helper.ACTIONS_TO_CURSORS[action]); + this.redraw(); } + utils.inherits(Rectangle, Area); - Helper.SIZE = 5; - Helper.OFFSET = -Math.ceil(Helper.SIZE / 2); - Helper.CLASS_NAME = 'helper'; - Helper.ACTIONS_TO_CURSORS = { - 'move' : 'move', - 'editLeft' : 'e-resize', - 'editRight' : 'w-resize', - 'editTop' : 'n-resize', - 'editBottom' : 's-resize', - 'editTopLeft' : 'nw-resize', - 'editTopRight' : 'ne-resize', - 'editBottomLeft' : 'sw-resize', - 'editBottomRight' : 'se-resize', - 'movePoint' : 'pointer' - }; - /** - * Set coordinates for this helper + * Set attributes for svg-elements of area by new parameters * - * @param x {number} - x-coordinate - * @param y {number} - y-coordinate - * @returns {Helper} + * -----top------ + * | | + * ---center_y--- + * | | + * ----bottom---- + * + * @param coords {Object} - Object with coords of this area (x, y, width, height) + * @returns {Rectangle} - this rectangle */ - Helper.prototype.setCoords = function(x, y) { - this._el.setAttribute('x', x + Helper.OFFSET); - this._el.setAttribute('y', y + Helper.OFFSET); + Rectangle.prototype.setSVGCoords = function(coords) { + this._el.setAttribute('x', coords.x); + this._el.setAttribute('y', coords.y); + this._el.setAttribute('width', coords.width); + this._el.setAttribute('height', coords.height); + + var top = coords.y, + center_y = coords.y + coords.height / 2, + bottom = coords.y + coords.height, + left = coords.x, + center_x = coords.x + coords.width / 2, + right = coords.x + coords.width; + + this._helpers.center.setCoords(center_x, center_y); + this._helpers.top.setCoords(center_x, top); + this._helpers.bottom.setCoords(center_x, bottom); + this._helpers.left.setCoords(left, center_y); + this._helpers.right.setCoords(right, center_y); + this._helpers.topLeft.setCoords(left, top); + this._helpers.topRight.setCoords(right, top); + this._helpers.bottomLeft.setCoords(left, bottom); + this._helpers.bottomRight.setCoords(right, bottom); return this; }; /** - * Set id of this helper in list of parent's helpers + * Set coords for this area * - * @param id {number} - * @returns {Helper} + * @param coords {coords} + * @returns {Rectangle} - this rectangle */ - Helper.prototype.setId = function(id) { - // TODO: move n-field from DOM-element to data-attribute - this._el.n = id; + Rectangle.prototype.setCoords = function(coords) { + this._coords.x = coords.x; + this._coords.y = coords.y; + this._coords.width = coords.width; + this._coords.height = coords.height; return this; }; /** - * The abstract constructor for area - * - * @constructor - * @abstract - * @param type {string} - type of area ('rectangle', 'circle' or 'polygon') - * @param coords {Object} - coordinates of area (e.g. x, y, width, height) - * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) - */ - function Area(type, coords, attributes) { - if (this.constructor === Area) { - throw new Error('This is abstract class'); - } - - this._type = type; - - /** - * @namespace - * @property href {string} - href-attribute of area - * @property alt {string} - alt-attribute of area - * @property title {string} - title-attribute of area - */ - this._attributes = { - href : '', - alt : '', - title : '' + * Calculates new coordinates in process of drawing + * + * @param x {number} - x-coordinate of cursor + * @param y {number} - y-coordinate of cursor + * @param isSquare {boolean} + * @returns {Object} - calculated coords of this area + */ + Rectangle.prototype.dynamicDraw = function(x, y, isSquare) { + var newCoords = { + x : this._coords.x, + y : this._coords.y, + width : x - this._coords.x, + height: y - this._coords.y }; - - if (attributes) { - this.setInfoAttributes(attributes); - } - - this._coords = coords; - - // the g-element, it contains this area and helpers elements - this._groupEl = document.createElementNS(Area.SVG_NS, 'g'); - app.addNodeToSvg(this._groupEl); - // TODO: remove this field from DOM-element - // Link to parent object - this._groupEl.obj = this; + if (isSquare) { + newCoords = Rectangle.getSquareCoords(newCoords); + } - // svg-dom-element of area - this._el = null; + newCoords = Rectangle.getNormalizedCoords(newCoords); - // Object with all helpers of area - this._helpers = {}; + this.redraw(newCoords); - // Add this new area to list of all areas - app.addObject(this); - } - Area.SVG_NS = 'http://www.w3.org/2000/svg'; // TODO: move to main editor constructor - Area.CLASS_NAMES = { - SELECTED : 'selected', - WITH_HREF : 'with_href' - }; - Area.CONSTRUCTORS = { - rectangle : Rectangle, - circle : Circle, - polygon : Polygon - }; - Area.REGEXP = { - AREA : //gmi, - HREF : / href="([\S\s]+?)"/, - ALT : / alt="([\S\s]+?)"/, - TITLE : / title="([\S\s]+?)"/, - DELIMETER : / ?, ?/ - }; - Area.HTML_NAMES_TO_AREA_NAMES = { - rect : 'rectangle', - circle : 'circle', - poly : 'polygon' + return newCoords; }; - Area.ATTRIBUTES_NAMES = ['HREF', 'ALT', 'TITLE']; /** - * This method should be implemented for child-classes + * Handler for drawing process (by mousemove) + * It includes only redrawing area by new coords + * (this coords doesn't save as own area coords) * - * @throws {AbstractMethodCall} + * @params e {MouseEvent} - mousemove event */ - Area.prototype.ABSTRACT_METHOD = function() { - throw new Error('This is abstract method'); + Rectangle.prototype.onProcessDrawing = function(e) { + var coords = utils.getRightCoords(e.pageX, e.pageY); + + this.dynamicDraw(coords.x, coords.y, e.shiftKey); }; /** - * All these methods are abstract + * Handler for drawing stoping (by second click on drawing canvas) + * It includes redrawing area by new coords + * and saving this coords as own area coords * - * @throws {AbstractMethodCall} + * @params e {MouseEvent} - click event */ - Area.prototype.setSVGCoords = - Area.prototype.setCoords = - Area.prototype.dynamicDraw = - Area.prototype.onProcessDrawing = - Area.prototype.onStopDrawing = - Area.prototype.edit = - Area.prototype.dynamicEdit = - Area.prototype.onProcessEditing = - Area.prototype.onStopEditing = - Area.prototype.toString = - Area.prototype.toHTMLMapElementString = - Area.prototype.getCoordsForDisplayingInfo = - Area.prototype.ABSTRACT_METHOD; + Rectangle.prototype.onStopDrawing = function(e) { + var coords = utils.getRightCoords(e.pageX, e.pageY); + + this.setCoords(this.dynamicDraw(coords.x, coords.y, e.shiftKey)).deselect(); + + app.removeAllEvents() + .setIsDraw(false) + .resetNewArea(); + }; /** - * Redraw this area with own or custom coordinates + * Changes area parameters by editing type and offsets * - * @param coords {Object} [coords=undefined] - * @returns {Area} - this area + * @param {string} editingType - A type of editing (e.g. 'move') + * @returns {Object} - Object with changed parameters of area */ - Area.prototype.redraw = function(coords) { - this.setSVGCoords(coords ? coords : this._coords); + Rectangle.prototype.edit = function(editingType, dx, dy) { + var tempParams = Object.create(this._coords); - return this; + switch (editingType) { + case 'move': + tempParams.x += dx; + tempParams.y += dy; + break; + + case 'editLeft': + tempParams.x += dx; + tempParams.width -= dx; + break; + + case 'editRight': + tempParams.width += dx; + break; + + case 'editTop': + tempParams.y += dy; + tempParams.height -= dy; + break; + + case 'editBottom': + tempParams.height += dy; + break; + + case 'editTopLeft': + tempParams.x += dx; + tempParams.y += dy; + tempParams.width -= dx; + tempParams.height -= dy; + break; + + case 'editTopRight': + tempParams.y += dy; + tempParams.width += dx; + tempParams.height -= dy; + break; + + case 'editBottomLeft': + tempParams.x += dx; + tempParams.width -= dx; + tempParams.height += dy; + break; + + case 'editBottomRight': + tempParams.width += dx; + tempParams.height += dy; + break; + } + + return tempParams; }; /** - * Remove this area from DOM-tree + * Calculates new coordinates in process of editing + * + * @param coords {Object} - area coords + * @param saveProportions {boolean} + * @returns {Object} - new coordinates of area */ - Area.prototype.remove = function() { - app.removeNodeFromSvg(this._groupEl); + Rectangle.prototype.dynamicEdit = function(coords, saveProportions) { + coords = Rectangle.getNormalizedCoords(coords); + + if (saveProportions) { + coords = Rectangle.getSavedProportionsCoords(coords); + } + + this.redraw(coords); + + return coords; }; - + /** - * Move this area by dx, dy + * Handler for editing process (by mousemove) + * It includes only redrawing area by new coords + * (this coords doesn't save as own area coords) * - * @returns {Area} - this area + * @params e {MouseEvent} - mousemove event */ - Area.prototype.move = function(dx, dy) { - this.setCoords(this.edit('move', dx, dy)).redraw(); - return this; + Rectangle.prototype.onProcessEditing = function(e) { + return this.dynamicEdit( + this.edit( + app.getEditType(), + e.pageX - this.editingStartPoint.x, + e.pageY - this.editingStartPoint.y + ), + e.shiftKey + ); }; /** - * Add class name for selected areas to this area + * Handler for editing stoping (by mouseup) + * It includes redrawing area by new coords + * and saving this coords as own area coords * - * @returns {Area} - this area + * @params e {MouseEvent} - mouseup event */ - Area.prototype.select = function() { - this._el.classList.add(Area.CLASS_NAMES.SELECTED); - console.info(this.toString() + ' is selected now'); - - return this; + Rectangle.prototype.onStopEditing = function(e) { + this.setCoords(this.onProcessEditing(e)); + app.removeAllEvents(); }; + + /** + * Returns string-representation of this rectangle + * + * @returns {string} + */ + Rectangle.prototype.toString = function() { + return 'Rectangle {x: '+ this._coords.x + + ', y: ' + this._coords.y + + ', width: ' + this._coords.width + + ', height: ' + this._coords.height + '}'; + } /** - * Remove class name for selected areas from this area + * Returns html-string of area html element with params of this rectangle * - * @returns {Area} - this area + * @returns {string} */ - Area.prototype.deselect = function() { - this._el.classList.remove(Area.CLASS_NAMES.SELECTED); - - return this; + Rectangle.prototype.toHTMLMapElementString = function() { + var x2 = this._coords.x + this._coords.width, + y2 = this._coords.y + this._coords.height; + + return '' + this._attributes.alt + ''; }; - + /** - * Set style of element with href attribute for this area + * Returns coords for area attributes form * - * @returns {Area} - this area + * @returns {Object} - object width coordinates of point */ - Area.prototype.setStyleOfElementWithHref = function() { - this._el.classList.add(Area.CLASS_NAMES.WITH_HREF); - - return this; + Rectangle.prototype.getCoordsForDisplayingInfo = function() { + return { + x : this._coords.x, + y : this._coords.y + }; }; /** - * Unset style of element with href attribute for this area - * - * @returns {Area} - this area + * Returns true if coords is valid for rectangles and false otherwise + * + * @static + * @param coords {Object} - object with coords for new rectangle + * @return {boolean} */ - Area.prototype.unsetStyleOfElementWithHref = function() { - this._el.classList.remove(Area.CLASS_NAMES.WITH_HREF); - - return this; + Rectangle.testCoords = function(coords) { + return coords.x && coords.y && coords.width && coords.height; }; /** - * Set attributes (href, alt and title) for this area - * - * @param attributes {Object} - Object with attributes for area + * Returns true if html coords array is valid for rectangles and false otherwise + * + * @static + * @param coords {Array} - coords for new rectangle as array + * @return {boolean} */ - Area.prototype.setInfoAttributes = function(attributes) { - this._attributes.href = attributes.href; - this._attributes.alt = attributes.alt; - this._attributes.title = attributes.title; + Rectangle.testHTMLCoords = function(coords) { + return coords.length === 4; }; - + /** - * Returns json-representation of this area + * Return rectangle coords object from html array * + * @param htmlCoordsArray {Array} * @returns {Object} */ - Area.prototype.toJSON = function() { - /** - * @namespace - * @property type {string} - type of this area (e.g. 'rectangle', 'circle') - * @property coords {Object} - coordinates of this area (e.g. 'x', 'width') - * @property attributes {Object} - attributes of this area (e.g. 'href', 'title') - */ + Rectangle.getCoordsFromHTMLArray = function(htmlCoordsArray) { + if (!Rectangle.testHTMLCoords(htmlCoordsArray)) { + throw new Error('This html-coordinates is not valid for rectangle'); + } + return { - type : this._type, - coords : this._coords, - attributes : this._attributes + x : htmlCoordsArray[0], + y : htmlCoordsArray[1], + width : htmlCoordsArray[2] - htmlCoordsArray[0], + height : htmlCoordsArray[3] - htmlCoordsArray[1] }; }; - + /** - * Returns new area object created with params from json-object + * Fixes coords if width or/and height are negative * * @static - * @param params {Object} - params of area, incl. type, coords and attributes - * @returns {Rectangle|Circle|Polygon} + * @param coords {Object} - Coordinates of this area + * @returns {Object} - Normalized coordinates of area */ - Area.fromJSON = function(params) { - var AreaConstructor = Area.CONSTRUCTORS[params.type]; - - if (!AreaConstructor) { - throw new Error('This area type is not valid'); + Rectangle.getNormalizedCoords = function(coords) { + if (coords.width < 0) { + coords.x += coords.width; + coords.width = Math.abs(coords.width); } - if (!AreaConstructor.testCoords(params.coords)) { - throw new Error('This coords is not valid for ' + params.type); + if (coords.height < 0) { + coords.y += coords.height; + coords.height = Math.abs(coords.height); } - app.setIsDraw(true); - - var area = new AreaConstructor(params.coords, params.attributes); + return coords; + }; + + /** + * Returns coords with equivivalent width and height + * + * @static + * @param coords {Object} - Coordinates of this area + * @returns {Object} - Coordinates of area with equivivalent width and height + */ + Rectangle.getSquareCoords = function(coords) { + var width = Math.abs(coords.width), + height = Math.abs(coords.height); - app.setIsDraw(false) - .resetNewArea(); + if (width > height) { + coords.width = coords.width > 0 ? height : -height; + } else { + coords.height = coords.height > 0 ? width : -width; + } - return area; + return coords; }; - + /** - * Creates new areas from html-string with elements + * Returns coords with saved proportions of original area * - * @param htmlStr {string} - * @returns {Array} - array with areas + * @static + * @param coords {Object} - Coordinates of this area + * @param originalCoords {Object} - Coordinates of the original area + * @returns {Object} - Coordinates of area with saved proportions of original area */ - Area.createAreasFromHTMLOfMap = function(htmlStr) { - if (!htmlStr) { - return false; - } - - while (true) { - var findedResult = Area.REGEXP.AREA.exec(htmlStr); // - if (!findedResult) { - break; - } - - var htmlAreaFinded = findedResult[0], // - type = findedResult[1], // $1 - coords = findedResult[2].split(Area.REGEXP.DELIMETER), // $2 - attributes = {}; - - Area.ATTRIBUTES_NAMES.forEach(function(item, i) { - var result = Area.REGEXP[item].exec(htmlAreaFinded); - - if (result) { - attributes[name] = result[1]; - } - }); - - coords = coords.map(function(item) { - return Number(item); - }); - - type = Area.HTML_NAMES_TO_AREA_NAMES[type]; - - Area.fromJSON({ - type : type, - coords : Area.CONSTRUCTORS[type].getCoordsFromHTMLArray(coords), - attributes : attributes - }); - + Rectangle.getSavedProportionsCoords = function(coords, originalCoords) { + var originalProportions = coords.width / coords.height, + currentProportions = originalCoords.width / originalCoords.height; + + if (currentProportions > originalProportions) { + coords.width = Math.round(coords.height * originalProportions); + } else { + coords.height = Math.round(coords.width / originalProportions); } - - return Boolean(htmlAreaFinded); + + return coords; }; - + /** - * Returns copy of original area, selected and moved by (10,10) from it + * Creates new rectangle and adds drawing handlers for DOM-elements * - * @param originalArea {Area} - * @returns {Area} - a copy of original area + * @static + * @param firstPointCoords {Object} + * @returns {Rectangle} */ - Area.copy = function(originalArea) { - return Area.fromJSON(originalArea.toJSON()).move(10, 10).select(); + Rectangle.createAndStartDrawing = function(firstPointCoords) { + var newArea = new Rectangle({ + x : firstPointCoords.x, + y : firstPointCoords.y, + width: 0, + height: 0 + }); + + app.addEvent(app.domElements.container, 'mousemove', newArea.onProcessDrawing.bind(newArea)) + .addEvent(app.domElements.container, 'click', newArea.onStopDrawing.bind(newArea)); + + return newArea; }; - - /* ---------- Constructors for real areas ---------- */ + /** - * The constructor for rectangles - * - * (x, y) ----- - * | | height - * ------------ - * width + * The constructor for circles + * + * ------ + * / \ + * | (x, y)<->| radius + * \ / + * ------ * * @constructor - * @param coords {Object} - object with parameters of new area (x, y, width, height) + * @param coords {Object} - object with parameters of new area (cx, cy, radius) * if some parameter is undefined, it will set 0 * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) */ - function Rectangle(coords, attributes) { - Area.call(this, 'rectangle', coords, attributes); + function Circle(coords, attributes) { + Area.call(this, 'circle', coords, attributes); /** * @namespace - * @property {number} x - Distance from the left edge of the image to the left side of the rectangle - * @property {number} y - Distance from the top edge of the image to the top side of the rectangle - * @property {number} width - Width of rectangle - * @property {number} height - Height of rectangle + * @property {number} cx - Distance from the left edge of the image to the center of the circle + * @property {number} cy - Distance from the top edge of the image to the center of the circle + * @property {number} radius - Radius of the circle */ this._coords = { - x : coords.x || 0, - y : coords.y || 0, - width : coords.width || 0, - height : coords.height || 0 + cx : coords.cx || 0, + cy : coords.cy || 0, + radius : coords.radius || 0 }; - - this._el = document.createElementNS(Area.SVG_NS, 'rect'); - this._groupEl.appendChild(this._el); - - var x = coords.x - this._coords.width / 2, - y = coords.y - this._coords.height / 2; - this._helpers = { - center : new Helper(this._groupEl, x, y, 'move'), - top : new Helper(this._groupEl, x, y, 'editTop'), - bottom : new Helper(this._groupEl, x, y, 'editBottom'), - left : new Helper(this._groupEl, x, y, 'editLeft'), - right : new Helper(this._groupEl, x, y, 'editRight'), - topLeft : new Helper(this._groupEl, x, y, 'editTopLeft'), - topRight : new Helper(this._groupEl, x, y, 'editTopRight'), - bottomLeft : new Helper(this._groupEl, x, y, 'editBottomLeft'), - bottomRight : new Helper(this._groupEl, x, y, 'editBottomRight') + this._el = document.createElementNS(Area.SVG_NS, 'circle'); + this._groupEl.appendChild(this._el); + + this.helpers = { + center : new Helper(this._groupEl, coords.cx, coords.cy, 'move'), + top : new Helper(this._groupEl, coords.cx, coords.cy, 'editTop'), + bottom : new Helper(this._groupEl, coords.cx, coords.cy, 'editBottom'), + left : new Helper(this._groupEl, coords.cx, coords.cy, 'editLeft'), + right : new Helper(this._groupEl, coords.cx, coords.cy, 'editRight') }; - + this.redraw(); } - utils.inherits(Rectangle, Area); + utils.inherits(Circle, Area); /** * Set attributes for svg-elements of area by new parameters * - * -----top------ - * | | - * ---center_y--- - * | | - * ----bottom---- - * - * @param coords {Object} - Object with coords of this area (x, y, width, height) - * @returns {Rectangle} - this rectangle + * @param coords {Object} - Object with coords of this area (cx, cy, radius) + * @returns {Circle} - this area */ - Rectangle.prototype.setSVGCoords = function(coords) { - this._el.setAttribute('x', coords.x); - this._el.setAttribute('y', coords.y); - this._el.setAttribute('width', coords.width); - this._el.setAttribute('height', coords.height); - - var top = coords.y, - center_y = coords.y + coords.height / 2, - bottom = coords.y + coords.height, - left = coords.x, - center_x = coords.x + coords.width / 2, - right = coords.x + coords.width; + Circle.prototype.setSVGCoords = function(coords) { + this._el.setAttribute('cx', coords.cx); + this._el.setAttribute('cy', coords.cy); + this._el.setAttribute('r', coords.radius); - this._helpers.center.setCoords(center_x, center_y); - this._helpers.top.setCoords(center_x, top); - this._helpers.bottom.setCoords(center_x, bottom); - this._helpers.left.setCoords(left, center_y); - this._helpers.right.setCoords(right, center_y); - this._helpers.topLeft.setCoords(left, top); - this._helpers.topRight.setCoords(right, top); - this._helpers.bottomLeft.setCoords(left, bottom); - this._helpers.bottomRight.setCoords(right, bottom); + this.helpers.center.setCoords(coords.cx, coords.cy); + this.helpers.top.setCoords(coords.cx, coords.cy - coords.radius); + this.helpers.right.setCoords(coords.cx + coords.radius, coords.cy); + this.helpers.bottom.setCoords(coords.cx, coords.cy + coords.radius); + this.helpers.left.setCoords(coords.cx - coords.radius, coords.cy); return this; }; @@ -1684,40 +1529,39 @@ var summerHtmlImageMapCreator = (function() { /** * Set coords for this area * - * @param coords {coords} - * @returns {Rectangle} - this rectangle + * @param coords {Object} - coordinates for thia area + * @returns {Circle} - this area */ - Rectangle.prototype.setCoords = function(coords) { - this._coords.x = coords.x; - this._coords.y = coords.y; - this._coords.width = coords.width; - this._coords.height = coords.height; + Circle.prototype.setCoords = function(coords) { + this._coords.cx = coords.cx; + this._coords.cy = coords.cy; + this._coords.radius = coords.radius; return this; }; /** * Calculates new coordinates in process of drawing + * (for circle normalizeCoords() don't needed because + * radius are always positive) * - * @param x {number} - x-coordinate of cursor - * @param y {number} - y-coordinate of cursor - * @param isSquare {boolean} - * @returns {Object} - calculated coords of this area + * @param x {number} - x-coordinate + * @param y {number} - y-coordinate + * @returns {Object} - calculated coordinates */ - Rectangle.prototype.dynamicDraw = function(x, y, isSquare) { - var newCoords = { - x : this._coords.x, - y : this._coords.y, - width : x - this._coords.x, - height: y - this._coords.y - }; - - if (isSquare) { - newCoords = Rectangle.getSquareCoords(newCoords); - } - - newCoords = Rectangle.getNormalizedCoords(newCoords); - + Circle.prototype.dynamicDraw = function(x, y) { + var radius = Math.round( + Math.sqrt( + Math.pow(this._coords.cx - x, 2) + + Math.pow(this._coords.cy - y, 2) + ) + ), + newCoords = { + cx : this._coords.cx, + cy : this._coords.cy, + radius : radius + }; + this.redraw(newCoords); return newCoords; @@ -1730,24 +1574,24 @@ var summerHtmlImageMapCreator = (function() { * * @params e {MouseEvent} - mousemove event */ - Rectangle.prototype.onProcessDrawing = function(e) { + Circle.prototype.onProcessDrawing = function(e) { var coords = utils.getRightCoords(e.pageX, e.pageY); - - this.dynamicDraw(coords.x, coords.y, e.shiftKey); + + this.dynamicDraw(coords.x, coords.y); }; /** - * Handler for drawing stoping (by second click on drawing canvas) + * Handler for drawing stoping (by second click) * It includes redrawing area by new coords * and saving this coords as own area coords * * @params e {MouseEvent} - click event */ - Rectangle.prototype.onStopDrawing = function(e) { + Circle.prototype.onStopDrawing = function(e) { var coords = utils.getRightCoords(e.pageX, e.pageY); - this.setCoords(this.dynamicDraw(coords.x, coords.y, e.shiftKey)).deselect(); - + this.setCoords(this.dynamicDraw(coords.x, coords.y)).deselect(); + app.removeAllEvents() .setIsDraw(false) .resetNewArea(); @@ -1756,81 +1600,52 @@ var summerHtmlImageMapCreator = (function() { /** * Changes area parameters by editing type and offsets * - * @param {string} editingType - A type of editing (e.g. 'move') + * @param {string} editingType - A type of editing * @returns {Object} - Object with changed parameters of area */ - Rectangle.prototype.edit = function(editingType, dx, dy) { + Circle.prototype.edit = function(editingType, dx, dy) { var tempParams = Object.create(this._coords); switch (editingType) { case 'move': - tempParams.x += dx; - tempParams.y += dy; - break; - - case 'editLeft': - tempParams.x += dx; - tempParams.width -= dx; - break; - - case 'editRight': - tempParams.width += dx; + tempParams.cx += dx; + tempParams.cy += dy; break; - + case 'editTop': - tempParams.y += dy; - tempParams.height -= dy; + tempParams.radius -= dy; break; case 'editBottom': - tempParams.height += dy; - break; - - case 'editTopLeft': - tempParams.x += dx; - tempParams.y += dy; - tempParams.width -= dx; - tempParams.height -= dy; - break; - - case 'editTopRight': - tempParams.y += dy; - tempParams.width += dx; - tempParams.height -= dy; + tempParams.radius += dy; break; - case 'editBottomLeft': - tempParams.x += dx; - tempParams.width -= dx; - tempParams.height += dy; + case 'editLeft': + tempParams.radius -= dx; break; - case 'editBottomRight': - tempParams.width += dx; - tempParams.height += dy; + case 'editRight': + tempParams.radius += dx; break; } - + return tempParams; }; /** * Calculates new coordinates in process of editing * - * @param coords {Object} - area coords - * @param saveProportions {boolean} - * @returns {Object} - new coordinates of area + * @param tempCoords {Object} - area coords + * @returns {Object} - calculated coordinates */ - Rectangle.prototype.dynamicEdit = function(coords, saveProportions) { - coords = Rectangle.getNormalizedCoords(coords); - - if (saveProportions) { - coords = Rectangle.getSavedProportionsCoords(coords); + Circle.prototype.dynamicEdit = function(tempCoords) { + if (tempCoords.radius < 0) { + tempCoords.radius = Math.abs(tempCoords.radius); } - this.redraw(coords); + this.setSVGCoords(tempCoords); - return coords; + return tempCoords; }; /** @@ -1840,14 +1655,11 @@ var summerHtmlImageMapCreator = (function() { * * @params e {MouseEvent} - mousemove event */ - Rectangle.prototype.onProcessEditing = function(e) { + Circle.prototype.onProcessEditing = function(e) { + var editType = app.getEditType(); + return this.dynamicEdit( - this.edit( - app.getEditType(), - e.pageX - this.editingStartPoint.x, - e.pageY - this.editingStartPoint.y - ), - e.shiftKey + this.edit(editType, e.pageX - this.editingStartPoint.x, e.pageY - this.editingStartPoint.y) ); }; @@ -1858,37 +1670,35 @@ var summerHtmlImageMapCreator = (function() { * * @params e {MouseEvent} - mouseup event */ - Rectangle.prototype.onStopEditing = function(e) { + Circle.prototype.onStopEditing = function(e) { + var editType = app.getEditType(); + this.setCoords(this.onProcessEditing(e)); + app.removeAllEvents(); }; - + /** - * Returns string-representation of this rectangle + * Returns string-representation of circle * * @returns {string} */ - Rectangle.prototype.toString = function() { - return 'Rectangle {x: '+ this._coords.x + - ', y: ' + this._coords.y + - ', width: ' + this._coords.width + - ', height: ' + this._coords.height + '}'; + Circle.prototype.toString = function() { + return 'Circle {cx: '+ this._coords.cx + + ', cy: ' + this._coords.cy + + ', radius: ' + this._coords.radius + '}'; } /** - * Returns html-string of area html element with params of this rectangle + * Returns html-string of area html element with params of this circle * * @returns {string} */ - Rectangle.prototype.toHTMLMapElementString = function() { - var x2 = this._coords.x + this._coords.width, - y2 = this._coords.y + this._coords.height; - - return '' + this._attributes.alt + ' height) { - coords.width = coords.width > 0 ? height : -height; - } else { - coords.height = coords.height > 0 ? width : -width; - } - - return coords; + Circle.testHTMLCoords = function(coords) { + return coords.length === 3; }; - + /** - * Returns coords with saved proportions of original area + * Returns circle coords object from html array * - * @static - * @param coords {Object} - Coordinates of this area - * @param originalCoords {Object} - Coordinates of the original area - * @returns {Object} - Coordinates of area with saved proportions of original area + * @param htmlCoordsArray {Array} + * @returns {Object} */ - Rectangle.getSavedProportionsCoords = function(coords, originalCoords) { - var originalProportions = coords.width / coords.height, - currentProportions = originalCoords.width / originalCoords.height; - - if (currentProportions > originalProportions) { - coords.width = Math.round(coords.height * originalProportions); - } else { - coords.height = Math.round(coords.width / originalProportions); + Circle.getCoordsFromHTMLArray = function(htmlCoordsArray) { + if (!Circle.testHTMLCoords(htmlCoordsArray)) { + throw new Error('This html-coordinates is not valid for circle'); } - - return coords; + + return { + cx : htmlCoordsArray[0], + cy : htmlCoordsArray[1], + radius : htmlCoordsArray[2] + }; }; /** - * Creates new rectangle and adds drawing handlers for DOM-elements - * + * Creates new circle and adds drawing handlers for DOM-elements + * * @static - * @param firstPointCoords {Object} - * @returns {Rectangle} + * @param firstPointCoords {Object} + * @returns {Circle} */ - Rectangle.createAndStartDrawing = function(firstPointCoords) { - var newArea = new Rectangle({ - x : firstPointCoords.x, - y : firstPointCoords.y, - width: 0, - height: 0 + Circle.createAndStartDrawing = function(firstPointCoords) { + var newArea = new Circle({ + cx : firstPointCoords.x, + cy : firstPointCoords.y, + radius : 0 }); app.addEvent(app.domElements.container, 'mousemove', newArea.onProcessDrawing.bind(newArea)) @@ -2031,68 +1777,88 @@ var summerHtmlImageMapCreator = (function() { return newArea; }; - /** - * The constructor for circles - * - * ------ - * / \ - * | (x, y)<->| radius - * \ / - * ------ + * The constructor for polygons + * + * {x0, y0} + * /\ + * / \ + * / \ + * {x1, y1} ----- {x2, y2} * * @constructor - * @param coords {Object} - object with parameters of new area (cx, cy, radius) - * if some parameter is undefined, it will set 0 - * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) + * @param coords {Object} - object with parameters of new area ('points' is list of points) + * if 'points' is empty, it will set to [(0,0)] + * @param coords.isOpened {boolean} - if polygon is opened (polyline instead polygon) + * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) */ - function Circle(coords, attributes) { - Area.call(this, 'circle', coords, attributes); + function Polygon(coords, attributes) { + Area.call(this, 'polygon', coords, attributes); /** * @namespace - * @property {number} cx - Distance from the left edge of the image to the center of the circle - * @property {number} cy - Distance from the top edge of the image to the center of the circle - * @property {number} radius - Radius of the circle + * @property {Array} points - Array with coordinates of polygon points */ this._coords = { - cx : coords.cx || 0, - cy : coords.cy || 0, - radius : coords.radius || 0 + points : coords.points || [{x: 0, y: 0}], + isOpened : coords.isOpened || false }; - this._el = document.createElementNS(Area.SVG_NS, 'circle'); + this._el = document.createElementNS( + Area.SVG_NS, + this._coords.isOpened ? 'polyline' : 'polygon' + ); this._groupEl.appendChild(this._el); - - this.helpers = { - center : new Helper(this._groupEl, coords.cx, coords.cy, 'move'), - top : new Helper(this._groupEl, coords.cx, coords.cy, 'editTop'), - bottom : new Helper(this._groupEl, coords.cx, coords.cy, 'editBottom'), - left : new Helper(this._groupEl, coords.cx, coords.cy, 'editLeft'), - right : new Helper(this._groupEl, coords.cx, coords.cy, 'editRight') + + this._helpers = { + points : [] }; - + + for (var i = 0, c = this._coords.points.length; i < c; i++) { + this._helpers.points.push( + (new Helper( + this._groupEl, + this._coords.points[i].x, + this._coords.points[i].y, + 'movePoint') + ).setId(i) + ); + } + + this._selectedPoint = -1; + this.redraw(); } - utils.inherits(Circle, Area); + utils.inherits(Polygon, Area); + + /** + * Closes path of the polygon (replaces polyline with polygon) + */ + Polygon.prototype.close = function() { + var polyline = this._el; + this._el = document.createElementNS(Area.SVG_NS, 'polygon'); + this._groupEl.replaceChild(this._el, polyline); + + this._coords.isOpened = false; + this.redraw().deselect(); + }; /** * Set attributes for svg-elements of area by new parameters * - * @param coords {Object} - Object with coords of this area (cx, cy, radius) - * @returns {Circle} - this area + * @param coords {Object} - Object with coords of this area, with field 'points' + * @returns {Polygon} - this area */ - Circle.prototype.setSVGCoords = function(coords) { - this._el.setAttribute('cx', coords.cx); - this._el.setAttribute('cy', coords.cy); - this._el.setAttribute('r', coords.radius); - - this.helpers.center.setCoords(coords.cx, coords.cy); - this.helpers.top.setCoords(coords.cx, coords.cy - coords.radius); - this.helpers.right.setCoords(coords.cx + coords.radius, coords.cy); - this.helpers.bottom.setCoords(coords.cx, coords.cy + coords.radius); - this.helpers.left.setCoords(coords.cx - coords.radius, coords.cy); + Polygon.prototype.setSVGCoords = function(coords) { + var polygonPointsAttrValue = coords.points.reduce(function(previousValue, currentItem) { + return previousValue + currentItem.x + ' ' + currentItem.y + ' '; + }, ''); + + this._el.setAttribute('points', polygonPointsAttrValue); + utils.foreach(this._helpers.points, function(helper, i) { + helper.setCoords(coords.points[i].x, coords.points[i].y); + }); return this; }; @@ -2100,176 +1866,211 @@ var summerHtmlImageMapCreator = (function() { /** * Set coords for this area * - * @param coords {Object} - coordinates for thia area - * @returns {Circle} - this area + * @param coords {coords} + * @returns {Polygon} - this area */ - Circle.prototype.setCoords = function(coords) { - this._coords.cx = coords.cx; - this._coords.cy = coords.cy; - this._coords.radius = coords.radius; + Polygon.prototype.setCoords = function(coords) { + this._coords.points = coords.points; + + return this; + }; + + /** + * Adds new point to polygon (and new helper too) + * + * @param x {number} - x-coordinate of new point + * @param y {number} - y-coordinate of new point + * @returns {Polygon} - this area + */ + Polygon.prototype.addPoint = function(x, y) { + if (!this._coords.isOpened) { + throw new Error('This polygon is closed!'); + } + + var helper = new Helper(this._groupEl, x, y, 'movePoint'); + helper.setId(this._helpers.points.length); + + this._helpers.points.push(helper); + this._coords.points.push({ + x : x, + y : y + }); + this.redraw(); + + return this; + }; + + /** + * Calculates new coordinates in process of drawing + * + * @param x {number} + * @param y {number} + * @param isRightAngle {boolean} + * @returns {Object} - calculated coordinates + */ + Polygon.prototype.dynamicDraw = function(x, y, isRightAngle) { + var temp_coords = { + points: [].concat(this._coords.points) + }; + + if (isRightAngle) { + var rightPointCoords = Polygon.getRightAngleLineLastPointCoords( + this._coords, {x: x, y: y} + ); + x = rightPointCoords.x; + y = rightPointCoords.y; + } - return this; - }; + temp_coords.points.push({x : x, y : y}); - /** - * Calculates new coordinates in process of drawing - * (for circle normalizeCoords() don't needed because - * radius are always positive) - * - * @param x {number} - x-coordinate - * @param y {number} - y-coordinate - * @returns {Object} - calculated coordinates - */ - Circle.prototype.dynamicDraw = function(x, y) { - var radius = Math.round( - Math.sqrt( - Math.pow(this._coords.cx - x, 2) + - Math.pow(this._coords.cy - y, 2) - ) - ), - newCoords = { - cx : this._coords.cx, - cy : this._coords.cy, - radius : radius - }; - - this.redraw(newCoords); + this.redraw(temp_coords); - return newCoords; + return temp_coords; }; /** * Handler for drawing process (by mousemove) * It includes only redrawing area by new coords - * (this coords doesn't save as own area coords) + * (this coordinates doesn't save as own area coords) * * @params e {MouseEvent} - mousemove event */ - Circle.prototype.onProcessDrawing = function(e) { + Polygon.prototype.onProcessDrawing = function(e) { var coords = utils.getRightCoords(e.pageX, e.pageY); - - this.dynamicDraw(coords.x, coords.y); + + this.dynamicDraw(coords.x, coords.y, e.shiftKey); }; /** - * Handler for drawing stoping (by second click) - * It includes redrawing area by new coords - * and saving this coords as own area coords + * Handler for polygon pointer adding (by click on drawing canvas) + * It includes redrawing area with this new point + * and saving this point to list of polygon points * * @params e {MouseEvent} - click event */ - Circle.prototype.onStopDrawing = function(e) { - var coords = utils.getRightCoords(e.pageX, e.pageY); + Polygon.prototype.onAddPointDrawing = function(e) { + var newPointCoords = utils.getRightCoords(e.pageX, e.pageY); + + if (e.shiftKey) { + newPointCoords = Polygon.getRightAngleLineLastPointCoords(this._coords, newPointCoords); + } - this.setCoords(this.dynamicDraw(coords.x, coords.y)).deselect(); - - app.removeAllEvents() - .setIsDraw(false) - .resetNewArea(); + this.addPoint(newPointCoords.x, newPointCoords.y); }; + /** + * Handler for drawing stoping (by click on first helper or press ENTER key) + * It includes redrawing area by new coords, closing this polygon + * and saving this coords as own area coords + * + * @params e {KeyboardEvent|MouseEvent} - click or keydown event + */ + Polygon.prototype.onStopDrawing = function(e) { + if (e.type == 'click' || (e.type == 'keydown' && e.keyCode == 13)) { + if (Polygon.testCoords(this._coords)) { + this.close(); + + app.removeAllEvents() + .setIsDraw(false) + .resetNewArea(); + } + } + e.stopPropagation(); + }; + /** * Changes area parameters by editing type and offsets * * @param {string} editingType - A type of editing * @returns {Object} - Object with changed parameters of area */ - Circle.prototype.edit = function(editingType, dx, dy) { + Polygon.prototype.edit = function(editingType, dx, dy) { var tempParams = Object.create(this._coords); switch (editingType) { case 'move': - tempParams.cx += dx; - tempParams.cy += dy; - break; - - case 'editTop': - tempParams.radius -= dy; - break; - - case 'editBottom': - tempParams.radius += dy; - break; - - case 'editLeft': - tempParams.radius -= dx; + for (var i = 0, c = tempParams.points.length; i < c; i++) { + tempParams.points[i].x += dx; + tempParams.points[i].y += dy; + } break; - case 'editRight': - tempParams.radius += dx; + case 'movePoint': + tempParams.points[this.selected_point].x += dx; + tempParams.points[this.selected_point].y += dy; break; } - + return tempParams; }; - /** - * Calculates new coordinates in process of editing - * - * @param tempCoords {Object} - area coords - * @returns {Object} - calculated coordinates - */ - Circle.prototype.dynamicEdit = function(tempCoords) { - if (tempCoords.radius < 0) { - tempCoords.radius = Math.abs(tempCoords.radius); - } - - this.setSVGCoords(tempCoords); - - return tempCoords; - }; - /** * Handler for editing process (by mousemove) - * It includes only redrawing area by new coords - * (this coords doesn't save as own area coords) + * It includes only redrawing area by new coords + * (this coords doesn't save as area coords) * * @params e {MouseEvent} - mousemove event */ - Circle.prototype.onProcessEditing = function(e) { + Polygon.prototype.onProcessEditing = function(e) { var editType = app.getEditType(); - return this.dynamicEdit( - this.edit(editType, e.pageX - this.editingStartPoint.x, e.pageY - this.editingStartPoint.y) + this.redraw( + this.edit( + editType, + e.pageX - this.editingStartPoint.x, + e.pageY - this.editingStartPoint.y + ) ); + + this.editingStartPoint.x = e.pageX; + this.editingStartPoint.y = e.pageY; }; /** - * Handler for editing stoping (by mouseup) - * It includes redrawing area by new coords - * and saving this coords as own area coords + * Handler for editing stoping (by mouseup on drawing canvas) + * It includes redrawing area by new coords and saving this new coords + * as own area coords * - * @params e {MouseEvent} - mouseup event + * @params e {MouseEvent} - click or keydown event */ - Circle.prototype.onStopEditing = function(e) { + Polygon.prototype.onStopEditing = function(e) { var editType = app.getEditType(); - - this.setCoords(this.onProcessEditing(e)); - + + this.setCoords( + this.edit( + editType, + e.pageX - this.editingStartPoint.x, + e.pageY - this.editingStartPoint.y + ) + ).redraw(); + app.removeAllEvents(); }; /** - * Returns string-representation of circle + * Returns string-representation of polygon * * @returns {string} */ - Circle.prototype.toString = function() { - return 'Circle {cx: '+ this._coords.cx + - ', cy: ' + this._coords.cy + - ', radius: ' + this._coords.radius + '}'; + Polygon.prototype.toString = function() { + return 'Polygon {points: ['+ + this._coords.points.map(function(item) { + return '[' + item.x + ', ' + item.y + ']' + }).join(', ') + ']}'; } /** - * Returns html-string of area html element with params of this circle + * Returns html-string of area html element with params of this polygon * * @returns {string} */ - Circle.prototype.toHTMLMapElementString = function() { - return '' + this._attributes.alt + '= 3; }; /** - * Returns true if html coords array is valid for circles and false otherwise + * Returns true if html coords array is valid for polygons and false otherwise * * @static - * @param coords {Array} - coords for new circle as array - * @return {boolean} + * @param coords {Array} - coords for new polygon as array [x1, y1, x2, y2, x3, y3, ...] + * @returns {boolean} */ - Circle.testHTMLCoords = function(coords) { - return coords.length === 3; + Polygon.testHTMLCoords = function(coords) { + return coords.length >= 6 && coords.length % 2 === 0; }; /** - * Returns circle coords object from html array + * Returns polygon coords object from html array * * @param htmlCoordsArray {Array} - * @returns {Object} + * @returns {Object} - object with calculated points */ - Circle.getCoordsFromHTMLArray = function(htmlCoordsArray) { - if (!Circle.testHTMLCoords(htmlCoordsArray)) { - throw new Error('This html-coordinates is not valid for circle'); + Polygon.getCoordsFromHTMLArray = function(htmlCoordsArray) { + if (!Polygon.testHTMLCoords(htmlCoordsArray)) { + throw new Error('This html-coordinates is not valid for polygon'); + } + + var points = []; + for (var i = 0, c = htmlCoordsArray.length/2; i < c; i++) { + points.push({ + x : htmlCoordsArray[2*i], + y : htmlCoordsArray[2*i + 1] + }); + } + + return { + points : points + }; + }; + + /** + * Returns coords of new point with right angle (or 45 deg) for last line + * This method recalculates coordinates of new point for + * last line with (0 | 45 | 90 | 135 | 180 | 225 | 270 | 315) deg + * For example, + * for (0 < deg < 23) -> 0 deg + * for (23 < deg < 67) -> 45 deg + * for (67 < deg < 90) -> 90 deg etc. + * + * 0 + * 315\ | /45 + * \|/ + * 270 --------- 90 + * /|\ + * 225/ | \135 + * 180 + * + * @static + * @param originalCoords {Object} - Coordinates of this area (without new point) + * @param newPointCoords {Object} - Coordinates of new point + * @returns {Object} - Right coordinates for last point + */ + Polygon.getRightAngleLineLastPointCoords = function(originalCoords, newPointCoords) { + var TANGENS = { + DEG_22 : 0.414, + DEG_67 : 2.414 + }, + lastPointIndex = originalCoords.points.length - 1, + lastPoint = originalCoords.points[lastPointIndex], + dx = newPointCoords.x - lastPoint.x, + dy = -(newPointCoords.y - lastPoint.y), + tan = dy / dx, + x = newPointCoords.x, + y = newPointCoords.y; + + if (dx > 0 && dy > 0) { + if (tan > TANGENS.DEG_67) { + x = lastPoint.x; + } else if (tan < TANGENS.DEG_22) { + y = lastPoint.y; + } else { + Math.abs(dx) > Math.abs(dy) ? + (x = lastPoint.x + dy) : (y = lastPoint.y - dx); + } + } else if (dx < 0 && dy > 0) { + if (tan < -TANGENS.DEG_67) { + x = lastPoint.x; + } else if (tan > -TANGENS.DEG_22) { + y = lastPoint.y; + } else { + Math.abs(dx) > Math.abs(dy) ? + (x = lastPoint.x - dy) : (y = lastPoint.y + dx); + } + } else if (dx < 0 && dy < 0) { + if (tan > TANGENS.DEG_67) { + x = lastPoint.x; + } else if (tan < TANGENS.DEG_22) { + y = lastPoint.y; + } else { + Math.abs(dx) > Math.abs(dy) ? + (x = lastPoint.x + dy) : (y = lastPoint.y - dx); + } + } else if (dx > 0 && dy < 0) { + if (tan < -TANGENS.DEG_67) { + x = lastPoint.x; + } else if (tan > -TANGENS.DEG_22) { + y = lastPoint.y; + } else { + Math.abs(dx) > Math.abs(dy) ? + (x = lastPoint.x - dy) : (y = lastPoint.y + dx); + } } - + return { - cx : htmlCoordsArray[0], - cy : htmlCoordsArray[1], - radius : htmlCoordsArray[2] + x : x, + y : y }; }; /** - * Creates new circle and adds drawing handlers for DOM-elements + * Creates new polygon and add drawing handlers for DOM-elements * * @static - * @param firstPointCoords {Object} - * @returns {Circle} + * @param firstPointCoords {Object} - coords for first point of polyline + * @returns {Polygon} - created polygon */ - Circle.createAndStartDrawing = function(firstPointCoords) { - var newArea = new Circle({ - cx : firstPointCoords.x, - cy : firstPointCoords.y, - radius : 0 + Polygon.createAndStartDrawing = function(firstPointCoords) { + var newArea = new Polygon({ + points : [firstPointCoords], + isOpened : true }); app.addEvent(app.domElements.container, 'mousemove', newArea.onProcessDrawing.bind(newArea)) - .addEvent(app.domElements.container, 'click', newArea.onStopDrawing.bind(newArea)); + .addEvent(app.domElements.container, 'click', newArea.onAddPointDrawing.bind(newArea)) + .addEvent(document, 'keydown', newArea.onStopDrawing.bind(newArea)) + .addEvent(newArea._helpers.points[0]._el, 'click', newArea.onStopDrawing.bind(newArea)); return newArea; }; - /** - * The constructor for polygons - * - * {x0, y0} - * /\ - * / \ - * / \ - * {x1, y1} ----- {x2, y2} - * - * @constructor - * @param coords {Object} - object with parameters of new area ('points' is list of points) - * if 'points' is empty, it will set to [(0,0)] - * @param coords.isOpened {boolean} - if polygon is opened (polyline instead polygon) - * @param attributes {Object} [attributes=undefined] - attributes for area (e.g. href, title) - */ - function Polygon(coords, attributes) { - Area.call(this, 'polygon', coords, attributes); + /* TODO: this modules will use app.js */ + /* Help block */ + var help = (function() { + var block = utils.id('help'), + overlay = utils.id('overlay'), + close_button = block.querySelector('.close_button'); + + function hide() { + utils.hide(block); + utils.hide(overlay); + } - /** - * @namespace - * @property {Array} points - Array with coordinates of polygon points - */ - this._coords = { - points : coords.points || [{x: 0, y: 0}], - isOpened : coords.isOpened || false + function show() { + utils.show(block); + utils.show(overlay); + } + + overlay.addEventListener('click', hide, false); + + close_button.addEventListener('click', hide, false); + + return { + show : show, + hide : hide }; - - this._el = document.createElementNS( - Area.SVG_NS, - this._coords.isOpened ? 'polyline' : 'polygon' - ); - this._groupEl.appendChild(this._el); + })(); - this._helpers = { - points : [] + /* For html code of created map */ + var code = (function(){ + var block = utils.id('code'), + content = utils.id('code_content'), + close_button = block.querySelector('.close_button'); + + close_button.addEventListener('click', function(e) { + utils.hide(block); + e.preventDefault(); + }, false); + + return { + print: function() { + content.innerHTML = app.getHTMLCode(true); + utils.show(block); + }, + hide: function() { + utils.hide(block); + } + }; + })(); + + + /* Edit selected area info */ + var info = (function() { + var form = utils.id('edit_details'), + header = form.querySelector('h5'), + href_attr = utils.id('href_attr'), + alt_attr = utils.id('alt_attr'), + title_attr = utils.id('title_attr'), + save_button = utils.id('save_details'), + close_button = form.querySelector('.close_button'), + sections = form.querySelectorAll('p'), + obj, + x, + y, + temp_x, + temp_y; + + function changedReset() { + form.classList.remove('changed'); + utils.foreach(sections, function(x) { + x.classList.remove('changed'); + }); + } + + function save(e) { + obj.setInfoAttributes({ + href : href_attr.value, + alt : alt_attr.value, + title : title_attr.value, + }); + + obj[obj.href ? 'setStyleOfElementWithHref' : 'unsetStyleOfElementWithHref'](); + + changedReset(); + unload(); + + e.preventDefault(); + } + + function unload() { + obj = null; + changedReset(); + utils.hide(form); + } + + function setCoords(x, y) { + form.style.left = (x + 5) + 'px'; + form.style.top = (y + 5) + 'px'; + } + + function moveEditBlock(e) { + setCoords(x + e.pageX - temp_x, y + e.pageY - temp_y); + } + + function stopMoveEditBlock(e) { + x = x + e.pageX - temp_x; + y = y + e.pageY - temp_y; + setCoords(x, y); + + app.removeAllEvents(); + } + + function change() { + form.classList.add('changed'); + this.parentNode.classList.add('changed'); + } + + save_button.addEventListener('click', save, false); + + href_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); + alt_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); + title_attr.addEventListener('keydown', function(e) { e.stopPropagation(); }, false); + + href_attr.addEventListener('change', change, false); + alt_attr.addEventListener('change', change, false); + title_attr.addEventListener('change', change, false); + + close_button.addEventListener('click', unload, false); + + header.addEventListener('mousedown', function(e) { + temp_x = e.pageX, + temp_y = e.pageY; + + app.addEvent(document, 'mousemove', moveEditBlock); + app.addEvent(header, 'mouseup', stopMoveEditBlock); + + e.preventDefault(); + }, false); + + return { + load : function(object, new_x, new_y) { + obj = object; + href_attr.value = object.href ? object.href : ''; + alt_attr.value = object.alt ? object.alt : ''; + title_attr.value = object.title ? object.title : ''; + utils.show(form); + if (new_x && new_y) { + x = new_x; + y = new_y; + setCoords(x, y); + } + }, + unload : unload }; + })(); - for (var i = 0, c = this._coords.points.length; i < c; i++) { - this._helpers.points.push( - (new Helper( - this._groupEl, - this._coords.points[i].x, - this._coords.points[i].y, - 'movePoint') - ).setId(i) - ); + + /* Load areas from html code */ + var from_html_form = (function() { + var form = utils.id('from_html_wrapper'), + code_input = utils.id('code_input'), + load_button = utils.id('load_code_button'), + close_button = form.querySelector('.close_button'); + + function load(e) { + if (Area.createAreasFromHTMLOfMap(code_input.value)) { + hide(); + } + + e.preventDefault(); } - this._selectedPoint = -1; + function hide() { + utils.hide(form); + } - this.redraw(); - } - utils.inherits(Polygon, Area); - - /** - * Closes path of the polygon (replaces polyline with polygon) - */ - Polygon.prototype.close = function() { - var polyline = this._el; - this._el = document.createElementNS(Area.SVG_NS, 'polygon'); - this._groupEl.replaceChild(this._el, polyline); - - this._coords.isOpened = false; - this.redraw().deselect(); - }; - - /** - * Set attributes for svg-elements of area by new parameters - * - * @param coords {Object} - Object with coords of this area, with field 'points' - * @returns {Polygon} - this area - */ - Polygon.prototype.setSVGCoords = function(coords) { - var polygonPointsAttrValue = coords.points.reduce(function(previousValue, currentItem) { - return previousValue + currentItem.x + ' ' + currentItem.y + ' '; - }, ''); + load_button.addEventListener('click', load, false); - this._el.setAttribute('points', polygonPointsAttrValue); - utils.foreach(this._helpers.points, function(helper, i) { - helper.setCoords(coords.points[i].x, coords.points[i].y); - }); + close_button.addEventListener('click', hide, false); - return this; - }; - - /** - * Set coords for this area - * - * @param coords {coords} - * @returns {Polygon} - this area - */ - Polygon.prototype.setCoords = function(coords) { - this._coords.points = coords.points; - - return this; - }; - - /** - * Adds new point to polygon (and new helper too) - * - * @param x {number} - x-coordinate of new point - * @param y {number} - y-coordinate of new point - * @returns {Polygon} - this area - */ - Polygon.prototype.addPoint = function(x, y) { - if (!this._coords.isOpened) { - throw new Error('This polygon is closed!'); - } + return { + show : function() { + code_input.value = ''; + utils.show(form); + }, + hide : hide + }; + })(); - var helper = new Helper(this._groupEl, x, y, 'movePoint'); - helper.setId(this._helpers.points.length); - this._helpers.points.push(helper); - this._coords.points.push({ - x : x, - y : y - }); - this.redraw(); - - return this; - }; + /* Get image form */ + var get_image = (function() { + var block = utils.id('get_image_wrapper'), + close_button = block.querySelector('.close_button'), + loading_indicator = utils.id('loading'), + button = utils.id('button'), + filename = null, + last_changed = null; + + // Drag'n'drop - the first way to loading an image + var drag_n_drop = (function() { + var dropzone = utils.id('dropzone'), + dropzone_clear_button = dropzone.querySelector('.clear_button'), + sm_img = utils.id('sm_img'); + + function testFile(type) { + switch (type) { + case 'image/jpeg': + case 'image/gif': + case 'image/png': + return true; + } + return false; + } + + dropzone.addEventListener('dragover', function(e){ + utils.stopEvent(e); + }, false); + + dropzone.addEventListener('dragleave', function(e){ + utils.stopEvent(e); + }, false); - /** - * Calculates new coordinates in process of drawing - * - * @param x {number} - * @param y {number} - * @param isRightAngle {boolean} - * @returns {Object} - calculated coordinates - */ - Polygon.prototype.dynamicDraw = function(x, y, isRightAngle) { - var temp_coords = { - points: [].concat(this._coords.points) - }; + dropzone.addEventListener('drop', function(e){ + utils.stopEvent(e); - if (isRightAngle) { - var rightPointCoords = Polygon.getRightAngleLineLastPointCoords( - this._coords, {x: x, y: y} - ); - x = rightPointCoords.x; - y = rightPointCoords.y; - } - - temp_coords.points.push({x : x, y : y}); + var reader = new FileReader(), + file = e.dataTransfer.files[0]; + + if (testFile(file.type)) { + dropzone.classList.remove('error'); + + reader.readAsDataURL(file); + + reader.onload = function(e) { + sm_img.src = e.target.result; + sm_img.style.display = 'inline-block'; + filename = file.name; + utils.show(dropzone_clear_button); + last_changed = drag_n_drop; + }; + } else { + clearDropzone(); + dropzone.classList.add('error'); + } - this.redraw(temp_coords); + }, false); + + function clearDropzone() { + sm_img.src = ''; + + utils.hide(sm_img) + .hide(dropzone_clear_button); + + dropzone.classList.remove('error'); + + last_changed = url_input; + } + + dropzone_clear_button.addEventListener('click', clearDropzone, false); + + return { + clear : clearDropzone, + init : function() { + dropzone.draggable = true; + this.clear(); + utils.hide(sm_img) + .hide(dropzone_clear_button); + }, + test : function() { + return Boolean(sm_img.src); + }, + getImage : function() { + return sm_img.src; + } + }; + })(); - return temp_coords; - }; + /* Set a url - the second way to loading an image */ + var url_input = (function() { + var url = utils.id('url'), + url_clear_button = url.parentNode.querySelector('.clear_button'); + + function testUrl(str) { + var url_str = str.trim(), + temp_array = url_str.split('.'), + ext; - /** - * Handler for drawing process (by mousemove) - * It includes only redrawing area by new coords - * (this coordinates doesn't save as own area coords) - * - * @params e {MouseEvent} - mousemove event - */ - Polygon.prototype.onProcessDrawing = function(e) { - var coords = utils.getRightCoords(e.pageX, e.pageY); + if(temp_array.length > 1) { + ext = temp_array[temp_array.length-1].toLowerCase(); + switch (ext) { + case 'jpg': + case 'jpeg': + case 'gif': + case 'png': + return true; + } + } + + return false; + } + + function onUrlChange() { + setTimeout(function(){ + if(url.value.length) { + utils.show(url_clear_button); + last_changed = url_input; + } else { + utils.hide(url_clear_button); + last_changed = drag_n_drop; + } + }, 0); + } - this.dynamicDraw(coords.x, coords.y, e.shiftKey); - }; + url.addEventListener('keypress', onUrlChange, false); + url.addEventListener('change', onUrlChange, false); + url.addEventListener('paste', onUrlChange, false); + + function clearUrl() { + url.value = ''; + utils.hide(url_clear_button); + url.classList.remove('error'); + last_changed = url_input; + } + + url_clear_button.addEventListener('click', clearUrl, false); - /** - * Handler for polygon pointer adding (by click on drawing canvas) - * It includes redrawing area with this new point - * and saving this point to list of polygon points - * - * @params e {MouseEvent} - click event - */ - Polygon.prototype.onAddPointDrawing = function(e) { - var newPointCoords = utils.getRightCoords(e.pageX, e.pageY); - - if (e.shiftKey) { - newPointCoords = Polygon.getRightAngleLineLastPointCoords(this._coords, newPointCoords); + return { + clear : clearUrl, + init : function() { + this.clear(); + utils.hide(url_clear_button); + }, + test : function() { + if(testUrl(url.value)) { + url.classList.remove('error'); + return true; + } else { + url.classList.add('error'); + } + return false; + }, + getImage : function() { + var tmp_arr = url.value.split('/'); + filename = tmp_arr[tmp_arr.length - 1]; + + return url.value.trim(); + } + }; + })(); + + + /* Block init */ + function init() { + utils.hide(loading_indicator); + drag_n_drop.init(); + url_input.init(); } + init(); - this.addPoint(newPointCoords.x, newPointCoords.y); - }; - - /** - * Handler for drawing stoping (by click on first helper or press ENTER key) - * It includes redrawing area by new coords, closing this polygon - * and saving this coords as own area coords - * - * @params e {KeyboardEvent|MouseEvent} - click or keydown event - */ - Polygon.prototype.onStopDrawing = function(e) { - if (e.type == 'click' || (e.type == 'keydown' && e.keyCode == 13)) { - if (Polygon.testCoords(this._coords)) { - this.close(); - - app.removeAllEvents() - .setIsDraw(false) - .resetNewArea(); - } + /* Block clear */ + function clear() { + drag_n_drop.clear(); + url_input.clear(); + last_changed = null; } - e.stopPropagation(); - }; - - /** - * Changes area parameters by editing type and offsets - * - * @param {string} editingType - A type of editing - * @returns {Object} - Object with changed parameters of area - */ - Polygon.prototype.edit = function(editingType, dx, dy) { - var tempParams = Object.create(this._coords); - switch (editingType) { - case 'move': - for (var i = 0, c = tempParams.points.length; i < c; i++) { - tempParams.points[i].x += dx; - tempParams.points[i].y += dy; - } - break; + /* Selected image loading */ + function onButtonClick(e) { + if (last_changed === url_input && url_input.test()) { + app.loadImage(url_input.getImage()).setFilename(filename); + } else if (last_changed === drag_n_drop && drag_n_drop.test()) { + app.loadImage(drag_n_drop.getImage()).setFilename(filename); + } - case 'movePoint': - tempParams.points[this.selected_point].x += dx; - tempParams.points[this.selected_point].y += dy; - break; + e.preventDefault(); } - - return tempParams; - }; - - /** - * Handler for editing process (by mousemove) - * It includes only redrawing area by new coords - * (this coords doesn't save as area coords) - * - * @params e {MouseEvent} - mousemove event - */ - Polygon.prototype.onProcessEditing = function(e) { - var editType = app.getEditType(); - this.redraw( - this.edit( - editType, - e.pageX - this.editingStartPoint.x, - e.pageY - this.editingStartPoint.y - ) - ); - - this.editingStartPoint.x = e.pageX; - this.editingStartPoint.y = e.pageY; - }; - - /** - * Handler for editing stoping (by mouseup on drawing canvas) - * It includes redrawing area by new coords and saving this new coords - * as own area coords - * - * @params e {MouseEvent} - click or keydown event - */ - Polygon.prototype.onStopEditing = function(e) { - var editType = app.getEditType(); - - this.setCoords( - this.edit( - editType, - e.pageX - this.editingStartPoint.x, - e.pageY - this.editingStartPoint.y - ) - ).redraw(); - - app.removeAllEvents(); - }; - - /** - * Returns string-representation of polygon - * - * @returns {string} - */ - Polygon.prototype.toString = function() { - return 'Polygon {points: ['+ - this._coords.points.map(function(item) { - return '[' + item.x + ', ' + item.y + ']' - }).join(', ') + ']}'; - } - - /** - * Returns html-string of area html element with params of this polygon - * - * @returns {string} - */ - Polygon.prototype.toHTMLMapElementString = function() { - var str = this._coords.points.map(function(item) { - return item.x + ', ' + item.y; - }).join(', '); + button.addEventListener('click', onButtonClick, false); - return '' + this._attributes.alt + ''; - }; - - /** - * Returns coords for area attributes form - * - * @returns {Object} - coordinates of point - */ - Polygon.prototype.getCoordsForDisplayingInfo = function() { - return { - x : this._coords.points[0].x, - y : this._coords.points[0].y - }; - }; - - /** - * Returns true if coords is valid for polygons and false otherwise - * - * @static - * @param coords {Object} - object with coords for new polygon - * @returns {boolean} - */ - Polygon.testCoords = function(coords) { - return coords.points.length >= 3; - }; - - /** - * Returns true if html coords array is valid for polygons and false otherwise - * - * @static - * @param coords {Array} - coords for new polygon as array [x1, y1, x2, y2, x3, y3, ...] - * @returns {boolean} - */ - Polygon.testHTMLCoords = function(coords) { - return coords.length >= 6 && coords.length % 2 === 0; - }; - - /** - * Returns polygon coords object from html array - * - * @param htmlCoordsArray {Array} - * @returns {Object} - object with calculated points - */ - Polygon.getCoordsFromHTMLArray = function(htmlCoordsArray) { - if (!Polygon.testHTMLCoords(htmlCoordsArray)) { - throw new Error('This html-coordinates is not valid for polygon'); + close_button.addEventListener('click', hide, false); + + function show() { + clear(); + utils.show(block); + } + + function hide() { + utils.hide(block); } + + /* Returned object */ + return { + show : function() { + app.hide(); + show(); + + return this; + }, + hide : function() { + app.show(); + hide(); + + return this; + }, + showLoadIndicator : function() { + utils.show(loading_indicator); + + return this; + }, + hideLoadIndicator : function() { + utils.hide(loading_indicator); + + return this; + } + }; + })(); + get_image.show(); + - var points = []; - for (var i = 0, c = htmlCoordsArray.length/2; i < c; i++) { - points.push({ - x : htmlCoordsArray[2*i], - y : htmlCoordsArray[2*i + 1] + /* Buttons and actions */ + var buttons = (function() { + var all = utils.id('nav').getElementsByTagName('li'), + save = utils.id('save'), + load = utils.id('load'), + rectangle = utils.id('rectangle'), + circle = utils.id('circle'), + polygon = utils.id('polygon'), + edit = utils.id('edit'), + clear = utils.id('clear'), + from_html = utils.id('from_html'), + to_html = utils.id('to_html'), + preview = utils.id('preview'), + new_image = utils.id('new_image'), + show_help = utils.id('show_help'); + + function deselectAll() { + utils.foreach(all, function(x) { + x.classList.remove(Area.CLASS_NAMES.SELECTED); }); } - - return { - points : points - }; - }; - - /** - * Returns coords of new point with right angle (or 45 deg) for last line - * This method recalculates coordinates of new point for - * last line with (0 | 45 | 90 | 135 | 180 | 225 | 270 | 315) deg - * For example, - * for (0 < deg < 23) -> 0 deg - * for (23 < deg < 67) -> 45 deg - * for (67 < deg < 90) -> 90 deg etc. - * - * 0 - * 315\ | /45 - * \|/ - * 270 --------- 90 - * /|\ - * 225/ | \135 - * 180 - * - * @static - * @param originalCoords {Object} - Coordinates of this area (without new point) - * @param newPointCoords {Object} - Coordinates of new point - * @returns {Object} - Right coordinates for last point - */ - Polygon.getRightAngleLineLastPointCoords = function(originalCoords, newPointCoords) { - var TANGENS = { - DEG_22 : 0.414, - DEG_67 : 2.414 - }, - lastPointIndex = originalCoords.points.length - 1, - lastPoint = originalCoords.points[lastPointIndex], - dx = newPointCoords.x - lastPoint.x, - dy = -(newPointCoords.y - lastPoint.y), - tan = dy / dx, - x = newPointCoords.x, - y = newPointCoords.y; + + function selectOne(button) { + deselectAll(); + button.classList.add(Area.CLASS_NAMES.SELECTED); + } + + function onSaveButtonClick(e) { + // Save in localStorage + app.saveInLocalStorage(); - if (dx > 0 && dy > 0) { - if (tan > TANGENS.DEG_67) { - x = lastPoint.x; - } else if (tan < TANGENS.DEG_22) { - y = lastPoint.y; - } else { - Math.abs(dx) > Math.abs(dy) ? - (x = lastPoint.x + dy) : (y = lastPoint.y - dx); - } - } else if (dx < 0 && dy > 0) { - if (tan < -TANGENS.DEG_67) { - x = lastPoint.x; - } else if (tan > -TANGENS.DEG_22) { - y = lastPoint.y; - } else { - Math.abs(dx) > Math.abs(dy) ? - (x = lastPoint.x - dy) : (y = lastPoint.y + dx); + e.preventDefault(); + } + + function onLoadButtonClick(e) { + // Load from localStorage + app.clear() + .loadFromLocalStorage(); + + e.preventDefault(); + } + + function onShapeButtonClick(e) { + // shape = rect || circle || polygon + app.setMode('drawing') + .setDrawClass() + .setShape(this.id) + .deselectAll() + .hidePreview(); + info.unload(); + selectOne(this); + + e.preventDefault(); + } + + function onClearButtonClick(e) { + // Clear all + if (confirm('Clear all?')) { + app.setMode(null) + .setDefaultClass() + .setShape(null) + .clear() + .hidePreview(); + deselectAll(); } - } else if (dx < 0 && dy < 0) { - if (tan > TANGENS.DEG_67) { - x = lastPoint.x; - } else if (tan < TANGENS.DEG_22) { - y = lastPoint.y; + + e.preventDefault(); + } + + function onFromHtmlButtonClick(e) { + // Load areas from html + from_html_form.show(); + + e.preventDefault(); + } + + function onToHtmlButtonClick(e) { + // Generate html code only + info.unload(); + code.print(); + + e.preventDefault(); + } + + function onPreviewButtonClick(e) { + if (app.getMode() === 'preview') { + app.setMode(null) + .hidePreview(); + deselectAll(); } else { - Math.abs(dx) > Math.abs(dy) ? - (x = lastPoint.x + dy) : (y = lastPoint.y - dx); + app.deselectAll() + .setMode('preview') + .setDefaultClass() + .preview(); + selectOne(this); } - } else if (dx > 0 && dy < 0) { - if (tan < -TANGENS.DEG_67) { - x = lastPoint.x; - } else if (tan > -TANGENS.DEG_22) { - y = lastPoint.y; + + e.preventDefault(); + } + + function onEditButtonClick(e) { + if (app.getMode() === 'editing') { + app.setMode(null) + .setDefaultClass() + .deselectAll(); + deselectAll(); + utils.show(domElements.svg); } else { - Math.abs(dx) > Math.abs(dy) ? - (x = lastPoint.x - dy) : (y = lastPoint.y + dx); + app.setShape(null) + .setMode('editing') + .setEditClass(); + selectOne(this); } + app.hidePreview(); + e.preventDefault(); } - return { - x : x, - y : y - }; - }; - - /** - * Creates new polygon and add drawing handlers for DOM-elements - * - * @static - * @param firstPointCoords {Object} - coords for first point of polyline - * @returns {Polygon} - created polygon - */ - Polygon.createAndStartDrawing = function(firstPointCoords) { - var newArea = new Polygon({ - points : [firstPointCoords], - isOpened : true - }); + function onNewImageButtonClick(e) { + // New image - clear all and back to loading image screen + if(confirm('Discard all changes?')) { + app.setMode(null) + .setDefaultClass() + .setShape(null) + .setIsDraw(false) + .clear() + .hide() + .hidePreview(); + deselectAll(); + get_image.show(); + } + + e.preventDefault(); + } - app.addEvent(app.domElements.container, 'mousemove', newArea.onProcessDrawing.bind(newArea)) - .addEvent(app.domElements.container, 'click', newArea.onAddPointDrawing.bind(newArea)) - .addEvent(document, 'keydown', newArea.onStopDrawing.bind(newArea)) - .addEvent(newArea._helpers.points[0]._el, 'click', newArea.onStopDrawing.bind(newArea)); - - return newArea; - }; + function onShowHelpButtonClick(e) { + help.show(); + + e.preventDefault(); + } + + save.addEventListener('click', onSaveButtonClick, false); + load.addEventListener('click', onLoadButtonClick, false); + rectangle.addEventListener('click', onShapeButtonClick, false); + circle.addEventListener('click', onShapeButtonClick, false); + polygon.addEventListener('click', onShapeButtonClick, false); + clear.addEventListener('click', onClearButtonClick, false); + from_html.addEventListener('click', onFromHtmlButtonClick, false); + to_html.addEventListener('click', onToHtmlButtonClick, false); + preview.addEventListener('click', onPreviewButtonClick, false); + edit.addEventListener('click', onEditButtonClick, false); + new_image.addEventListener('click', onNewImageButtonClick, false); + show_help.addEventListener('click', onShowHelpButtonClick, false); + })(); })();