2 Dervied from the OpenStreetBugs client, which is available
3 under the following license.
5 This OpenStreetBugs client is free software: you can redistribute it
6 and/or modify it under the terms of the GNU Affero General Public License
7 as published by the Free Software Foundation, either version 3 of the
8 License, or (at your option) any later version.
10 This file is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
13 License <http://www.gnu.org/licenses/> for more details.
16 OpenLayers.Layer.Notes = new OpenLayers.Class(OpenLayers.Layer.Markers, {
18 * The URL of the OpenStreetMap API.
22 serverURL : "/api/0.6/",
25 * Associative array (index: note ID) that is filled with the notes
26 * loaded in this layer.
33 * The username to be used to change or create notes on OpenStreetMap.
40 * The icon to be used for an open note.
42 * @var OpenLayers.Icon
44 iconOpen : new OpenLayers.Icon("/images/open_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
47 * The icon to be used for a closed note.
49 * @var OpenLayers.Icon
51 iconClosed : new OpenLayers.Icon("/images/closed_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
54 * The icon to be used when adding a new note.
56 * @var OpenLayers.Icon
58 iconNew : new OpenLayers.Icon("/images/new_note_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
61 * The projection of the coordinates sent by the OpenStreetMap API.
63 * @var OpenLayers.Projection
65 apiProjection : new OpenLayers.Projection("EPSG:4326"),
68 * If this is set to true, the user may not commit comments or close notes.
75 * When the layer is hidden, all open popups are stored in this
76 * array in order to be re-opened again when the layer is made
82 * A URL to append lon=123&lat=123&zoom=123 for the Permalinks.
86 permalinkURL : "http://www.openstreetmap.org/",
89 * A CSS file to be included. Set to null if you don’t need this.
93 theme : "/stylesheets/notes.css",
98 initialize: function(name, options) {
99 OpenLayers.Layer.Markers.prototype.initialize.apply(this, [
101 OpenLayers.Util.extend({
103 projection: new OpenLayers.Projection("EPSG:4326") }, options)
106 putAJAXMarker.layers.push(this);
107 this.events.addEventType("markerAdded");
109 this.events.register("visibilitychanged", this, this.updatePopupVisibility);
110 this.events.register("visibilitychanged", this, this.loadNotes);
113 // check existing links for equivalent url
115 var nodes = document.getElementsByTagName('link');
116 for (var i = 0, len = nodes.length; i < len; ++i) {
117 if (OpenLayers.Util.isEquivalentUrl(nodes.item(i).href, this.theme)) {
122 // only add a new node if one with an equivalent url hasn't already
125 var cssNode = document.createElement('link');
126 cssNode.setAttribute('rel', 'stylesheet');
127 cssNode.setAttribute('type', 'text/css');
128 cssNode.setAttribute('href', this.theme);
129 document.getElementsByTagName('head')[0].appendChild(cssNode);
135 * Called automatically called when the layer is added to a map.
136 * Initialises the automatic note loading in the visible bounding box.
138 afterAdd: function() {
139 var ret = OpenLayers.Layer.Markers.prototype.afterAdd.apply(this, arguments);
141 this.map.events.register("moveend", this, this.loadNotes);
148 * At the moment the OpenStreetMap API responses to requests using
149 * JavaScript code. This way the Same Origin Policy can be worked
150 * around. Unfortunately, this makes communicating with the API a
151 * bit too asynchronous, at the moment there is no way to tell to
152 * which request the API actually responses.
154 * This method creates a new script HTML element that imports the
155 * API request URL. The API JavaScript response then executes the
156 * global functions provided below.
158 * @param String url The URL this.serverURL + url is requested.
160 apiRequest: function(url) {
161 var script = document.createElement("script");
162 script.type = "text/javascript";
163 script.src = this.serverURL + url + "&nocache="+(new Date()).getTime();
164 document.body.appendChild(script);
168 * Is automatically called when the visibility of the layer
169 * changes. When the layer is hidden, all visible popups are
170 * closed and their visibility is saved. When the layer is made
171 * visible again, these popups are re-opened.
173 updatePopupVisibility: function() {
174 if (this.getVisibility()) {
175 for (var i =0 ; i < this.reopenPopups.length; i++)
176 this.reopenPopups[i].show();
178 this.reopenPopups = [ ];
180 for (var i = 0; i < this.markers.length; i++) {
181 if (this.markers[i].feature.popup &&
182 this.markers[i].feature.popup.visible()) {
183 this.markers[i].feature.popup.hide();
184 this.reopenPopups.push(this.markers[i].feature.popup);
191 * Sets the user name to be used for interactions with OpenStreetMap.
193 setUserName: function(username) {
194 if (this.username == username)
197 this.username = username;
199 for (var i = 0; i < this.markers.length; i++) {
200 var popup = this.markers[i].feature.popup;
203 var els = popup.contentDom.getElementsByTagName("input");
205 for (var j = 0; j < els.length; j++) {
206 if (els[j].className == "username")
207 els[j].value = username;
214 * Returns the currently set username or “NoName” if none is set.
216 getUserName: function() {
218 return this.username;
224 * Loads the notes in the current bounding box. Is automatically
225 * called by an event handler ("moveend" event) that is created in
226 * the afterAdd() method.
228 loadNotes: function() {
229 var bounds = this.map.getExtent();
231 if (bounds && this.getVisibility()) {
232 bounds.transform(this.map.getProjectionObject(), this.apiProjection);
234 this.apiRequest("notes"
235 + "?bbox=" + this.round(bounds.left, 5)
236 + "," + this.round(bounds.bottom, 5)
237 + "," + this.round(bounds.right, 5)
238 + "," + this.round(bounds.top, 5));
243 * Rounds the given number to the given number of digits after the
246 * @param Number number
247 * @param Number digits
250 round: function(number, digits) {
251 var scale = Math.pow(10, digits);
253 return Math.round(number * scale) / scale;
257 * Adds an OpenLayers.Marker representing a note to the map. Is
258 * usually called by loadNotes().
260 * @param Number id The note ID
262 createMarker: function(id) {
263 if (this.notes[id]) {
264 if (this.notes[id].popup && !this.notes[id].popup.visible())
265 this.setPopupContent(this.notes[id].popup, id);
267 if (this.notes[id].closed != putAJAXMarker.notes[id][2])
268 this.notes[id].destroy();
273 var lonlat = putAJAXMarker.notes[id][0].clone().transform(this.apiProjection, this.map.getProjectionObject());
274 var comments = putAJAXMarker.notes[id][1];
275 var closed = putAJAXMarker.notes[id][2];
276 var icon = closed ? this.iconClosed : this.iconOpen;
278 var feature = new OpenLayers.Feature(this, lonlat, {
282 feature.popupClass = OpenLayers.Popup.FramedCloud.Notes;
284 feature.closed = closed;
285 this.notes[id] = feature;
287 var marker = feature.createMarker();
288 marker.feature = feature;
289 marker.events.register("click", feature, this.markerClick);
290 //marker.events.register("mouseover", feature, this.markerMouseOver);
291 //marker.events.register("mouseout", feature, this.markerMouseOut);
292 this.addMarker(marker);
294 this.events.triggerEvent("markerAdded");
298 * Recreates the content of the popup of a marker.
300 * @param OpenLayers.Popup popup
301 * @param Number id The note ID
303 setPopupContent: function(popup, id) {
307 var newContent = document.createElement("div");
309 el1 = document.createElement("h3");
310 el1.appendChild(document.createTextNode(putAJAXMarker.notes[id][2] ? i18n("javascripts.note.closed") : i18n("javascripts.note.open")));
312 el1.appendChild(document.createTextNode(" ["));
313 el2 = document.createElement("a");
314 el2.href = "/browse/note/" + id;
315 el2.onclick = function() {
316 layer.map.setCenter(putAJAXMarker.notes[id][0].clone().transform(layer.apiProjection, layer.map.getProjectionObject()), 15);
318 el2.appendChild(document.createTextNode(i18n("javascripts.note.details")));
319 el1.appendChild(el2);
320 el1.appendChild(document.createTextNode("]"));
322 if (this.permalinkURL) {
323 el1.appendChild(document.createTextNode(" ["));
324 el2 = document.createElement("a");
325 el2.href = this.permalinkURL + (this.permalinkURL.indexOf("?") == -1 ? "?" : "&") + "lon="+putAJAXMarker.notes[id][0].lon+"&lat="+putAJAXMarker.notes[id][0].lat+"&zoom=15";
326 el2.appendChild(document.createTextNode(i18n("javascripts.note.permalink")));
327 el1.appendChild(el2);
328 el1.appendChild(document.createTextNode("]"));
330 newContent.appendChild(el1);
332 var containerDescription = document.createElement("div");
333 newContent.appendChild(containerDescription);
335 var containerChange = document.createElement("div");
336 newContent.appendChild(containerChange);
338 var displayDescription = function() {
339 containerDescription.style.display = "block";
340 containerChange.style.display = "none";
343 var displayChange = function() {
344 containerDescription.style.display = "none";
345 containerChange.style.display = "block";
348 displayDescription();
350 el1 = document.createElement("dl");
351 for (var i = 0; i < putAJAXMarker.notes[id][1].length; i++) {
352 el2 = document.createElement("dt");
353 el2.className = (i == 0 ? "note-description" : "note-comment");
354 el2.appendChild(document.createTextNode(i == 0 ? i18n("javascripts.note.description") : i18n("javascripts.note.comment")));
355 el1.appendChild(el2);
356 el2 = document.createElement("dd");
357 el2.className = (i == 0 ? "note-description" : "note-comment");
358 el2.appendChild(document.createTextNode(putAJAXMarker.notes[id][1][i]));
359 el1.appendChild(el2);
361 el2 = document.createElement("br");
362 el1.appendChild(el2);
365 containerDescription.appendChild(el1);
367 if (putAJAXMarker.notes[id][2]) {
368 el1 = document.createElement("p");
369 el1.className = "note-fixed";
370 el2 = document.createElement("em");
371 el2.appendChild(document.createTextNode(i18n("javascripts.note.render_warning")));
372 el1.appendChild(el2);
373 containerDescription.appendChild(el1);
374 } else if (!this.readonly) {
375 el1 = document.createElement("div");
376 el2 = document.createElement("input");
377 el2.setAttribute("type", "button");
378 el2.onclick = function() {
381 el2.value = i18n("javascripts.note.update");
382 el1.appendChild(el2);
383 containerDescription.appendChild(el1);
385 var el_form = document.createElement("form");
386 el_form.onsubmit = function() {
387 if (inputComment.value.match(/^\s*$/))
389 layer.submitComment(id, inputComment.value);
390 layer.hidePopup(popup);
394 el1 = document.createElement("dl");
395 el2 = document.createElement("dt");
396 el2.appendChild(document.createTextNode(i18n("javascripts.note.nickname")));
397 el1.appendChild(el2);
398 el2 = document.createElement("dd");
399 var inputUsername = document.createElement("input");
400 var inputUsername = document.createElement("input");;
401 if (typeof loginName === "undefined") {
402 inputUsername.value = this.username;
404 inputUsername.value = loginName;
405 inputUsername.setAttribute("disabled", "true");
407 inputUsername.className = "username";
408 inputUsername.onkeyup = function() {
409 layer.setUserName(inputUsername.value);
411 el2.appendChild(inputUsername);
412 el3 = document.createElement("a");
413 el3.setAttribute("href", "login");
414 el3.className = "hide_if_logged_in";
415 el3.appendChild(document.createTextNode(i18n("javascripts.note.login")));
417 el1.appendChild(el2);
419 el2 = document.createElement("dt");
420 el2.appendChild(document.createTextNode(i18n("javascripts.note.comment")));
421 el1.appendChild(el2);
422 el2 = document.createElement("dd");
423 var inputComment = document.createElement("textarea");
424 inputComment.setAttribute("cols",40);
425 inputComment.setAttribute("rows",3);
427 el2.appendChild(inputComment);
428 el1.appendChild(el2);
430 el_form.appendChild(el1);
432 el1 = document.createElement("ul");
433 el1.className = "buttons";
434 el2 = document.createElement("li");
435 el3 = document.createElement("input");
436 el3.setAttribute("type", "button");
437 el3.onclick = function() {
438 this.form.onsubmit();
441 el3.value = i18n("javascripts.note.add_comment");
442 el2.appendChild(el3);
443 el1.appendChild(el2);
445 el2 = document.createElement("li");
446 el3 = document.createElement("input");
447 el3.setAttribute("type", "button");
448 el3.onclick = function() {
449 this.form.onsubmit();
454 el3.value = i18n("javascripts.note.close");
455 el2.appendChild(el3);
456 el1.appendChild(el2);
457 el_form.appendChild(el1);
458 containerChange.appendChild(el_form);
460 el1 = document.createElement("div");
461 el2 = document.createElement("input");
462 el2.setAttribute("type", "button");
463 el2.onclick = function(){ displayDescription(); };
464 el2.value = i18n("javascripts.note.cancel");
465 el1.appendChild(el2);
466 containerChange.appendChild(el1);
469 popup.setContentHTML(newContent);
473 * Creates a new note.
475 * @param OpenLayers.LonLat lonlat The coordinates in the API projection.
476 * @param String description
478 createNote: function(lonlat, description) {
479 this.apiRequest("note/create"
480 + "?lat=" + encodeURIComponent(lonlat.lat)
481 + "&lon=" + encodeURIComponent(lonlat.lon)
482 + "&text=" + encodeURIComponent(description)
483 + "&name=" + encodeURIComponent(this.getUserName())
488 * Adds a comment to a note.
491 * @param String comment
493 submitComment: function(id, comment) {
494 this.apiRequest("note/" + encodeURIComponent(id) + "/comment"
495 + "?text=" + encodeURIComponent(comment)
496 + "&name=" + encodeURIComponent(this.getUserName())
501 * Marks a note as fixed.
505 closeNote: function(id) {
506 this.apiRequest("note/" + encodeURIComponent(id) + "/close"
511 * Removes the content of a marker popup (to reduce the amount of
514 * @param OpenLayers.Popup popup
516 resetPopupContent: function(popup) {
518 popup.setContentHTML(document.createElement("div"));
522 * Makes the popup of the given marker visible. Makes sure that
523 * the popup content is created if it does not exist yet.
525 * @param OpenLayers.Feature feature
527 showPopup: function(feature) {
528 var popup = feature.popup;
531 popup = feature.createPopup(true);
533 popup.events.register("close", this, function() {
534 this.resetPopupContent(popup);
538 this.setPopupContent(popup, feature.noteId);
541 this.map.addPopup(popup);
545 if (!popup.visible())
550 * Hides the popup of the given marker.
552 * @param OpenLayers.Feature feature
554 hidePopup: function(feature) {
555 if (feature.popup && feature.popup.visible()) {
556 feature.popup.hide();
557 feature.popup.events.triggerEvent("close");
562 * Is run on the “click” event of a marker in the context of its
563 * OpenLayers.Feature. Toggles the visibility of the popup.
565 markerClick: function(e) {
568 if (feature.popup && feature.popup.visible())
569 feature.layer.hidePopup(feature);
571 feature.layer.showPopup(feature);
573 OpenLayers.Event.stop(e);
577 * Is run on the “mouseover” event of a marker in the context of
578 * its OpenLayers.Feature. Makes the popup visible.
580 markerMouseOver: function(e) {
583 feature.layer.showPopup(feature);
585 OpenLayers.Event.stop(e);
589 * Is run on the “mouseout” event of a marker in the context of
590 * its OpenLayers.Feature. Hides the popup (if it has not been
593 markerMouseOut: function(e) {
596 if (feature.popup && feature.popup.visible())
597 feature.layer.hidePopup(feature);
599 OpenLayers.Event.stop(e);
605 addNote: function(lonlat) {
608 var lonlatApi = lonlat.clone().transform(map.getProjectionObject(), this.apiProjection);
609 var feature = new OpenLayers.Feature(this, lonlat, { icon: this.iconNew.clone(), autoSize: true });
610 feature.popupClass = OpenLayers.Popup.FramedCloud.Notes;
611 var marker = feature.createMarker();
612 marker.feature = feature;
613 this.addMarker(marker);
616 /** Implement a drag and drop for markers */
617 /* TODO: veryfy that the scoping of variables works correctly everywhere */
618 var dragging = false;
619 var dragMove = function(e) {
620 lonlat = map.getLonLatFromViewPortPx(e.xy);
621 lonlatApi = lonlat.clone().transform(map.getProjectionObject(), map.noteLayer.apiProjection);
622 marker.moveTo(map.getLayerPxFromViewPortPx(e.xy));
623 marker.popup.moveTo(map.getLayerPxFromViewPortPx(e.xy));
624 marker.popup.updateRelativePosition();
627 var dragComplete = function(e) {
628 map.events.unregister("mousemove", map, dragMove);
629 map.events.unregister("mouseup", map, dragComplete);
635 marker.events.register("mouseover", this, function() {
636 map.viewPortDiv.style.cursor = "move";
638 marker.events.register("mouseout", this, function() {
640 map.viewPortDiv.style.cursor = "default";
642 marker.events.register("mousedown", this, function() {
644 map.events.register("mousemove", map, dragMove);
645 map.events.register("mouseup", map, dragComplete);
649 var newContent = document.createElement("div");
651 el1 = document.createElement("h3");
652 el1.appendChild(document.createTextNode(i18n("javascripts.note.create_title")));
653 newContent.appendChild(el1);
654 newContent.appendChild(document.createTextNode(i18n("javascripts.note.create_help1")));
655 newContent.appendChild(document.createElement("br"));
656 newContent.appendChild(document.createTextNode(i18n("javascripts.note.create_help2")));
657 newContent.appendChild(document.createElement("br"));
658 newContent.appendChild(document.createElement("br"));
660 var el_form = document.createElement("form");
662 el1 = document.createElement("dl");
663 el2 = document.createElement("dt");
664 el2.appendChild(document.createTextNode(i18n("javascripts.note.nickname")));
665 el1.appendChild(el2);
666 el2 = document.createElement("dd");
667 var inputUsername = document.createElement("input");;
668 if (typeof loginName === 'undefined') {
669 inputUsername.value = this.username;
671 inputUsername.value = loginName;
672 inputUsername.setAttribute('disabled','true');
674 inputUsername.className = "username";
676 inputUsername.onkeyup = function() {
677 this.setUserName(inputUsername.value);
679 el2.appendChild(inputUsername);
680 el3 = document.createElement("a");
681 el3.setAttribute("href","login");
682 el3.className = "hide_if_logged_in";
683 el3.appendChild(document.createTextNode(i18n("javascripts.note.login")));
684 el2.appendChild(el3);
685 el1.appendChild(el2);
686 el2 = document.createElement("br");
687 el1.appendChild(el2);
689 el2 = document.createElement("dt");
690 el2.appendChild(document.createTextNode(i18n("javascripts.note.description")));
691 el1.appendChild(el2);
692 el2 = document.createElement("dd");
693 var inputDescription = document.createElement("textarea");
694 inputDescription.setAttribute("cols",40);
695 inputDescription.setAttribute("rows",3);
696 el2.appendChild(inputDescription);
697 el1.appendChild(el2);
698 el_form.appendChild(el1);
700 el1 = document.createElement("div");
701 el2 = document.createElement("input");
702 el2.setAttribute("type", "button");
703 el2.value = i18n("javascripts.note.report");
704 el2.onclick = function() {
705 layer.createNote(lonlatApi, inputDescription.value);
706 marker.feature = null;
710 el1.appendChild(el2);
711 el2 = document.createElement("input");
712 el2.setAttribute("type", "button");
713 el2.value = i18n("javascripts.note.cancel");
714 el2.onclick = function(){ feature.destroy(); };
715 el1.appendChild(el2);
716 el_form.appendChild(el1);
717 newContent.appendChild(el_form);
719 el2 = document.createElement("hr");
720 el1.appendChild(el2);
721 el2 = document.createElement("a");
722 el2.setAttribute("href","edit");
723 el2.appendChild(document.createTextNode(i18n("javascripts.note.edityourself")));
724 el1.appendChild(el2);
726 feature.data.popupContentHTML = newContent;
727 var popup = feature.createPopup(true);
728 popup.events.register("close", this, function() {
733 marker.popup = popup;
736 CLASS_NAME: "OpenLayers.Layer.Notes"
741 * This class changes the usual OpenLayers.Popup.FramedCloud class by
742 * using a DOM element instead of an innerHTML string as content for
743 * the popup. This is necessary for creating valid onclick handlers
744 * that still work with multiple Notes layer objects.
746 OpenLayers.Popup.FramedCloud.Notes = new OpenLayers.Class(OpenLayers.Popup.FramedCloud, {
751 * See OpenLayers.Popup.FramedCloud.initialize() for
752 * parameters. As fourth parameter, pass a DOM node instead of a
755 initialize: function() {
756 this.displayClass = this.displayClass + " " + this.CLASS_NAME.replace("OpenLayers.", "ol").replace(/\./g, "");
758 var args = new Array(arguments.length);
759 for(var i=0; i<arguments.length; i++)
760 args[i] = arguments[i];
762 // Unset original contentHTML parameter
765 var closeCallback = arguments[6];
767 // Add close event trigger to the closeBoxCallback parameter
768 args[6] = function(e){ if(closeCallback) closeCallback(); else this.hide(); OpenLayers.Event.stop(e); this.events.triggerEvent("close"); };
770 OpenLayers.Popup.FramedCloud.prototype.initialize.apply(this, args);
772 this.events.addEventType("close");
774 this.setContentHTML(arguments[3]);
778 * Like OpenLayers.Popup.FramedCloud.setContentHTML(), but takes a
779 * DOM element as parameter.
781 setContentHTML: function(contentDom) {
782 if(contentDom != null)
783 this.contentDom = contentDom;
785 if(this.contentDiv == null || this.contentDom == null || this.contentDom == this.contentDiv.firstChild)
788 while(this.contentDiv.firstChild)
789 this.contentDiv.removeChild(this.contentDiv.firstChild);
791 this.contentDiv.appendChild(this.contentDom);
793 // Copied from OpenLayers.Popup.setContentHTML():
796 this.registerImageListeners();
801 destroy: function() {
802 this.contentDom = null;
803 OpenLayers.Popup.FramedCloud.prototype.destroy.apply(this, arguments);
806 CLASS_NAME: "OpenLayers.Popup.FramedCloud.Notes"
811 * This global function is executed by the OpenStreetMap API getBugs script.
813 * Each Notes layer adds itself to the putAJAXMarker.layer array. The
814 * putAJAXMarker() function executes the createMarker() method on each
815 * layer in that array each time it is called. This has the
816 * side-effect that notes displayed in one map on a page are already
817 * loaded on the other map as well.
819 function putAJAXMarker(id, lon, lat, text, closed)
821 var comments = text.split(/<hr \/>/);
822 for(var i=0; i<comments.length; i++)
823 comments[i] = comments[i].replace(/"/g, "\"").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
824 putAJAXMarker.notes[id] = [
825 new OpenLayers.LonLat(lon, lat),
829 for(var i=0; i<putAJAXMarker.layers.length; i++)
830 putAJAXMarker.layers[i].createMarker(id);
834 * This global function is executed by the OpenStreetMap API. The
835 * “create note”, “comment” and “close note” scripts execute it to give
836 * information about their success.
838 * In case of success, this function is called without a parameter, in
839 * case of an error, the error message is passed. This is lousy
840 * workaround to make it any functional at all, the OSB API is likely
841 * to be extended later (then it will provide additional information
842 * such as the ID of a created note and similar).
844 function osbResponse(error)
847 alert("Error: "+error);
849 for(var i=0; i<putAJAXMarker.layers.length; i++)
850 putAJAXMarker.layers[i].loadNotes();
853 putAJAXMarker.layers = [ ];
854 putAJAXMarker.notes = { };