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