1 /*!
  2  * PhotoTopo @VERSION - A javascript climbing route editing widget
  3  *
  4  * Copyright (c) 2010 Brendan Heywood
  5  *
  6  * http://github.com/brendanheywood/PhotoTopo/
  7  *
  8  * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
  9  */
 10 
 11 
 12 /*
 13 
 14 TODO
 15 
 16 BUG: If a single route ends where another single route starts they don't join properly
 17 
 18 BUG: Chrome sometimes draws a black route - possibly the getLbel selector is wrong for some rows (perhaps an empty cell?)
 19 
 20 
 21 Optimise: Defer redrawing of new routes until all are loaded?
 22 
 23 
 24 */
 25 "use strict";
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 /**
 34  * @private
 35  * stores the collection of points that are in the same location
 36  * @constructor 
 37  * @param {Point} point The first point in this group
 38  */
 39 function PointGroup(point){
 40 	this.points = [];
 41 	this.points[0] = point;
 42 	
 43 	// these coords may get changed imediately after creation
 44 	this.x = point.x;
 45 	this.y = point.y;
 46 }
 47 
 48 /**
 49  * add another point to an existing pointGroup
 50  * @param {Point} point the point to add
 51  */
 52 PointGroup.prototype.add = function(point){
 53 	var c;
 54 
 55 	this.points[this.points.length] = point;
 56 	this.sort();
 57 	if (this.points[0].route.phototopo.loading){ 
 58 		return;
 59 	}
 60 	for (c=0; c < this.points.length; c++){
 61 		this.points[c].updateLabelPosition();
 62 	} 
 63 };
 64 PointGroup.prototype.sort = function(){
 65 	this.points.sort(function(a,b){
 66 		return a.route.order > b.route.order ? 1 : -1;
 67 	});
 68 
 69 };
 70 
 71 
 72 /**
 73  * gets the points order in this group
 74  * points have a natural order which is typically the order the routes are shown in the guide (eg left to right)
 75  */
 76 PointGroup.prototype.getLabelPosition = function(point){
 77 	var c,
 78 		pos = 0;
 79 
 80 	for(c=0; c < this.points.length; c++){
 81 		// only count points that have a visible label
 82 		if(this.points[c].labelEl){ pos++; }
 83 		if (this.points[c] === point){
 84 			return c;
 85 		}
 86 	}
 87 };
 88 
 89 /*
 90  * return the amount the curve should be offset when multiple curves overlap
 91  */
 92 PointGroup.prototype.getSplitOffset = function(point){
 93 	var c, ret;
 94 
 95 	for(c=0; c < this.points.length; c++){
 96 		if (this.points[c] === point){
 97 			ret = (1 - this.points.length) / 2 + c;
 98 			return ret;
 99 		}
100 	}
101 	return 0;
102 };
103 
104 
105 /*
106  * the point has moved to redraw all connected paths
107  */
108 PointGroup.prototype.redraw = function(point){
109 	var c,p;
110 	for(c=0; c < this.points.length; c++){
111 		p = this.points[c];
112 		if (p.nextPath){
113 			p.nextPath.redraw();
114 		}
115 		if (p.prevPath){
116 			p.prevPath.redraw();
117 		}
118 	}
119 };
120 
121 /*
122  * 
123  */
124 PointGroup.prototype.remove = function(point){
125 	var c, pt;
126 	for(c=this.points.length-1; c>=0; c--){
127 		if (this.points[c] === point){
128 			this.points.splice(c,1);
129 		}
130 	}
131 	if (this.points.length === 0){
132 		pt = point.route.phototopo;
133 		delete pt.pointGroups[pt.getKey(point)];
134 	}
135 	for (c=0; c<this.points.length; c++){
136 		this.points[c].updateLabelPosition();
137 	}
138 };
139 
140 
141 
142 /*
143  * returns the x/y coords of the cubic bezier curve that should come out of this point
144  * just make them negative to get the handle for the opposite end of the cubic curve
145  * a simplistic algortith just averages them all, a more complex one takes into account which routes actually merge or don't
146  */
147 PointGroup.prototype.getAngle = function(point){
148 
149 	// for each point get the diff of it and the next point and the previous point and add them all together
150 	// then find that angle and scale the point to the shortest path segment length
151 	
152 	var dx = 0,
153 		dy = 0,
154 		ddx = 0,
155 		ddy = 0,
156 		p,c,
157 		sqr,
158 		minSqr = 1000000,
159 		angle,
160 		dist;
161 	
162 	for(c=0; c < this.points.length; c++){
163 		p = this.points[c];
164 		if (p.nextPoint){
165 			dx = (p.nextPoint.x - p.x);
166 			dy = (p.nextPoint.y - p.y);
167 			sqr = dx*dx + dy*dy;
168 			if (sqr < minSqr){ minSqr = sqr; }
169 			ddx += dx;
170 			ddy += dy;
171 		}
172 		if (p.prevPoint){
173 			dx = (p.prevPoint.x - p.x);
174 			dy = (p.prevPoint.y - p.y);
175 			sqr = dx*dx + dy*dy;
176 			if (sqr < minSqr){ minSqr = sqr; }
177 			ddx -= dx;
178 			ddy -= dy;
179 		}
180 	}
181 	
182 	angle = Math.atan2(ddx, ddy);
183 	dist = Math.sqrt(minSqr) * 0.4;
184 	
185 	ddx = dist * Math.sin(angle);
186 	ddy = dist * Math.cos(angle);
187 		
188 	return {dx:ddx,dy:ddy};
189 };
190 
191 
192 
193 
194 /**
195  * @private
196  * there is one point for every point on a route - two points can occupy the same location
197  * @constructor 
198  * @param {Route} 
199  * @param {Integer} x x cordinate
200  * @param {Integer} y y coordinate
201  * @param {Integer} position where along the route it is added
202  */
203 function Point(route, x, y, type, position){
204 
205 	var styles,
206 		circle,
207 		point = this;
208 	this.route = route;
209 	this.x = x*1;
210 	this.y = y*1;
211 	this.type = type;
212 	this.position = position;
213 	
214 	this.labelEl = null;
215 	this.iconEl = null;
216 	
217 	
218 	this.nextPoint = null;
219 	this.prevPoint = null;
220 	this.nextPath = null;
221 	this.prevPath = null;
222 	
223 	this.pointGroup = this.route.phototopo.getPointGroup(this);
224 	
225 	
226 	styles = this.route.phototopo.styles;
227 
228 	if (this.route.phototopo.options.editable){
229 
230 
231 		this.circle = this.route.phototopo.canvas.circle(this.x, this.y, 5);
232 		circle = this.circle;
233 		circle.point = this;
234 		circle.attr(styles.handle);
235 		if (this.route.autoColor){
236 			circle.attr('fill', this.route.autoColor);
237 			circle.attr('stroke', this.route.autoColorBorder);
238 		}
239 
240 		function dragStart(){
241 			var selectedRoute = this.point.route.phototopo.selectedRoute;
242 			$(this.point.route.phototopo.photoEl).addClass('dragging');
243 
244 			this.ox = this.attr("cx");
245 			this.oy = this.attr("cy");
246 			
247 			// don't allow draging of points if another route is selected
248 			if (selectedRoute && selectedRoute !== this.point.route){
249 				return;
250 			}
251 			circle.animate(styles.handleHover, 100);
252 			this.point.select();
253 	
254 		}
255 		function dragMove(dx, dy){
256 			var selectedRoute = this.point.route.phototopo.selectedRoute, pos;
257 			if (selectedRoute && selectedRoute !== this.point.route){
258 				return;
259 			}
260 			pos = circle.point.moveTo(this.ox + dx, this.oy + dy);
261 			circle.attr({cx: pos.x, cy: pos.y});
262 		}
263 		function dragEnd(){
264 			var selectedRoute = this.point.route.phototopo.selectedRoute;
265 			$(this.point.route.phototopo.photoEl).removeClass('dragging');
266 
267 			if (selectedRoute && selectedRoute !== this.point.route){
268 				return;
269 			}
270 			this.point.setStyle();
271 		}
272 		
273 		circle.drag(dragMove, dragStart, dragEnd); 
274 		circle.mouseover(function(){
275 			// is it the last point of a routes?
276 			if (this.point.nextPoint){
277 				$('#phototopoContextMenu .hidden').removeClass('disabled');
278 				$('#phototopoContextMenu .jumpoff').addClass('disabled');
279 			} else {
280 				$('#phototopoContextMenu .hidden').addClass('disabled');
281 				$('#phototopoContextMenu .jumpoff').removeClass('disabled');
282 			}
283 
284 			$(this.point.route.phototopo.photoEl).addClass('point');
285 
286 			//this.point.setStyle();
287 			this.point.route.onmouseover(this.point);
288 			this.animate(styles.handleHover, 100);
289 		});
290 		circle.mouseout(function(){
291 			$(this.point.route.phototopo.photoEl).removeClass('point');
292 			this.point.route.onmouseout(this.point);
293 			this.point.setStyle();
294 		});
295 		circle.click(function(e){
296 			var route = this.point.route.phototopo.selectedRoute;
297 			// don't allow adding a point in the current route back to itself
298 			if (this.point.route === route){
299 				return;
300 			}
301 			if (route){
302 				route.addAfter(this.point.route.phototopo.selectedPoint, this.point.x, this.point.y);
303 			} else {
304 				this.point.select();
305 			}
306 		});
307 		circle.dblclick(function(){
308 			this.point.remove();
309 		});
310 	
311 	
312 		$(circle.node).contextMenu({
313 			menu: 'phototopoContextMenu'
314 		},
315 			function(action, el, pos) {
316 				point.setType(action);
317 				point.pointGroup.redraw();
318 				point.route.phototopo.saveData();
319 
320 		});
321 		
322 		
323 	}
324 	this.setType(this.type);
325 }
326 /**
327  * @private
328  */
329 Point.prototype.setType = function(type){
330 	if (!this.route.phototopo.options.showPointTypes){
331 		return;
332 	}
333 	if (!type || type === 'none'){
334 		this.type = '';
335 		if (this.iconEl){
336 			this.iconEl.className = 'pt_label pt_icon none';
337 			this.updateIconPosition();
338 		}
339 		return;
340 	}
341 	this.type = type;
342 	var div = this.iconEl;
343 	if (!this.iconEl){
344 		this.iconEl = document.createElement("div");
345 		this.iconEl.point = this;
346 		this.route.phototopo.labelsEl.appendChild(this.iconEl);
347 		div = this.iconEl;
348 		$(div).hover(
349 		function(event){
350 			div.point.route.onmouseover();
351 		},
352 		function(event){
353 			div.point.route.onmouseout();
354 		}
355 		);
356 		div.onclick = function(event){
357 			this.point.select(); // should this only be in edit mode?
358 		};
359 	}
360 	this.iconEl.className = 'pt_label pt_icon ' + this.type;
361 	this.updateIconPosition();
362 
363 };
364 /**
365  * @private
366  */
367 Point.prototype.updateIconPosition = function(){
368 	var div = this.iconEl,
369 		offsetX = this.route.phototopo.options.editable ? 8 : -8,
370 		offsetY = -8,
371 		top, left;
372 	if (!div){ return; }
373 	left = this.x + offsetX;
374 	top  = this.y + offsetY;
375 	div.style.left = left + 'px';
376 	div.style.top = top + 'px';
377 };
378 
379 /**
380  * @private
381  */
382 Point.prototype.setLabel = function(classes, text){
383 	var div = this.labelEl;
384 	if (!this.labelEl){
385 		this.labelEl = document.createElement("div");
386 		this.labelEl.point = this;
387 		this.route.phototopo.labelsEl.appendChild(this.labelEl);
388 		div = this.labelEl;
389 		$(div).hover(
390 		function(event){
391 			div.point.route.onmouseover();
392 		},
393 		function(event){
394 			div.point.route.onmouseout();
395 		}
396 		);
397 		div.onclick = function(event){
398 			this.point.select(); // should this only be in edit mode?
399 		};
400 		this.labelEl.className = 'pt_label '+classes;
401 		if (!this.route.phototopo.loading){ 
402 			this.updateLabelPosition();
403 		}
404 	}
405 	this.labelEl.className = 'pt_label '+classes;
406 	div.innerHTML = text;
407 
408 };
409 
410 
411 
412 /**
413  * @private
414  * remove a point from its route
415  */
416 Point.prototype.remove = function(){
417 
418 	var p = this,
419 		path = null,
420 		r, c;
421 	p.remove = 'todo';
422 
423 	// remove handle from raphael
424 	p.circle.remove();
425 
426 	// remove from point group
427 	p.pointGroup.remove(p);
428 	p.pointGroup.redraw();
429 	p.pointGroup = null;
430 
431 	// remove all handlers for the point and refs to/from dom
432 	r = this.route;
433 	
434 	// remove point from array
435 	r.points.splice(p.position, 1);
436 
437 	// fix all position's of points after this one
438 	for(c=p.position; c<r.points.length; c++){
439 		r.points[c].position = c;
440 	}
441 
442 	// if one path then remove and relink
443 	if (p.prevPath){
444 		// remove prev path and relink
445 		path = p.prevPath;
446 		path.remove = 'now!';
447 
448 		// fix point refs
449 		p.prevPoint.nextPoint = p.nextPoint;
450 		p.prevPoint.nextPath = p.nextPath;
451 
452 		// fix path points
453 		if (p.nextPoint){	
454 			p.nextPoint.prevPoint = p.prevPoint;
455 		}
456 		if (p.nextPath){	
457 			p.nextPath.point1 = p.prevPoint;
458 		}
459 
460 		r.paths.splice(p.position-1, 1);
461 		path.curve.remove();
462 		path.outline.remove();
463 		path.ghost.remove();
464 
465 		// select prev point
466 		p.prevPoint.select();
467 	} else if (p.nextPath){
468 		// it is the first
469 		// if the first point then move the label to the next point
470 		// select next point
471 		path = p.nextPath;
472 		path.remove = 'now2!';
473 		
474 		p.nextPoint.prevPoint = null;
475 		p.nextPoint.prevPath = null;
476 		
477 		r.paths.splice(p.position, 1);
478 		path.curve.remove();
479 		path.outline.remove();
480 		path.ghost.remove();
481 
482 		// select next point
483 		p.nextPoint.select();
484 	
485 	} else {
486 		
487 		// just one point so delete it
488 	}
489 	if (this.labelEl){
490 		this.labelEl.parentNode.removeChild(this.labelEl);
491 		this.labelEl = null;
492 	}
493 	if (this.iconEl){
494 		this.iconEl.parentNode.removeChild(this.iconEl);
495 		this.iconEl = null;
496 	}
497 
498 	if (this.route.phototopo.selectedPoint === this){
499 		this.route.phototopo.selectedPoint = null;
500 	}
501 
502 	p.route.redraw();
503 	if (p.nextPoint){
504 		p.nextPoint.pointGroup.redraw();
505 	}
506 	if (p.prevPoint){
507 		p.prevPoint.pointGroup.redraw();
508 	}
509 	this.route.phototopo.saveData();
510 	$(this.route.phototopo.photoEl).removeClass('point');
511 	this.route.phototopo.updateCursor();
512 
513 };
514 
515 /**
516  * @private
517  * updates the labels position
518  */
519 Point.prototype.updateLabelPosition = function(){
520 	
521 	var div = this.labelEl,
522 		offsetX, offsetY,
523 		width, top, left,
524 		maxTop,
525 		phototopo = this.route.phototopo;
526 	if (!div){ return; }
527 	
528 	offsetX = this.pointGroup.getSplitOffset(this) * div.offsetWidth;
529 	width = (div.offsetWidth) / 2;
530 	offsetY = phototopo.options.thickness;	
531 
532 	left = this.x - width + offsetX;
533 	top  = this.y + offsetY;
534 	
535 	maxTop = phototopo.shownHeight - div.offsetHeight;
536 	if (top>maxTop){ top = this.y - offsetY - div.offsetHeight; }
537 	
538 	div.style.left = left + 'px';
539 	div.style.top = top + 'px';
540 };
541 
542 /**
543  * @private
544  */
545 Point.prototype.setStyle = function(){
546 	var styles = this.route.phototopo.styles;
547 	if (this.circle){
548 		// if the active point			  
549 		if (this === this.route.phototopo.selectedPoint){
550 			this.circle.attr(styles.handleSelectedActive);
551 			/*
552 			this.circle.attr(styles.handleSelected);
553 			
554 			this.circle.animate(styles.handleSelectedActive, 500, function(){
555 				this.animate(styles.handleSelected, 500, function(){
556 					point.setStyle();
557 				});
558 			});
559 			*/
560 		} else if (this.route === this.route.phototopo.selectedRoute){
561 			// if any point on the selected route
562 			this.circle.animate(styles.handleSelected, 100);
563 		} else {
564 			// if any other point on another route
565 			this.circle.animate(styles.handle, 100);
566 			if (this.route.autoColor){
567 				this.circle.animate({'fill': this.route.autoColor}, 100);
568 			}
569 		}
570 	}
571 };
572 
573 /**
574  * @private
575  * select the active point. new points on the route are added after this point
576  * also explicitly selects the route the point is on
577  */
578 Point.prototype.select = function(dontSelectRoute){
579 	
580 //	if (this.route.phototopo.selectedPoint === this) return;
581 	
582 	var previous = this.route.phototopo.selectedPoint;
583 	
584 	//this.route.phototopo.selectedPoint = this;
585 	if (!dontSelectRoute){
586 		this.route.select(this);
587 	}
588 	if (previous){
589 		previous.setStyle();
590 	}
591 		
592 	this.setStyle();
593 };
594 
595 
596 
597 /**
598  * @private
599  * attempts to move the Point to a new location - it may not move due to 'stickyness' to itself and other points
600  */
601 Point.prototype.moveTo = function(x,y){
602 
603 	if (this.x === x && this.y === y){
604 		return { x: x, y: y };
605 	}
606 	if (isNaN(x) || isNaN(y) ){
607 		return { x: this.x, y: this.y };
608 	}
609 
610 	this.pointGroup.remove(this);
611 	this.pointGroup.redraw();
612 
613 	this.x = x;
614 	this.y = y;
615 
616 	this.pointGroup = this.route.phototopo.getPointGroup(this);
617 	
618 	// retrive the x and y from the point group which might not be what we specified
619 	this.x = this.pointGroup.x;
620 	this.y = this.pointGroup.y;
621 
622 
623 	this.pointGroup.redraw();
624 	if (this.nextPoint){
625 		this.nextPoint.pointGroup.redraw();
626 	}
627 	if (this.prevPoint){
628 		this.prevPoint.pointGroup.redraw();
629 	}
630 
631 	if (this.labelEl){
632 		this.updateLabelPosition();
633 	}
634 
635 	this.updateIconPosition();
636 
637 	this.route.phototopo.saveData();
638 
639 	return { x: this.x, y: this.y };
640 
641 
642 };
643 
644 
645 
646 
647 
648 
649 
650 
651 /**
652  * @private
653  * a path connects two points
654  * @constructor 
655  * @param {Point} point1 The starting point
656  * @param {Point} point2 The ending point
657  */
658 function Path(point1, point2){
659 
660 	function getID(){
661 		var id=0;
662 		return id++;
663 	}
664 
665 
666 
667 
668 	var route, offset, path, phototopo;
669 
670 	this.point1 = point1;
671 	this.point2 = point2;
672 	this.id = getID();
673 	this.svg_part = '';
674 	this.point1.nextPath = this;
675 	this.point2.prevPath = this;
676 
677 	phototopo = this.point1.route.phototopo;	
678 
679 
680 	path = 'M'+this.point1.x+' '+this.point1.y+' L'+this.point2.x+' '+this.point2.y;
681 	this.svg_part = path;
682 
683 	this.pathPart = ' L'+this.point2.x+' '+this.point2.y;
684 
685 
686 	this.outline = phototopo.canvas.path(path);
687 	this.ghost   = phototopo.canvas.path(path);
688 	
689 	this.outline.toBack();
690 	this.ghost.toBack();
691 	if (this.point1.route.phototopo.bg){
692 		this.point1.route.phototopo.bg.toBack();
693 	}
694 	this.point1.route.phototopo.fill.toBack();
695 
696 
697 	this.curve      = phototopo.canvas.path(path);
698 	this.outline.attr(phototopo.styles.outline);
699 	if (this.point1.route.autoColorBorder){
700 		this.outline.attr('stroke', this.point1.route.autoColorBorder);
701 	}
702 	
703 	
704 	this.ghost.attr  (phototopo.styles.ghost);
705 	this.curve.attr  (phototopo.styles.stroke);
706 
707 	if (this.point1.route.autoColor){
708 		this.curve.attr('stroke', this.point1.route.autoColor);
709 	}
710 	
711 	this.curve.path = this;
712 	this.outline.path = this;
713 	this.ghost.path = this;
714 
715 	if (phototopo.options.editable){
716 		this.point1.circle.toFront();
717 		this.point2.circle.toFront();
718 	}
719 	
720 	this.curve.mouseover(function(event){	this.path.point1.route.onmouseover();	});
721 	this.ghost.mouseover(function(event){	this.path.point1.route.onmouseover();	});
722 	this.outline.mouseover(function(event){	this.path.point1.route.onmouseover();	});
723 	this.curve.mouseout(function(event){	this.path.point1.route.onmouseout();	});
724 	this.ghost.mouseout(function(event){	this.path.point1.route.onmouseout();	});
725 	this.outline.mouseout(function(event){	this.path.point1.route.onmouseout();	});
726 	
727 	
728 	
729 	function PathClick(event){
730 		if (!phototopo.options.editable){
731 			this.path.point1.select();
732 			return;			
733 		}
734 		var route = phototopo.selectedRoute,
735 			path = event.target.raphael.path;
736 		if (route){
737 			if (path.point1.route === route){
738 				path.point1.select();
739 			}
740 			offset = $(phototopo.photoEl).offset();
741 
742 			route.addAfter(phototopo.selectedPoint,
743 				event.clientX - offset.left + $(window).scrollLeft(),
744 				event.clientY - offset.top  + $(window).scrollTop()  );
745 		} else {
746 			this.path.point1.select();
747 		}
748 	}
749 
750 	this.curve.click(PathClick);
751 	this.outline.click(PathClick);
752 	this.ghost.click(PathClick);
753 }
754 
755 
756 /*
757  * @private
758  * changes the start point
759  */
760 Path.prototype.redraw = function(point){
761 	var handle1 = this.point1.pointGroup.getAngle(this.point1),
762 		handle2 = this.point2.pointGroup.getAngle(this.point2),
763 		points,
764 		path,
765 		phototopo = this.point1.route.phototopo,
766 		path_finish = '',
767 		off1, off2, thickness,
768 		ddx, ddy,
769 		delta, angle, aWidth, aHeight, size,
770 		ex, ey;
771 
772 	points = [
773 		this.point1,
774 		{x: this.point1.x + handle1.dx, y:this.point1.y + handle1.dy},
775 		{x: this.point2.x - handle2.dx, y:this.point2.y - handle2.dy},
776 		this.point2
777 	];
778 	
779 	
780 	/*
781 	 * takes a set of points that defines a bezier curve and offsets it
782 	 */
783 	function getBezierOffset(points, offset1, offset2){
784 	
785 		function secant(theta){
786 			return 1 / Math.cos(theta);
787 		}
788 	
789 		var res = [{}],
790 			c,
791 			angles = [],
792 			size = points.length -1,
793 			offset,
794 			offSec,
795 			angleAvg;
796 		for(c=0; c<3; c++){
797 			angles[c] = Math.atan2(points[c+1].y - points[c].y, points[c+1].x - points[c].x);
798 		}
799 		for(c=1; c<size; c++){
800 			offset = (offset1 * (size-c)) / size + (offset2 * c)/size;
801 			offSec = offset * secant((angles[c] - angles[c-1])/2);
802 			angleAvg = (angles[c]+angles[c-1])/2;
803 			res[c] = {
804 				x: points[c].x - offSec * Math.sin(angleAvg),
805 				y: points[c].y + offSec * Math.cos(angleAvg)
806 			};
807 		}
808 		res[0] = {
809 			x: points[0].x - offset1 * Math.sin(angles[0]),
810 			y: points[0].y + offset1 * Math.cos(angles[0])
811 		};
812 		res[size] = {
813 			x: points[size].x - offset2 * Math.sin(angles[size-1]),
814 			y: points[size].y + offset2 * Math.cos(angles[size-1])
815 		};
816 		for(c=0; c<res.length; c++){
817 			res[c].x = Math.round(res[c].x);
818 			res[c].y = Math.round(res[c].y);
819 		}
820 		return res;
821 	}
822 
823 
824 
825 	if (phototopo.options.seperateRoutes){
826 		thickness = phototopo.options.thickness;
827 		off1 = this.point1.pointGroup.getSplitOffset(this.point1) * thickness * 1.4;
828 		off2 = this.point2.pointGroup.getSplitOffset(this.point2) * thickness * 1.4;
829 		points = getBezierOffset(points, off1, off2);
830 	}
831 
832 	this.svg_part = 
833 		'C' + points[1].x + ' '+points[1].y +
834 		' ' + points[2].x + ' '+points[2].y +
835 		' ' + points[3].x + ' '+points[3].y;
836 		
837 	path = 'M' + points[0].x + ' '+points[0].y + this.svg_part;
838 		
839 
840 	function offset(angle, x, y, dx, dy){
841 		ddx = Math.round((x - Math.sin(angle)*dx - Math.cos(angle)*dy)*10)/10;
842 		ddy = Math.round((y + Math.cos(angle)*dx - Math.sin(angle)*dy)*10)/10;
843 		return 'L'+ddx+' '+ddy+' ';
844 	}
845 
846 	delta = this.point2.pointGroup.getAngle();
847 	angle = Math.atan2(delta.dy, delta.dx);
848 	size = phototopo.options.thickness * 0.5;
849 
850 	// x,y of end point
851 	ex = points[3].x;
852 	ey = points[3].y;
853 	// If this is the end of the Path then draw an arrow head
854 	if (!this.point2.nextPoint && this.point2.type === 'jumpoff'){
855 		aWidth = size*4;
856 		aHeight = size*0.1;
857 
858 		path_finish += offset(angle, ex, ey,       0,  -aHeight   ); // bottom middle
859 		path_finish += offset(angle, ex, ey, -aWidth,  -aHeight   ); // bottom left
860 		path_finish += offset(angle, ex, ey, -aWidth,   aHeight   ); // top left
861 		path_finish += offset(angle, ex, ey,  aWidth,   aHeight   ); // top right
862 		path_finish += offset(angle, ex, ey,  aWidth,  -aHeight   ); // bottom left
863 		path_finish += offset(angle, ex, ey, -aWidth,  -aHeight   ); // bottom left
864 	
865 	// draw a T bar stop
866 	} else if (!this.point2.nextPoint){
867 		aWidth  = size*1.5;
868 		aHeight = size*1.5;
869 		path_finish += offset(angle, ex, ey,       0,  size*1.2   ); // middle
870 		path_finish += offset(angle, ex, ey, -aWidth,  aHeight    ); // bottom left
871 		path_finish += offset(angle, ex, ey,  aWidth,  aHeight    ); // bottom right
872 		path_finish += offset(angle, ex, ey,       0, -size*2.3   ); // top
873 		path_finish += offset(angle, ex, ey, -aWidth,  aHeight    ); // bottom left
874 		path_finish += offset(angle, ex, ey,  aWidth,  aHeight    ); // bottom right
875 	}
876 	this.svg_part += path_finish;
877 	path += path_finish;
878 
879 	this.curve.attr('path', path);
880 	
881 	if (this.point1.type === 'hidden'){
882 		this.curve.attr(phototopo.styles.strokeHidden);
883 	} else {
884 		this.curve.attr(phototopo.styles.strokeVisible);
885 	}
886 	
887 	this.outline.attr('path', path);
888 	this.ghost.attr('path', path);
889 };
890 
891 
892 
893 
894 
895 
896 
897 
898 
899 
900 
901 /**
902  * @constructor 
903  * @param phototopo the topo to add this route to
904  * @param id - is a unique string identifying the route (eg a primary id in the DB)
905  * #param order is a number used for sorting the routes into a natural order (typically 1..x from left to right)
906  * @property {String} id the unique id of this route
907  
908  */
909 function Route(phototopo, id, order){
910 	this.phototopo = phototopo;
911 	this.id = id;
912 	this.order = order ? order : id;
913 	this.points = [];
914 	this.paths = [];
915 	this.label = {};
916 }
917 
918 /**
919  * @private
920  */
921 Route.prototype.addPoint = function(x,y,type,offset){
922 
923 	var c, p, path;
924 
925 	// if offset is not specified then add it at the end of the route
926 	if (offset === undefined || offset === null){
927 		offset = this.points.length;
928 	}
929 	x = Math.floor(x);
930 	y = Math.floor(y);
931 
932 	p = new Point(this, x, y, type, offset);
933 
934 	// fix next and prev pointers
935 	p.prevPoint = this.points[offset-1];
936 	if (p.prevPoint){
937 		p.nextPoint = p.prevPoint.nextPoint;
938 		p.nextPath = p.prevPoint.nextPath;
939 		p.prevPoint.nextPoint = p;
940 	}
941 
942 	// what about p.next??
943 	
944 	// loop through and fix positions
945 	
946 	//each point has both next and prev points AND paths
947 
948 	// add this point into the point list
949 	this.points.splice(offset, 0, p);
950 
951 
952 	// recalc the points positions
953 	for(c = this.points.length-1; c>offset; c--){
954 		this.points[c].position = c;
955 	}
956 	if (p.nextPoint){
957 		p.nextPoint.prevPoint = p;
958 	}
959 
960 	// if more than one point make a path
961 	if(this.points.length >1){
962 		path = new Path(this.points[offset-1], this.points[offset]);
963 		this.paths.splice(offset-1, 0, path);
964 		if (this.paths[offset]){
965 			this.paths[offset].point1 = p;
966 		}
967 	}
968 	
969 	this.phototopo.saveData();
970 	this.phototopo.updateHint();
971 
972 	if (p.nextPoint){
973 		p.nextPoint.pointGroup.redraw();
974 	}
975 	if (p.prevPoint){
976 		p.prevPoint.pointGroup.redraw();
977 	}
978 	p.pointGroup.redraw();
979 
980 	return p;
981 };
982 
983 
984 /**
985  * @private
986  * add's a new point to a route, optionally after the point
987  *
988  */
989 Route.prototype.addAfter = function(afterPoint, x, y, type){
990 	
991 	var pos = afterPoint ? afterPoint.position+1 : null,
992 		newPoint = this.addPoint(x,y,type, pos);
993 	newPoint.select();
994 
995 };
996 
997 
998 /**
999  * @private
1000  * sets the label for this route
1001  * The label may appear in more than one place
1002  * if selected if will have a class of 'selected'
1003  * it may also have a class of 'start'
1004  */
1005 Route.prototype.setLabel = function(label){
1006 	this.label = label;
1007 	if (this.label.label && this.points.length > 0){
1008 		this.points[0].setLabel('start '+this.label.classes, this.label.label);
1009 	}
1010 	// else draw the label somewhere else so notify that it is missing??
1011 };
1012 
1013 /**
1014  * @private
1015  * serialise the point data and send back to the page to be saved 
1016  */
1017 Route.prototype.getPoints = function(){
1018 	var points = '',
1019 		path = '',
1020 		point,
1021 		c;
1022 	for(c=0; c<this.points.length; c++){
1023 		point = this.points[c];
1024 		if (c!== 0){
1025 			points += ',';
1026 		} else {
1027 			path += 'M' + point.x + ' '+point.y;
1028 		}
1029 		points += point.x + ' ' + point.y;
1030 		if (point.type){
1031 			points += ' ' + point.type;
1032 		}
1033 		if (point.nextPath){
1034 			path += point.nextPath.svg_part;
1035 		}
1036 	}
1037 	return { points: points, path: path };
1038 };
1039 
1040 /**
1041  * @private
1042  * select this route, and optionally specifies which point to select within the route
1043  * if no point specifices selects the last point in the route (if it has any points at all)
1044  */
1045 Route.prototype.select = function(selectedPoint){
1046 	var phototopo = this.phototopo,
1047 		previousRoute = phototopo.selectedRoute,
1048 		styles = phototopo.styles,
1049 		c;
1050 	
1051 	if (phototopo.selectedRoute === this && selectedPoint === phototopo.selectedPoint){
1052 		return;
1053 	}
1054 	
1055 
1056 	if (previousRoute && previousRoute !== this){
1057 		previousRoute.deselect();
1058 	}
1059 	
1060 	phototopo.selectedRoute = this;
1061 	
1062 	if (this.label.label && this.points.length > 0){
1063 		this.points[0].setLabel('selected start '+this.label.classes, this.label.label);
1064 	}
1065 
1066 	
1067 	if (!selectedPoint){
1068 		if (this.points.length > 0){
1069 			selectedPoint = this.points[this.points.length-1];
1070 		}
1071 	}
1072 	phototopo.selectedPoint = selectedPoint;
1073 	if (selectedPoint){
1074 		selectedPoint.select(true);
1075 	}
1076 
1077 	if (phototopo.options.onselect){
1078 		phototopo.options.onselect(this);
1079 	}
1080 	
1081 	// now highlight the new route and make sure it is at the front
1082 	for(c=0; c< this.paths.length; c++){
1083 		this.paths[c].outline.attr(styles.outlineSelected).toFront();
1084 	}
1085 	for(c=0; c< this.paths.length; c++){
1086 		this.paths[c].curve.attr(styles.strokeSelected).toFront();
1087 	}
1088 	if (phototopo.options.editable === true){
1089 		for(c=0; c< this.paths.length; c++){
1090 			this.paths[c].point2.circle.attr(styles.handleSelected).toFront();
1091 		}
1092 		if (this.points[0]){
1093 			this.points[0].circle.attr(styles.handleSelected).toFront();
1094 		}
1095 	}
1096 	
1097 	phototopo.updateHint();
1098 	phototopo.updateCursor();
1099 
1100 };
1101 
1102 /**
1103  * @private
1104  * deselect this route
1105  */
1106 Route.prototype.deselect = function(){
1107 
1108 	var autoColor = this.autoColor,
1109 	autoColorBorder = this.autoColorBorder,
1110 	phototopo = this.phototopo,
1111 	c;
1112 
1113 	if (phototopo.options.ondeselect){
1114 		phototopo.options.ondeselect(this);
1115 	}
1116 
1117 	phototopo.selectedRoute = null;
1118 	phototopo.selectedPoint = null;
1119 
1120 
1121 	for(c=0; c< this.paths.length; c++){
1122 		this.paths[c].curve.attr(phototopo.styles.stroke);
1123 		this.paths[c].outline.attr(phototopo.styles.outline);
1124 
1125 		if (autoColor){
1126 			this.paths[c].curve.attr('stroke', autoColor);
1127 			this.paths[c].outline.attr('stroke', autoColorBorder);
1128 		}
1129 	}
1130 	if (phototopo.options.editable === true){
1131 		for(c=0; c< this.points.length; c++){
1132 			this.points[c].circle.attr(phototopo.styles.handle);
1133 			if (autoColor){
1134 				this.points[c].circle.attr('fill', autoColor);
1135 				this.points[c].circle.attr('stroke', autoColorBorder);
1136 			}
1137 		}
1138 	}
1139 	if (this.label.label && this.points.length > 0){
1140 		this.points[0].setLabel('start '+this.label.classes, this.label.label);
1141 	}
1142 	phototopo.updateHint();
1143 	phototopo.updateCursor();
1144 
1145 };
1146 
1147 /**
1148  * @private
1149  * redraw all components of this route 
1150  */
1151 Route.prototype.redraw = function(){
1152 	var c;
1153 	for(c=0; c< this.paths.length; c++){
1154 		this.paths[c].redraw();
1155 	}
1156 };
1157 
1158 /**
1159  * @private
1160  */
1161 Route.prototype.onmouseover = function(point){
1162 	$(this.phototopo.photoEl).addClass('route');
1163 	if (this === this.phototopo.selectedRoute){
1164 		$(this.phototopo.photoEl).addClass('selectedRoute');
1165 	}
1166 	
1167 	if (this.phototopo.options.onmouseover){
1168 		this.phototopo.options.onmouseover(this);
1169 	}
1170 };
1171 
1172 /**
1173  * @private
1174  */
1175 Route.prototype.onmouseout = function(point){
1176 	$(this.phototopo.photoEl).removeClass('route selectedRoute');
1177 	
1178 	if (this.phototopo.options.onmouseout){
1179 		this.phototopo.options.onmouseout(this);
1180 	}
1181 };
1182 /**
1183  * @private
1184  */
1185 Route.prototype.onclick = function(point){
1186 	if (this.phototopo.options.onclick){
1187 		this.phototopo.options.onclick(this);
1188 	}
1189 };
1190 
1191 
1192 
1193 
1194 
1195 
1196 
1197 
1198 
1199 
1200 
1201 
1202 
1203 
1204 
1205 
1206 
1207 /**
1208  * A widget for viewing and drawing climbing routes overlaid on a photo
1209  * @constructor 
1210  * @param {PhotoTopoOptions} opts The options for this phototopo
1211  */
1212 
1213 function PhotoTopo(opts){
1214 
1215 
1216 
1217 /**
1218  * A callback function
1219  * @constructor
1220  * @type Callback 
1221  * @param {Route} route The route
1222  */
1223 PhotoTopo.Callback = function(){};
1224 /**
1225  * The options to pass in when creating a topo
1226  * @constructor
1227  * @property elementId The id of the html div that the topo shold be created in
1228  * @property {Interger} width The width of the topo in pixels
1229  * @property {Interger} height The height of the topo in pixels
1230  * @property {String} imageUrl The url to the photo
1231  * @property  routes a json hash of routes. Each 
1232  * @property {Boolean} editable true if you want the widget to be editable
1233  * @property {Boolean} seperateRoutes If you want the routes to not overlap when they use the same points
1234  * @property {Boolean} autoColors If you want the color of the route to be inherited from the color of the label 
1235  * @property {Integer} thickness The thickness of the routes in pixels 
1236  * @property {Function} onmouseover  A callback with the Route
1237  * @property {Function} onmouseout A callback with the Route
1238  * @property {Function} onclick A callback with the Route
1239  * @property {Function} onselect A callback with the Route
1240  * @property {Function} ondeselect A callback with the Route
1241  * @property {Function} onchange A callback with a JSON dump of all route data to persist somehow
1242  * @property {Function} getlabel A function which when given a routeId should return a RouteLabel
1243  * @property {Boolean} showPointTypes If false point types are hidden (eg on small versions of a topo)
1244  * @property {Float} viewScale A scaling factor for drawing the topo						  
1245  */
1246 PhotoTopo.Options = function(){};
1247 
1248 
1249 /**
1250  * A label to be passed back to the getLabel callback
1251  * @constructor 
1252  * @property {String} label A small label for the route, eg a number, or the initials of the route name
1253  * @property {String} class A css class for styline
1254  */
1255 PhotoTopo.RouteLabel = function(){};
1256 
1257 
1258 	var errors = false,
1259 		data,
1260 		pc, c,
1261 		label,
1262 		tempEl,
1263 		autoColor,
1264 		autoColorBorder,
1265 		labelsdiv,
1266 		viewScale, parts, points;
1267 	
1268 	/**
1269 	 * @private
1270 	 */
1271 	function checkDefault(option, value){
1272 		if (opts[option] === undefined){
1273 			opts[option] = value;
1274 		}
1275 	}
1276 	/**
1277 	 * @private
1278 	 */
1279 	function missingError(exp, text){
1280 		if (!exp){
1281 			errors = true;
1282 			throw('PhotoTopo config error: '+text);
1283 		}
1284 	}
1285 
1286 	this.options = opts;
1287 	
1288 	missingError(this.options, 'No options hash');
1289 	missingError(this.options.elementId, 'No elementId');
1290 	missingError(this.options.width, 'No width');
1291 	missingError(this.options.height, 'No height');
1292 	checkDefault('thickness', 5);
1293 	checkDefault('viewScale', 1);
1294 	checkDefault('showPointTypes', true);
1295 //	checkDefault('onchange', function(){} );
1296 
1297 
1298 
1299 
1300 
1301 /*
1302 	missingError(this.options.origWidth, 'No origWidth');
1303 	missingError(this.options.origHeight, 'No origHeight');
1304 */
1305 	missingError(this.options.imageUrl, 'No imageUrl');
1306 	
1307 	this.photoEl = document.getElementById(this.options.elementId);
1308 	this.photoEl.phototopo = this;
1309 	this.photoEl.className = 'phototopo' + (this.options.editable === true ? ' editable' : '');
1310 
1311 
1312 	labelsdiv = document.createElement("div");
1313 	
1314 	labelsdiv.className = 'labels';
1315 	labelsdiv.innerHTML = '';
1316 	this.labelsEl = labelsdiv;
1317 	this.labelsEl.style.top = (-this.options.height+'px');
1318 	document.getElementById(this.options.elementId).appendChild(this.labelsEl);
1319 
1320 	
1321 	
1322 	this.canvas = Raphael(this.options.elementId, this.options.width, this.options.height);
1323 
1324 
1325 	if (this.options.editable && !document.getElementById('#phototopoContextMenu') ){
1326 		$('body').append(
1327 '<ul id="phototopoContextMenu" class="contextMenu">'+
1328 '    <li class="none"><a href="#">None</a></li>'+
1329 '    <li class="jumpoff"><a href="#jumpoff">Jump off</a></li>'+
1330 '    <li class="hidden"><a href="#hidden">Hidden</a></li>'+
1331 '    <li class="separator">Protection</li>'+
1332 '    <li class="bolt"><a href="#bolt">Bolt</a></li>'+
1333 '    <li class="draw"><a href="#draw">Clip</a></li>'+
1334 '    <li class="separator">Misc</li>'+
1335 '    <li class="crux"><a href="#crux">Crux</a></li>'+
1336 '    <li class="warning"><a href="#warning">Warning</a></li>'+
1337 '    <li class="belay"><a href="#belay">Belay</a></li>'+
1338 '    <li class="belaysemi"><a href="#belaysemi">Semi-belay</a></li>'+
1339 '    <li class="belayhanging"><a href="#belayhanging">Hanging Belay</a></li>'+
1340 '</ul>'
1341 );
1342 	}
1343 
1344 
1345 	// size of visible image
1346 	this.shownWidth = -1;
1347 	this.shownHeight = -1;
1348 	this.scale = 1;
1349 	
1350 	this.selectedRoute = null;
1351 	this.selectedPoint = null;
1352 	this.routesVisible = true;
1353 	
1354 
1355 	// a store for the routes
1356 	this.routes = {};
1357 	
1358 	// point groups
1359 	this.pointGroups = {};
1360 
1361 
1362 	this.styles = {
1363 		outline: {
1364 			'stroke': 'black', // default if it can't inherit from label colour
1365 			'stroke-width': this.options.thickness * 1.7,
1366 			'stroke-linejoin': 'miter',
1367 			'stroke-linecap': 'round',
1368 			'stroke-opacity': 1 
1369 		},
1370 		outlineSelected: {
1371 			'stroke': 'white' // default if it can't inherit from label colour
1372 		},
1373 		ghost: {
1374 			'stroke': 'red',
1375 			'stroke-width': this.options.thickness * 4,
1376 			'stroke-linejoin': 'miter',
1377 			'stroke-linecap': 'round',
1378 			'stroke-opacity': 0.01 
1379 		},
1380 		stroke: {
1381 			'stroke': 'yellow',
1382 			'stroke-width': this.options.thickness,
1383 			'stroke-linejoin': 'miter',
1384 			'stroke-linecap': 'round',
1385 			'stroke-opacity': 1 
1386 		},
1387 		strokeSelected: {
1388 			'stroke-width': this.options.thickness,
1389 			'stroke': '#3D80DF' // default if it can't inherit from label colour
1390 		},
1391 		strokeVisible: {
1392 			'stroke-dasharray': ''
1393 		},
1394 		strokeHidden: {
1395 			'stroke-dasharray': '.'
1396 		},
1397 		handle: {
1398 			'stroke': 'black', // default if it can't inherit from label colour
1399 			'r': this.options.thickness * 1.2,
1400 			'fill': 'yellow',
1401 			'stroke-width': this.options.thickness * 0.4
1402 		},
1403 		handleHover: {
1404 			'fill': 'white'
1405 		},
1406 		handleSelected: {
1407 			'fill': '#3D80DF',
1408 			'stroke': 'white' // default if it can't inherit from label colour
1409 		},
1410 		handleSelectedActive: {
1411 			'fill': '#fff' // same as handle selected stroke colour
1412 		}
1413 	};
1414 
1415 
1416 
1417 
1418 
1419 
1420 	this.fill = this.canvas.rect(0, 0, this.options.width, this.options.height).attr({
1421 		fill: '#7a9'
1422 	});
1423 
1424 
1425 	if (errors){
1426 		return;
1427 	}
1428 
1429 
1430 
1431 	// colour the background
1432 	this.setImage(this.options.imageUrl);	
1433 
1434 	// Now draw the routes
1435 	// ALL of this logic should be inside the Route class!!
1436 	this.loading = true;
1437 	viewScale = this.options.viewScale;
1438 	for(c=0; c<this.options.routes.length; c++){
1439 		data = {};
1440 		data = this.options.routes[c];
1441 		if (this.routes[data.id]){
1442 			alert('Error: duplicate route=['+data.id+'] '+this.options.elementId);
1443 		}
1444 		this.routes[data.id] = new Route(this, data.id, data.order);
1445 		if (this.options.getlabel){
1446 			label = this.options.getlabel(data.id);
1447 			if (this.options.autoColors){
1448 				tempEl = $("<div class='"+label.classes+"'>");
1449 				this.labelsEl.appendChild(tempEl[0]);
1450 				autoColor = tempEl.css('background-color');
1451 				autoColorBorder = tempEl.css('border-top-color');
1452 				this.labelsEl.removeChild(tempEl[0]);
1453 				this.routes[data.id].autoColor = autoColor;
1454 				this.routes[data.id].autoColorBorder = autoColorBorder;
1455 			}
1456 		}
1457 		if (data.points){
1458 			points = data.points.split(',');
1459 			for(pc = 0; pc < points.length; pc++){
1460 				parts = points[pc].split(/\s/);
1461 				if (parts[0] === ''){
1462 					parts.splice(0,1);
1463 				}
1464 				this.addToRoute(data.id, parts[0]*viewScale, parts[1]*viewScale, parts[2]);
1465 			}
1466 		}
1467 		if (this.options.getlabel){
1468 			label = this.options.getlabel(data.id);
1469 			this.routes[data.id].setLabel(label);
1470 		}
1471 	
1472 
1473 	}
1474 	this.loading = false;
1475 
1476 	this.redraw();
1477 	this.saveData();
1478 	
1479 	this.updateHint();
1480 	this.updateCursor();	
1481 }
1482 
1483 /**
1484  * Sets wether the route lines are visible
1485  * @param {Boolean} visible if true makes the routes visible
1486  */
1487 PhotoTopo.prototype.setRouteVisibility = function(visible){
1488 	var phototopo = this;
1489 	phototopo.routesVisible = visible;
1490 	if (show){
1491 		phototopo.bg.toBack();
1492 		phototopo.fill.toBack();
1493 		phototopo.labelsEl.style.display = 'block';
1494 	} else {
1495 		phototopo.bg.toFront();
1496 		phototopo.labelsEl.style.display = 'none';
1497 	}
1498 };
1499 
1500 /**
1501  * @private
1502  */
1503 PhotoTopo.prototype.updateHint = function(){
1504 	if (!this.options.editable){
1505 		return;
1506 	}
1507 
1508 	if (this.selectedRoute === null){
1509 		this.setHint('Select the route you wish to draw or edit in the table below');
1510 	} else if (this.selectedRoute.points.length === 0){
1511 		this.setHint('Click at the beginning of the route to start drawing this route');
1512 	} else {
1513 		this.setHint('Click to add or select, then drag to move. Double click to remove');
1514 	}
1515 
1516 };
1517 
1518 /**
1519  * @private
1520  */
1521 PhotoTopo.prototype.setHint = function(hintHTML){
1522 	if (!this.hintEl){
1523 		this.hintEl = $('<div class="hint ui-state-highlight"></div>').show('slide').appendTo(this.photoEl)[0];
1524 	}
1525 	this.hintEl.innerHTML = '<span class="ui-icon ui-icon-info" style="float: left;"></span><strong>Hint:</strong> '+hintHTML;
1526 	$(this.hintEl).offset(0,0);
1527 };
1528 
1529 
1530 /**
1531  * Selects the route with a given id
1532  * @param {Route} route the route to select
1533  * @param {Boolean} [toggle] if true and the route is already selected will delesect
1534  */
1535 PhotoTopo.prototype.selectRoute = function(routeId, toggle){
1536 	
1537 	if (this.routes[routeId]){
1538 		if (toggle && this.routes[routeId] === this.selectedRoute){
1539 			this.selectedRoute.deselect();
1540 		} else {
1541 			this.routes[routeId].select();
1542 		}
1543 	} else {
1544 		if (this.selectedRoute){
1545 			this.selectedRoute.deselect();
1546 		}
1547 	}
1548 };
1549 
1550 
1551 
1552 /**
1553  * @private
1554  * save the data down to the page to be serialised in some form
1555  * @returns a json strucure with all point data
1556  */
1557 PhotoTopo.prototype.saveData = function(){
1558 	var routeId,
1559 		data = {routes: [], changed: false },
1560 		route,
1561 		points,
1562 		path;
1563 	if (this.loading){
1564 		return;
1565 	}
1566 	if (this.changed){
1567 		data.changed = true;
1568 	}
1569 	if (!this.changed){
1570 		this.changed = true;
1571 	}
1572 	if (!this.options.onchange ){
1573 		return;
1574 	}
1575 	
1576 	for(routeId in this.routes){
1577 		route = this.routes[routeId];
1578 		routeData = route.getPoints();
1579 		data.routes[data.routes.length] = {
1580 			id: routeId,
1581 			points: routeData.points,
1582 			svg_path: routeData.path,
1583 			order: route.order
1584 		};
1585 	}
1586 
1587 	this.options.onchange(data);
1588 
1589 };
1590 
1591 /**
1592  * @private
1593  */
1594 PhotoTopo.prototype.redraw = function(){
1595 	var routeId, r;
1596 	for(routeId in this.routes){
1597 		r = this.routes[routeId];
1598 		r.redraw();
1599 		if (r.points.length){
1600 			r.select(); // select them all to flush the outline z-index
1601 			r.points[0].updateLabelPosition();
1602 		}
1603 	}
1604 	this.selectRoute(); // select nothing
1605 };
1606 
1607 
1608 /**
1609  * @private
1610  * creates or retreives a point group for a new point location
1611  * if the point is close to another point it will 'stick' the point to the previous point
1612  */
1613 PhotoTopo.prototype.getPointGroup = function(point){
1614 
1615 	var x = point.x,
1616 		y = point.y,
1617 		threshhold,
1618 		key,
1619 		group;
1620 
1621 	x = Math.round(x);
1622 	y = Math.round(y);
1623 
1624 	// make sures it's inside the picture
1625 	if (x<0){ x = 0;}
1626 	if (y<0){ y = 0;}
1627 	if (x>this.shownWidth ){ x = this.shownWidth; }
1628 	if (y>this.shownHeight){ y = this.shownHeight;}
1629 
1630 	key = this.getKey(point);
1631 	group = this.pointGroups[key];
1632 		
1633 	point.x = x;
1634 	point.y = y;
1635 	
1636 	if(group){
1637 		group.add(point);
1638 	} else {
1639 		group = new PointGroup(point);
1640 		this.pointGroups[key] = group;
1641 	}
1642 
1643 	return group;
1644 		
1645 };
1646 
1647 /**
1648  * @private
1649  * given an x,y coord return a key for saving this
1650  */
1651 PhotoTopo.prototype.getKey = function(point){
1652 	var
1653 	threshhold = this.options.thickness * 4,
1654 	tx = point.x - point.x % threshhold + threshhold / 2,
1655 	ty = point.y - point.y % threshhold + threshhold / 2;
1656 	return tx + '_' + ty;
1657 };
1658 
1659 
1660 
1661 /**
1662  * @private
1663  * Adds or inserts a new Point into a route
1664  */
1665 PhotoTopo.prototype.addToRoute = function(routeId, x, y, type, position){
1666 	this.routes[routeId].addPoint(x,y,type,position);
1667 	
1668 };
1669 
1670 
1671 
1672 /**
1673  * Set the photo image and triggers a redraw
1674  * @param {String} imageUrl a url to an image
1675  */
1676 PhotoTopo.prototype.setImage = function(imageUrl){
1677 		
1678 	var phototopo = this,
1679 		options = phototopo.options,
1680 		img = new Image();
1681 		
1682 	
1683 	// Set these first so we have a workable size before the image gets loaded asynchronously
1684 	this.shownWidth  = this.options.width;
1685 	this.shownHeight = this.options.height;
1686 	
1687 	this.options.imageUrl = imageUrl;
1688 	$(img).load(function(){
1689 		options.origWidth = img.width;
1690 		options.origHeight = img.height;
1691 
1692 		phototopo.shownWidth = img.width;
1693 		phototopo.shownHeight = img.height;
1694 		if (phototopo.shownHeight > options.height){
1695 			phototopo.scale = options.height / phototopo.shownHeight;
1696 			phototopo.shownHeight *= phototopo.scale;
1697 			phototopo.shownWidth *= phototopo.scale; 
1698 		}
1699 		if (phototopo.shownWidth > options.width){
1700 			phototopo.scale = phototopo.scale * options.width / phototopo.shownWidth;
1701 			phototopo.shownHeight = options.origHeight * phototopo.scale;
1702 			phototopo.shownWidth  = options.origWidth  * phototopo.scale; 
1703 		}
1704 
1705 
1706 		options.imageUrl = imageUrl;
1707 		if (phototopo.bg){
1708 			phototopo.bg.remove();
1709 		}
1710 		phototopo.bg = phototopo.canvas.image(options.imageUrl, 0, 0, phototopo.shownWidth, phototopo.shownHeight);
1711 		phototopo.bg.click(function(event){
1712 			phototopo.clickBackground(event);
1713 		});
1714 								 
1715 		phototopo.bg.toBack();
1716 		phototopo.fill.toBack();
1717 	})
1718 	.attr('src', imageUrl);
1719 };
1720 
1721 
1722 /**
1723  * @private
1724  * handle a click on the background
1725  * if in edit mode and a point is selected then insert it
1726  */
1727 PhotoTopo.prototype.clickBackground = function(event){
1728 	
1729 	if (this.options.editable){
1730 		if (this.selectedRoute){
1731 			var offset = $(this.photoEl).offset();
1732 			this.selectedRoute.addAfter(this.selectedPoint, event.clientX - offset.left + $(window).scrollLeft(), event.clientY - offset.top + $(window).scrollTop(), '');
1733 		} else {
1734 			this.setHint('First select a route or point');
1735 		}
1736 	}
1737 	this.updateCursor();
1738 	
1739 };
1740 
1741 /**
1742  * @private
1743  */
1744 PhotoTopo.prototype.updateCursor = function(){
1745 	var cursor = '',
1746 	jq = $(this.photoEl);
1747 	
1748 	if (!this.selectedRoute){
1749 		cursor = 'noneselected';
1750 	} else {
1751 	// if no route selected then show a 'selectabke' on a mouseover'
1752 	
1753 		if (this.selectedRoute.points.length === 0){
1754 			cursor = 'addfirst';
1755 		} else {
1756 			cursor = 'addmore';
1757 		}
1758 	
1759 	}
1760 	// if a route selected but no points show a 'addfirst'
1761 	// otherwise show a 'add'
1762 
1763 	// if mouseover a point then show a 'draggable'
1764 	jq.removeClass('noneselected addfirst addmore');
1765 	jq.addClass(cursor);
1766 };
1767 /**
1768  * Sets the order of the routes which affects the way they visually thread through each point
1769  * @param order a hash of the id to the order
1770  */
1771 PhotoTopo.prototype.setOrder = function(order){
1772 
1773 	// reorder all the routes
1774 	var id, label;
1775 	for(id in order){
1776 		if (this.routes[id]){
1777 			this.routes[id].order = order[id];
1778 			if (this.options.getlabel){
1779 				label = this.options.getlabel(id);
1780 				this.routes[id].setLabel(label);
1781 			}
1782 		}
1783 	}
1784 	
1785 	// refresh the render
1786 	for(id in this.pointGroups){
1787 		this.pointGroups[id].sort();
1788 		this.pointGroups[id].redraw();
1789 	}
1790 
1791 	this.saveData();
1792 };
1793