// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }()); ;(function(){ function SVG(el, clazz){ var e = document.createElementNS("http://www.w3.org/2000/svg", el); if (clazz) e.setAttribute('class', clazz); return e; } PhotoTopo.prototype.palettes = [ { id: "types", buttons: [ "select", "route", "area" ] } ]; /* * Do a full recalc and render */ PhotoTopo.prototype.rerender = function(){ this.cache = this.calculate(this.model); this.calcPaths(); this.render(true); this.types[this.editmode].render(); } PhotoTopo.prototype.toJSON = function(){ var m = this.model; var objects = []; function areaToJSON(area) { var d = ''; var l = area.label; if (l){ d += l.x + ' ' + l.y; d += ' '; d += l.halign; d += l.valign; d += l.visible; d += l.line; d += l.expand; d += ' '; d += encodeURIComponent(l.text); for(var c=0; c < area.points.length; c++){ var p = area.points[c]; d += ',' + p.x + ' ' + p.y; } } objects.push({ id: a, store: d, order: area.orig.order }); } for(var a in m.areas){ areaToJSON(m.areas[a]); } for(var a in m.annotations){ areaToJSON(m.annotations[a]); } for(var r in m.routes){ var route = m.routes[r]; var d = ''; for(var c=0; c < route.points.length; c++){ var p = route.points[c]; if (c != 0){ d += ','; } d += Math.round(p.x * 10) / 10 + ' ' + + Math.round(p.y * 10) / 10; if (p.type){ d += ' ' + p.type; } if (!p.visible){ d += ' hidden'; } } objects.push({ id: r, store: d, order: route.orig.order }); } // do we only send unlink'ed items now? do we do a diff of last saved to current state? return objects; } /* * Setup edit mode for a topo, create extra layers * add hooks to intercept normal view code */ PhotoTopo.prototype.palette = function(label, contents){ this.attributes .empty() .append('
') .append('
Click on a shape to edit it, or select an undrawn route/area from the table below') ; $attr.find('.js-showhide').on('click',function(e){ var $i = $(this).find('i') var show = $i.hasClass('icon-eye-open'); if (show){ topo.hide() $i.attr('class', 'icon-eye-close'); } else { topo.show() $i.attr('class', 'icon-eye-open'); } e.preventDefault(); e.stopPropagation(); }); this.topo.palette('Topo attributes', $attr); // .append('Choose the topo zoom scale') TODO ; } PhotoTopoSelect.prototype.toggle = function(nodeId){ // If we click on something else just swap mode that that object this.topo.focus('select', nodeId); } /******************************************************************** * A draggable control handle */ function ControlHandle(topo, id, pos, x, y){ this.topo = topo; this.id = id; this.pos = pos; this.x = x; this.y = y; this.ox = x; // also store pre-drag x / y this.oy = y; var sc = topo.options.viewScale; // has dragged? if not dragged then remove it this.dragged = false; if (!topo.$handles){ topo.$handles = $(SVG('g', 'handles')); topo.$svg.append(topo.$handles); } var handle = SVG('circle', 'handle'); this.$handle = $(handle); this.$handle.attr({ cx: this.x * sc, cy: this.y * sc, r: this.pos == -1 ? 7 : 5 }); // If a label control, make sure it's last so it's on top if (this.pos == -1){ topo.$handles.append(this.$handle); } else { topo.$handles.prepend(this.$handle); } var isAnimating = false; this.dragStart = function(e){ // If any mouse button other than primary was used, ignore it if (e.button){ return; } e.preventDefault(); if(e.touches && e.touches.length > 1) { return; // multi-touch - zoom? TODO cancel drag and zoom } // Add class so we get nicer cursors this.topo.$element.addClass('cursor-dragging'); if (this.pos != -1){ var tool = this.topo.types[this.topo.editmode]; tool.setFocusPoint(this.pos + 1); } var xy = this.topo.getEventPos(e); this.sx = xy.x; this.sy = xy.y; if (window.navigator.msPointerEnabled) { document.addEventListener('MSPointerMove', this.dragMove, {capture: true, passive:false}); document.addEventListener('MSPointerUp', this.dragStop, {capture: true, passive:false}); } else { document.addEventListener('touchmove', this.dragMove, {capture: true, passive:false}); document.addEventListener('mousemove', this.dragMove, {capture: true, passive:false}); document.addEventListener('mouseup', this.dragStop, {capture: true, passive:false}); document.addEventListener('touchend', this.dragStop, {capture: true, passive:false}); document.addEventListener('touchcancel', this.dragStop, {capture: true, passive:false}); } }.bind(this); this.updateHandle = function(){ this.$handle.attr({ cx: this.x * sc, cy: this.y * sc, r: this.pos == -1 ? 7 : 5 }); this.topo.rerender(); isAnimating = false; }.bind(this); this.dragMove = function(e){ var xy = this.topo.getEventPos(e); var dx = xy.x - this.sx; var dy = xy.y - this.sy; this.x = dx + this.ox; this.y = dy + this.oy; var w = this.topo.options.width / sc; var h = this.topo.options.height / sc; var tool = this.topo.types[this.topo.editmode]; if (!e.ctrlKey ){ tool.snap(this); } this.topo.constrain(this); // If we have ever moved, with or without snapping then reset the old focus if (this.ox != this.x || this.oy != this.y){ this.dragged = true; this.topo.hasChanged(); } var foc = this.topo.model[topo.editmode+'s'][this.id]; var model = this.pos == -1 ? foc.label : foc.points[this.pos]; model.x = this.x; model.y = this.y; if (!isAnimating){ isAnimating = true; window.requestAnimationFrame(this.updateHandle); } }.bind(this); this.dragStop = function(e){ if (window.navigator.msPointerEnabled) { document.removeEventListener('MSPointerMove', this.dragMove, true); document.removeEventListener('MSPointerUp', this.dragStop, true); } else { document.removeEventListener('touchmove', this.dragMove, true); document.removeEventListener('mousemove', this.dragMove, true); document.removeEventListener('mouseup', this.dragStop, true); document.removeEventListener('touchend', this.dragStop, true); document.removeEventListener('touchcancel', this.dragStop, true); } var tool = this.topo.types[this.topo.editmode]; // If we haven't moved, and we were previously the focused point then delete it AND we are a point handle if (!this.dragged && tool.ofocusPoint == tool.focusPoint && this.pos != -1){ this.deletePoint(); } else { this.ox = this.x; this.oy = this.y; } this.dragged = false; this.topo.$element.removeClass('cursor-dragging'); }.bind(this); // Setup the events on the handle topo.evtStart(handle, this.dragStart); handle.addEventListener("contextmenu", function (e) { e.preventDefault(); e.stopPropagation(); this.openContextMenu(e); }.bind(this), true); } ControlHandle.prototype.deletePoint = function(){ var tool = this.topo.types[this.topo.editmode]; var points = tool.topo.model[this.topo.editmode+'s'][tool.focusId].points; // delete point from model points.splice(tool.focusPoint-1,1); // remove control handle from list var del = tool.handles.splice(tool.focusPoint-1,1); // delete control handle from dom del[0].$handle.remove(); // Now fix up point ref's in all 'up' points for (var p=0; p< points.length; p++){ tool.handles[p].pos = p; } tool.setFocusPoint(this.pos); this.topo.rerender(); this.topo.hasChanged(); } ControlHandle.prototype.openContextMenu = function(e){ function getLeftLocation(e) { var mouseWidth = e.pageX; var pageWidth = $(window).width(); var menuWidth = $menu.width(); // opening menu would pass the side of the page if (mouseWidth + menuWidth > pageWidth && menuWidth < mouseWidth) { return mouseWidth - menuWidth; } return mouseWidth; } function getTopLocation(e) { var mouseHeight = e.pageY; var pageHeight = $(window).height() + $(window).scrollTop(); var menuHeight = $menu.height(); // opening menu would pass the bottom of the page if (mouseHeight + menuHeight > pageHeight && mouseHeight > menuHeight) { return mouseHeight - menuHeight; } return mouseHeight; } if (this.$contextMenu){ this.$contextMenu.remove(); } var tool = this.topo.types[this.topo.editmode]; var menu = tool.getContextMenu(this.pos); if (!menu) return; var menu = '
')
.append('Segment visibility: ')
.append('
')
.append('')
.append('' )
.append('
')
.append('Point type: ')
.append('
')
.append(opt)
.append('
')
.append("
Warning: Some smaller point types are obscured while the route is selected. Deselect the route (or press escape) to preview")
;
$attributes.find('.js-deselect').on('click',function(e){
that.topo.focus('select');
});
$attributes.find('option[value='+type+']').attr('selected', 'selected');
$attributes.find('select').change(function(e){
var type = $(this).find(':selected').val();
that.focusRoute.points[that.focusPoint-1].type = type;
that.topo.hasChanged();
that.topo.rerender();
});
var that = this;
$attributes.find(':radio').click(function(e){
that.focusRoute.points[that.focusPoint-1].visible = this.value != 'hidden';
that.topo.hasChanged();
that.topo.rerender();
});
this.topo.$element.removeClass('cursor-first-point');
} else {
this.topo.$element.addClass('cursor-first-point');
$attributes
.append('Click anywhere to start drawing your route');
}
this.topo.palette('Route attributes' ,$attributes);
}
/*
*
*/
PhotoTopoRoute.prototype.insertPoints = function(position, insert, e){
var topo = this.topo;
topo.hasChanged();
var points = topo.model[topo.editmode+'s'][this.focusId].points;
// For eachy point inserted, add a new control handle
for (var p=0; p< insert.length; p++){
var point = insert[p];
points.splice(position + p, 0, point);
this.handles.splice(position + p, 0, new ControlHandle( topo, this.focusId, p, point.x, point.y));
}
// Now fix up point ref's in all 'up' points
for (var p=0; p< points.length; p++){
this.handles[p].pos = p;
}
// now start the drag on the point we last made
this.handles[position+insert.length-1].dragStart(e);
}
PhotoTopoRoute.prototype.insertAt = function(seg,e){
var pos = this.topo.getEventPos(e);
var prev = this.focusRoute.points[seg];
var insert = [{
x: pos.x,
y: pos.y,
visible: prev ? prev.visible : true,
type: ''
}];
this.insertPoints(seg+1, insert, e);
this.topo.rerender();
}
/*
* Add's a point from anywhere
*/
PhotoTopoRoute.prototype.addFromBG = function(e){
var seg = this.focusPoint -1;
this.insertAt(seg,e);
}
/*
* Splits a segment, adds a new control handle
*/
PhotoTopoRoute.prototype.splitSegment = function(e){
var seg = e.target.getAttribute('seg') * 1;
this.insertAt(seg,e);
}
/*
* Updates the data model, and adds a new control handle
*/
PhotoTopoRoute.prototype.insertFromPointGroup = function(e){
var pointGroupId = e.target.getAttribute('pid');
// Does the point we clicked on share a route?
// If so is this point further along the other route than this point?
// If so then instead of adding just this one point, we add a whole sub section
var pointGroup = this.topo.cache.pointGroups[pointGroupId];
// inserts a point into the currently selected route
// either after the selected point, or if none at the end
// What route are be cloning point(s) from?
var insert;
var i;
if (this.focusRoute.points.length > 0){
var curPoint = this.focusRoute.points[this.focusPoint-1];
var pKey = curPoint.x + '_' + curPoint.y;
var currentPointGroup = this.topo.cache.pointGroups[pKey];
// Does the new point, and the current point share any routes?
for(i in pointGroup.routes) {
if(i in currentPointGroup.routes && currentPointGroup.routes[i] < pointGroup.routes[i]) {
insert = this.topo.cache.routes[i].points.slice(currentPointGroup.routes[i], pointGroup.routes[i]);
break;
}
}
}
if (!insert){
// If we got this far, and i isn't a route id then just pick the first one to clone
if (!i){
i = Object.keys( pointGroup.routes)[0];
}
var clone = this.topo.cache.routes[i].points[pointGroup.routes[i]-1]
insert = [{
x: pointGroup.x,
y: pointGroup.y,
visible: clone.visible,
type: clone.type
}];
}
this.insertPoints(this.focusPoint, insert, e);
this.topo.rerender();
}
PhotoTopoRoute.prototype.drawEdit = function(){
// extra is the point position, is always set
// If calc is dirty redo it?
var that = this;
var topo = this.topo;
var cache = topo.cache;
var sc = topo.options.viewScale;
// Make background event intercepter
// - if click add new point after selected point
var $bge = $(SVG('rect', 'bge'));
$bge.attr({
x: 0,
y: 0,
width: '100%',
height: '100%'
});
function add(e){
that.addFromBG(e);
};
topo.evtStart($bge[0], add);
topo.$bge = $bge.appendTo(topo.$svg);
// Make a svg group for each segment which can be split
var $segments = $(SVG('g', 'segments')); // These are the points on 'other' routes
var segs = cache.routes[that.focusId].pathSegments;
for(var s=0; s ')
.append('
'
+''
+'
'
+'
'
+ '
'
+' '
// +''
+ '