888477e7f3094824c3e05a8aedbe49c481d1af0d
[rails.git] / public / javascripts / openstreetbugs.js
1 /*
2         This OpenStreetBugs client is free software: you can redistribute it
3         and/or modify it under the terms of the GNU Affero General Public License
4         as published by the Free Software Foundation, either version 3 of the
5         License, or (at your option) any later version.
6
7         This file is distributed in the hope that it will be useful, but
8         WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9         or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
10         License <http://www.gnu.org/licenses/> for more details.
11 */
12
13 /**
14  * A fully functional OpenStreetBugs layer. See http://openstreetbugs.schokokeks.org/.
15  * Even though the OpenStreetBugs API originally does not intend this, you can create multiple instances of this Layer and add them to different maps (or to one single map for whatever crazy reason) without problems.
16 */
17
18 OpenLayers.Layer.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Layer.Markers, {
19         /**
20          * The URL of the OpenStreetBugs API.
21          * @var String
22         */
23         serverURL : "http://openstreetbugs.schokokeks.org/api/0.1/",
24
25         /**
26          * Associative array (index: bug ID) that is filled with the bugs loaded in this layer
27          * @var String
28         */
29         bugs : { },
30
31         /**
32          * The username to be used to change or create bugs on OpenStreetBugs
33          * @var String
34         */
35         username : "NoName",
36
37         /**
38          * The icon to be used for an open bug
39          * @var OpenLayers.Icon
40         */
41         iconOpen : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/open_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
42
43         /**
44          * The icon to be used for a closed bug
45          * @var OpenLayers.Icon
46         */
47         iconClosed : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/closed_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
48
49         /**
50          * The projection of the coordinates sent by the OpenStreetBugs API.
51          * @var OpenLayers.Projection
52         */
53         apiProjection : new OpenLayers.Projection("EPSG:4326"),
54
55         /**
56          * If this is set to true, the user may not commit comments or close bugs.
57          * @var Boolean
58         */
59         readonly : false,
60
61         /**
62          * When the layer is hidden, all open popups are stored in this array in order to be re-opened again when the layer is made visible again.
63         */
64         reopenPopups : [ ],
65
66         /**
67          * The user name will be saved in a cookie if this isn’t set to false.
68          * @var Boolean
69         */
70         setCookie : true,
71
72         /**
73          * The lifetime of the user name cookie in days.
74          * @var Number
75         */
76         cookieLifetime : 1000,
77
78         /**
79          * The path where the cookie will be available on this server.
80          * @var String
81         */
82         cookiePath : null,
83
84         /**
85          * A URL to append lon=123&lat=123&zoom=123 for the Permalinks.
86          * @var String
87         */
88         permalinkURL : "http://www.openstreetmap.org/",
89
90         /**
91          * A CSS file to be included. Set to null if you don’t need this.
92          * @var String
93         */
94         theme : "http://osm.cdauth.de/map/openstreetbugs.css",
95
96         /**
97          * @param String name
98         */
99         initialize : function(name, options)
100         {
101                 OpenLayers.Layer.Markers.prototype.initialize.apply(this, [ name, OpenLayers.Util.extend({ opacity: 0.7, projection: new OpenLayers.Projection("EPSG:4326") }, options) ]);
102                 putAJAXMarker.layers.push(this);
103                 this.events.addEventType("markerAdded");
104
105                 this.events.register("visibilitychanged", this, this.updatePopupVisibility);
106                 this.events.register("visibilitychanged", this, this.loadBugs);
107
108                 var cookies = document.cookie.split(/;\s*/);
109                 for(var i=0; i<cookies.length; i++)
110                 {
111                         var cookie = cookies[i].split("=");
112                         if(cookie[0] == "osbUsername")
113                         {
114                                 this.username = decodeURIComponent(cookie[1]);
115                                 break;
116                         }
117                 }
118
119                 /* Copied from OpenLayers.Map */
120                 if(this.theme) {
121             // check existing links for equivalent url
122             var addNode = true;
123             var nodes = document.getElementsByTagName('link');
124             for(var i=0, len=nodes.length; i<len; ++i) {
125                 if(OpenLayers.Util.isEquivalentUrl(nodes.item(i).href,
126                                                    this.theme)) {
127                     addNode = false;
128                     break;
129                 }
130             }
131             // only add a new node if one with an equivalent url hasn't already
132             // been added
133             if(addNode) {
134                 var cssNode = document.createElement('link');
135                 cssNode.setAttribute('rel', 'stylesheet');
136                 cssNode.setAttribute('type', 'text/css');
137                 cssNode.setAttribute('href', this.theme);
138                 document.getElementsByTagName('head')[0].appendChild(cssNode);
139             }
140         }
141         },
142
143         /**
144          * Is automatically called when the layer is added to an OpenLayers.Map. Initialises the automatic bug loading in the visible bounding box.
145         */
146         afterAdd : function()
147         {
148                 var ret = OpenLayers.Layer.Markers.prototype.afterAdd.apply(this, arguments);
149
150                 this.map.events.register("moveend", this, this.loadBugs);
151                 this.loadBugs();
152
153                 return ret;
154         },
155
156         /**
157          * At the moment the OSB API responses to requests using JavaScript code. This way the Same Origin Policy can be worked around. Unfortunately, this makes communicating with the API a bit too asynchronous, at the moment there is no way to tell to which request the API actually responses.
158          * This method creates a new script HTML element that imports the API request URL. The API JavaScript response then executes the global functions provided below.
159          * @param String url The URL this.serverURL + url is requested.
160         */
161         apiRequest : function(url) {
162                 var script = document.createElement("script");
163                 script.type = "text/javascript";
164                 script.src = this.serverURL + url + "&nocache="+(new Date()).getTime();
165                 document.body.appendChild(script);
166         },
167
168         /**
169          * Is automatically called when the visibility of the layer changes. When the layer is hidden, all visible popups
170          * are closed and their visibility is saved. When the layer is made visible again, these popups are re-opened.
171         */
172         updatePopupVisibility : function()
173         {
174                 if(this.getVisibility())
175                 {
176                         for(var i=0; i<this.reopenPopups.length; i++)
177                                 this.reopenPopups[i].show();
178                         this.reopenPopups = [ ];
179                 }
180                 else
181                 {
182                         for(var i=0; i<this.markers.length; i++)
183                         {
184                                 if(this.markers[i].feature.popup && this.markers[i].feature.popup.visible())
185                                 {
186                                         this.markers[i].feature.popup.hide();
187                                         this.reopenPopups.push(this.markers[i].feature.popup);
188                                 }
189                         }
190                 }
191         },
192
193         /**
194          * Sets the user name to be used for interactions with OpenStreetBugs.
195         */
196         setUserName : function(username)
197         {
198                 if(this.username == username)
199                         return;
200
201                 this.username = username;
202
203                 if(this.setCookie)
204                 {
205                         var cookie = "osbUsername="+encodeURIComponent(username);
206                         if(this.cookieLifetime)
207                                 cookie += ";expires="+(new Date((new Date()).getTime() + this.cookieLifetime*86400000)).toGMTString();
208                         if(this.cookiePath)
209                                 cookie += ";path="+this.cookiePath;
210                         document.cookie = cookie;
211                 }
212
213                 for(var i=0; i<this.markers.length; i++)
214                 {
215                         if(!this.markers[i].feature.popup) continue;
216                         var els = this.markers[i].feature.popup.contentDom.getElementsByTagName("input");
217                         for(var j=0; j<els.length; j++)
218                         {
219                                 if(els[j].className != "osbUsername") continue;
220                                 els[j].value = username;
221                         }
222                 }
223         },
224
225         /**
226          * Returns the currently set username or “NoName” if none is set.
227         */
228
229         getUserName : function()
230         {
231                 if(this.username)
232                         return this.username;
233                 else
234                         return "NoName";
235         },
236
237         /**
238          * Loads the bugs in the current bounding box. Is automatically called by an event handler ("moveend" event) that is created in the afterAdd() method.
239         */
240         loadBugs : function()
241         {
242                 if(!this.getVisibility())
243                         return true;
244
245                 var bounds = this.map.getExtent();
246                 if(!bounds) return false;
247                 bounds.transform(this.map.getProjectionObject(), this.apiProjection);
248
249                 this.apiRequest("bugs"
250                         + "?bbox="+this.round(bounds.left, 5)
251             + ","+this.round(bounds.bottom, 5)
252                     + ","+this.round(bounds.right, 5)                   
253                         + ","+this.round(bounds.top, 5));
254         },
255
256         /**
257          * Rounds the given number to the given number of digits after the floating point.
258          * @param Number number
259          * @param Number digits
260          * @return Number
261         */
262         round : function(number, digits)
263         {
264                 var factor = Math.pow(10, digits);
265                 return Math.round(number*factor)/factor;
266         },
267
268         /**
269          * Adds an OpenLayers.Marker representing a bug to the map. Is usually called by loadBugs().
270          * @param Number id The bug ID
271         */
272         createMarker: function(id)
273         {
274                 if(this.bugs[id])
275                 {
276                         if(this.bugs[id].popup && !this.bugs[id].popup.visible())
277                                 this.setPopupContent(id);
278                         if(this.bugs[id].closed != putAJAXMarker.bugs[id][2])
279                                 this.bugs[id].destroy();
280                         else
281                                 return;
282                 }
283
284                 var lonlat = putAJAXMarker.bugs[id][0].clone().transform(this.apiProjection, this.map.getProjectionObject());
285                 var comments = putAJAXMarker.bugs[id][1];
286                 var closed = putAJAXMarker.bugs[id][2];
287                 var feature = new OpenLayers.Feature(this, lonlat, { icon: (closed ? this.iconClosed : this.iconOpen).clone(), autoSize: true });
288                 feature.popupClass = OpenLayers.Popup.FramedCloud.OpenStreetBugs;
289                 feature.osbId = id;
290                 feature.closed = closed;
291
292                 var marker = feature.createMarker();
293                 marker.feature = feature;
294                 marker.events.register("click", feature, this.markerClick);
295                 marker.events.register("mouseover", feature, this.markerMouseOver);
296                 marker.events.register("mouseout", feature, this.markerMouseOut);
297                 this.addMarker(marker);
298
299                 this.bugs[id] = feature;
300                 this.events.triggerEvent("markerAdded");
301         },
302
303         /**
304          * Recreates the content of the popup of a marker.
305          * @param Number id The bug ID
306         */
307
308         setPopupContent: function(id) {
309                 if(!this.bugs[id].popup)
310                         return;
311
312                 var el1,el2,el3;
313                 var layer = this;
314
315                 var newContent = document.createElement("div");
316
317                 el1 = document.createElement("h3");
318                 el1.appendChild(document.createTextNode(closed ? OpenLayers.i18n("Fixed Error") : OpenLayers.i18n("Unresolved Error")));
319
320                 el1.appendChild(document.createTextNode(" ["));
321                 el2 = document.createElement("a");
322                 el2.href = "#";
323                 el2.onclick = function(){ layer.map.setCenter(putAJAXMarker.bugs[id][0].clone().transform(layer.apiProjection, layer.map.getProjectionObject()), 15); };
324                 el2.appendChild(document.createTextNode(OpenLayers.i18n("Zoom")));
325                 el1.appendChild(el2);
326                 el1.appendChild(document.createTextNode("]"));
327
328                 if(this.permalinkURL)
329                 {
330                         el1.appendChild(document.createTextNode(" ["));
331                         el2 = document.createElement("a");
332                         el2.href = this.permalinkURL + (this.permalinkURL.indexOf("?") == -1 ? "?" : "&") + "lon="+putAJAXMarker.bugs[id][0].lon+"&lat="+putAJAXMarker.bugs[id][0].lat+"&zoom=15";
333                         el2.appendChild(document.createTextNode(OpenLayers.i18n("Permalink")));
334                         el1.appendChild(el2);
335                         el1.appendChild(document.createTextNode("]"));
336                 }
337                 newContent.appendChild(el1);
338
339                 var containerDescription = document.createElement("div");
340                 newContent.appendChild(containerDescription);
341
342                 var containerChange = document.createElement("div");
343                 newContent.appendChild(containerChange);
344
345                 var displayDescription = function(){
346                         containerDescription.style.display = "block";
347                         containerChange.style.display = "none";
348                         layer.bugs[id].popup.updateSize();
349                 };
350                 var displayChange = function(){
351                         containerDescription.style.display = "none";
352                         containerChange.style.display = "block";
353                         layer.bugs[id].popup.updateSize();
354                 };
355                 displayDescription();
356
357                 el1 = document.createElement("dl");
358                 for(var i=0; i<putAJAXMarker.bugs[id][1].length; i++)
359                 {
360                         el2 = document.createElement("dt");
361                         el2.className = (i == 0 ? "osb-description" : "osb-comment");
362                         el2.appendChild(document.createTextNode(i == 0 ? OpenLayers.i18n("Description") : OpenLayers.i18n("Comment")));
363                         el1.appendChild(el2);
364                         el2 = document.createElement("dd");
365                         el2.className = (i == 0 ? "osb-description" : "osb-comment");
366                         el2.appendChild(document.createTextNode(putAJAXMarker.bugs[id][1][i]));
367                         el1.appendChild(el2);
368                 }
369                 containerDescription.appendChild(el1);
370
371                 if(putAJAXMarker.bugs[id][2])
372                 {
373                         el1 = document.createElement("p");
374                         el1.className = "osb-fixed";
375                         el2 = document.createElement("em");
376                         el2.appendChild(document.createTextNode(OpenLayers.i18n("Has been fixed.")));
377                         el1.appendChild(el2);
378                         containerDescription.appendChild(el1);
379                 }
380                 else if(!this.readonly)
381                 {
382                         el1 = document.createElement("div");
383                         el2 = document.createElement("input");
384                         el2.setAttribute("type", "button");
385                         el2.onclick = function(){ displayChange(); };
386                         el2.value = OpenLayers.i18n("Comment/Close");
387                         el1.appendChild(el2);
388                         containerDescription.appendChild(el1);
389
390                         var el_form = document.createElement("form");
391                         el_form.onsubmit = function(){ if(inputComment.value.match(/^\s*$/)) return false; layer.submitComment(id, inputComment.value); layer.hidePopup(id); return false; };
392
393                         el1 = document.createElement("dl");
394                         el2 = document.createElement("dt");
395                         el2.appendChild(document.createTextNode(OpenLayers.i18n("Nickname")));
396                         el1.appendChild(el2);
397                         el2 = document.createElement("dd");
398                         var inputUsername = document.createElement("input");
399                         inputUsername.value = this.username;
400                         inputUsername.className = "osbUsername";
401                         inputUsername.onkeyup = function(){ layer.setUserName(inputUsername.value); };
402                         el2.appendChild(inputUsername);
403                         el1.appendChild(el2);
404
405                         el2 = document.createElement("dt");
406                         el2.appendChild(document.createTextNode(OpenLayers.i18n("Comment")));
407                         el1.appendChild(el2);
408                         el2 = document.createElement("dd");
409                         var inputComment = document.createElement("textarea");
410                         inputComment.setAttribute("cols",40);                   
411                         el2.appendChild(inputComment);
412                         el1.appendChild(el2);
413                         el_form.appendChild(el1);
414
415                         el1 = document.createElement("ul");
416                         el1.className = "buttons";
417                         el2 = document.createElement("li");
418                         el3 = document.createElement("input");
419                         el3.setAttribute("type", "submit");
420                         el3.value = OpenLayers.i18n("Add comment");
421                         el2.appendChild(el3);
422                         el1.appendChild(el2);
423
424                         el2 = document.createElement("li");
425                         el3 = document.createElement("input");
426                         el3.setAttribute("type", "button");
427                         el3.onclick = function(){ this.form.onsubmit(); layer.closeBug(id); layer.bugs[id].popup.hide(); return false; };
428                         el3.value = OpenLayers.i18n("Mark as fixed");
429                         el2.appendChild(el3);
430                         el1.appendChild(el2);
431                         el_form.appendChild(el1);
432                         containerChange.appendChild(el_form);
433
434                         el1 = document.createElement("div");
435                         el2 = document.createElement("input");
436                         el2.setAttribute("type", "button");
437                         el2.onclick = function(){ displayDescription(); };
438                         el2.value = OpenLayers.i18n("Cancel");
439                         el1.appendChild(el2);
440                         containerChange.appendChild(el1);
441                 }
442
443                 this.bugs[id].popup.setContentHTML(newContent);
444         },
445
446         /**
447          * Creates a new bug.
448          * @param OpenLayers.LonLat lonlat The coordinates in the API projection.
449          * @param String description
450         */
451         createBug: function(lonlat, description) {
452                 this.apiRequest("bug/create"
453                         + "?lat="+encodeURIComponent(lonlat.lat)
454                         + "&lon="+encodeURIComponent(lonlat.lon)
455                         + "&text="+encodeURIComponent(description)
456                         + "&name="+encodeURIComponent(this.getUserName())
457                         + "&format=js"
458                 );
459                 createBugCallBack();
460         },
461
462         /**
463          * Adds a comment to a bug.
464          * @param Number id
465          * @param String comment
466         */
467         submitComment: function(id, comment) {
468                 this.apiRequest("bug/"+encodeURIComponent(id)+"/comment"
469                         + "?text="+encodeURIComponent(comment)
470                         + "&name="+encodeURIComponent(this.getUserName())
471                         + "&format=js"
472                 );
473         },
474
475         /**
476          * Marks a bug as fixed.
477          * @param Number id
478         */
479         closeBug: function(id) {
480                 this.apiRequest("bug/"+encodeURIComponent(id)+"/close"
481                         + "?format=js"
482                 );
483         },
484
485         /**
486          * Removes the content of a marker popup (to reduce the amount of needed resources).
487          * @param Number id
488         */
489         resetPopupContent: function(id) {
490                 if(!this.bugs[id].popup)
491                         return;
492
493                 this.bugs[id].popup.setContentHTML(document.createElement("div"));
494         },
495
496         /**
497          * Makes the popup of the given marker visible. Makes sure that the popup content is created if it does not exist yet.
498          * @param Number id
499         */
500         showPopup: function(id) {
501                 var add = null;
502                 if(!this.bugs[id].popup)
503                 {
504                         add = this.bugs[id].createPopup(true);
505                         add.events.register("close", this, function(){ this.resetPopupContent(id); if(this.bugs[id].osbClicked) this.bugs[id].osbClicked = false; });
506                 }
507                 else if(this.bugs[id].popup.visible())
508                         return;
509
510                 this.setPopupContent(id);
511                 if(add)
512                         this.map.addPopup(add);
513                 this.bugs[id].popup.show();
514                 this.bugs[id].popup.updateSize();
515         },
516
517         /**
518          * Hides the popup of the given marker.
519          * @param Number id
520         */
521         hidePopup: function(id) {
522                 if(!this.bugs[id].popup || !this.bugs[id].popup.visible())
523                         return;
524
525                 this.bugs[id].popup.hide();
526                 this.bugs[id].popup.events.triggerEvent("close");
527         },
528
529         /**
530          * Is run on the “click” event of a marker in the context of its OpenLayers.Feature. Toggles the visibility of the popup.
531         */
532         markerClick: function(e) {
533                 var feature = this; // Context is the feature
534
535                 feature.osbClicked = !feature.osbClicked;
536                 if(feature.osbClicked)
537                         feature.layer.showPopup(feature.osbId);
538                 else
539                         feature.layer.hidePopup(feature.osbId);
540                 OpenLayers.Event.stop(e);
541         },
542
543         /**
544          * Is run on the “mouseover” event of a marker in the context of its OpenLayers.Feature. Makes the popup visible.
545         */
546         markerMouseOver: function(e) {
547                 var feature = this; // Context is the feature
548
549                 feature.layer.showPopup(feature.osbId);
550                 OpenLayers.Event.stop(e);
551         },
552
553         /**
554          * Is run on the “mouseout” event of a marker in the context of its OpenLayers.Feature. Hides the popup (if it has not been clicked).
555         */
556         markerMouseOut: function(e) {
557                 var feature = this; // Context is the feature
558
559                 if(!feature.osbClicked)
560                         feature.layer.hidePopup(feature.osbId);
561                 OpenLayers.Event.stop(e);
562         },
563
564         CLASS_NAME: "OpenLayers.Layer.OpenStreetBugs"
565 });
566
567 /**
568  * An OpenLayers control to create new bugs on mouse clicks on the map. Add an instance of this to your map using
569  * the OpenLayers.Map.addControl() method and activate() it.
570 */
571
572 OpenLayers.Control.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Control, {
573         title : null, // See below because of translation call
574
575         /**
576          * The icon to be used for the temporary markers that the “create bug” popup belongs to.
577          * @var OpenLayers.Icon
578         */
579         icon : new OpenLayers.Icon("http://openstreetbugs.schokokeks.org/client/icon_error_add.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)),
580
581         /**
582          * An instance of the OpenStreetBugs layer that this control shall be connected to. Is set in the constructor.
583          * @var OpenLayers.Layer.OpenStreetBugs
584         */
585         osbLayer : null,
586
587         /**
588          * @param OpenLayers.Layer.OpenStreetBugs osbLayer The OpenStreetBugs layer that this control will be connected to.
589         */
590         initialize: function(osbLayer, options) {
591                 this.osbLayer = osbLayer;
592
593                 this.title = OpenLayers.i18n("Create OpenStreetBug");
594
595                 OpenLayers.Control.prototype.initialize.apply(this, [ options ]);
596
597                 this.events.register("activate", this, function() {
598                         if(!this.osbLayer.getVisibility())
599                                 this.osbLayer.setVisibility(true);
600                 });
601
602                 this.osbLayer.events.register("visibilitychanged", this, function() {
603                         if(this.active && !this.osbLayer.getVisibility())
604                                 this.osbLayer.setVisibility(true);
605                 });
606         },
607
608         destroy: function() {
609                 if (this.handler)
610                         this.handler.destroy();
611                 this.handler = null;
612
613                 OpenLayers.Control.prototype.destroy.apply(this, arguments);
614         },
615
616         draw: function() {
617                 this.handler = new OpenLayers.Handler.Click(this, {'click': this.click}, { 'single': true, 'double': false, 'pixelTolerance': 0, 'stopSingle': false, 'stopDouble': false });
618         },
619
620         /**
621          * Map clicking event handler. Adds a temporary marker with a popup to the map, the popup contains the form to add a bug.
622         */
623         click: function(e) {
624                 if(!this.map) return true;
625
626                 var control = this;
627                 var lonlat = this.map.getLonLatFromViewPortPx(e.xy);
628                 var lonlatApi = lonlat.clone().transform(this.map.getProjectionObject(), this.osbLayer.apiProjection);
629                 var feature = new OpenLayers.Feature(this.osbLayer, lonlat, { icon: this.icon.clone(), autoSize: true });
630                 feature.popupClass = OpenLayers.Popup.FramedCloud.OpenStreetBugs;
631                 var marker = feature.createMarker();
632                 marker.feature = feature;
633                 this.osbLayer.addMarker(marker);
634
635                 var newContent = document.createElement("div");
636                 var el1,el2,el3;
637                 el1 = document.createElement("h3");
638                 el1.appendChild(document.createTextNode(OpenLayers.i18n("Create bug")));
639                 newContent.appendChild(el1);
640
641                 var el_form = document.createElement("form");
642                 el_form.onsubmit = function() { control.osbLayer.createBug(lonlatApi, inputDescription.value); marker.feature = null; feature.destroy(); return false; };
643
644                 el1 = document.createElement("dl");
645                 el2 = document.createElement("dt");
646                 el2.appendChild(document.createTextNode(OpenLayers.i18n("Nickname")));
647                 el1.appendChild(el2);
648                 el2 = document.createElement("dd");
649                 var inputUsername = document.createElement("input");
650                 inputUsername.value = this.osbLayer.username;
651                 inputUsername.className = "osbUsername";
652                 inputUsername.onkeyup = function(){ control.osbLayer.setUserName(inputUsername.value); };
653                 el2.appendChild(inputUsername);
654                 el1.appendChild(el2);
655
656                 el2 = document.createElement("dt");
657                 el2.appendChild(document.createTextNode(OpenLayers.i18n("Bug description")));
658                 el1.appendChild(el2);
659                 el2 = document.createElement("dd");
660                 var inputDescription = document.createElement("textarea");
661                 inputDescription.setAttribute("cols",40);
662                 el2.appendChild(inputDescription);
663                 el1.appendChild(el2);
664                 el_form.appendChild(el1);
665
666                 el1 = document.createElement("div");
667                 el2 = document.createElement("input");
668                 el2.setAttribute("type", "submit");
669                 el2.value = OpenLayers.i18n("Create");
670                 el1.appendChild(el2);
671                 el_form.appendChild(el1);
672                 newContent.appendChild(el_form);
673
674                 feature.data.popupContentHTML = newContent;
675                 var popup = feature.createPopup(true);
676                 popup.events.register("close", this, function(){ feature.destroy(); });
677                 this.map.addPopup(popup);
678                 popup.updateSize();
679         },
680
681         CLASS_NAME: "OpenLayers.Control.OpenStreetBugs"
682 });
683
684
685 /**
686  * This class changes the usual OpenLayers.Popup.FramedCloud class by using a DOM element instead of an innerHTML string as content for the popup.
687  * This is necessary for creating valid onclick handlers that still work with multiple OpenStreetBugs layer objects.
688 */
689
690 OpenLayers.Popup.FramedCloud.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Popup.FramedCloud, {
691         contentDom : null,
692         autoSize : true,
693
694         /**
695          * See OpenLayers.Popup.FramedCloud.initialize() for parameters. As fourth parameter, pass a DOM node instead of a string.
696         */
697         initialize: function() {
698                 this.displayClass = this.displayClass + " " + this.CLASS_NAME.replace("OpenLayers.", "ol").replace(/\./g, "");
699
700                 var args = new Array(arguments.length);
701                 for(var i=0; i<arguments.length; i++)
702                         args[i] = arguments[i];
703
704                 // Unset original contentHTML parameter
705                 args[3] = null;
706
707                 var closeCallback = arguments[6];
708
709                 // Add close event trigger to the closeBoxCallback parameter
710                 args[6] = function(e){ if(closeCallback) closeCallback(); else this.hide(); OpenLayers.Event.stop(e); this.events.triggerEvent("close"); };
711
712                 OpenLayers.Popup.FramedCloud.prototype.initialize.apply(this, args);
713
714                 this.events.addEventType("close");
715
716                 this.setContentHTML(arguments[3]);
717         },
718
719         /**
720          * Like OpenLayers.Popup.FramedCloud.setContentHTML(), but takes a DOM element as parameter.
721         */
722         setContentHTML: function(contentDom) {
723                 if(contentDom != null)
724                         this.contentDom = contentDom;
725
726                 if(this.contentDiv == null || this.contentDom == null || this.contentDom == this.contentDiv.firstChild)
727                         return;
728
729                 while(this.contentDiv.firstChild)
730                         this.contentDiv.removeChild(this.contentDiv.firstChild);
731
732                 this.contentDiv.appendChild(this.contentDom);
733
734                 // Copied from OpenLayers.Popup.setContentHTML():
735                 if(this.autoSize)
736                 {
737                         this.registerImageListeners();
738                         this.updateSize();
739                 }
740         },
741
742         destroy: function() {
743                 this.contentDom = null;
744                 OpenLayers.Popup.FramedCloud.prototype.destroy.apply(this, arguments);
745         },
746
747         CLASS_NAME: "OpenLayers.Popup.FramedCloud.OpenStreetBugs"
748 });
749
750 /**
751  * Necessary improvement to the translate function: Fall back to default language if translated string is not
752  * available (see http://trac.openlayers.org/ticket/2308).
753 */
754
755 OpenLayers.i18n = OpenLayers.Lang.translate = function(key, context) {
756         var message = OpenLayers.Lang[OpenLayers.Lang.getCode()][key];
757         if(!message)
758         {
759                 if(OpenLayers.Lang[OpenLayers.Lang.defaultCode][key])
760                         message = OpenLayers.Lang[OpenLayers.Lang.defaultCode][key];
761                 else
762                         message = key;
763         }
764         if(context)
765                 message = OpenLayers.String.format(message, context);
766         return message;
767 };
768
769 /**
770  * This global function is executed by the OpenStreetBugs API getBugs script.
771  * Each OpenStreetBugs layer adds itself to the putAJAXMarker.layer array. The putAJAXMarker() function executes the createMarker() method
772  * on each layer in that array each time it is called. This has the side-effect that bugs displayed in one map on a page are already loaded
773  * on the other map as well.
774 */
775
776 function putAJAXMarker(id, lon, lat, text, closed)
777 {
778         var comments = text.split(/<hr \/>/);
779         for(var i=0; i<comments.length; i++)
780                 comments[i] = comments[i].replace(/&quot;/g, "\"").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
781         putAJAXMarker.bugs[id] = [
782                 new OpenLayers.LonLat(lon, lat),
783                 comments,
784                 closed
785         ];
786         for(var i=0; i<putAJAXMarker.layers.length; i++)
787                 putAJAXMarker.layers[i].createMarker(id);
788 }
789
790 /**
791  * This global function is executed by the OpenStreetBugs API. The “create bug”, “comment” and “close bug” scripts execute it to give information about their success.
792  * In case of success, this function is called without a parameter, in case of an error, the error message is passed. This is lousy workaround to make it any functional at all, the OSB API is likely to be extended later (then it will provide additional information such as the ID of a created bug and similar).
793 */
794
795 function osbResponse(error)
796 {
797         if(error)
798                 alert("Error: "+error);
799
800         for(var i=0; i<putAJAXMarker.layers.length; i++)
801                 putAJAXMarker.layers[i].loadBugs();
802 }
803
804 putAJAXMarker.layers = [ ];
805 putAJAXMarker.bugs = { };
806
807
808 /* Translations */
809
810 OpenLayers.Lang.en = OpenLayers.Util.extend(OpenLayers.Lang.en, {
811         "Fixed Error" : "Fixed Error",
812         "Unresolved Error" : "Unresolved Error",
813         "Description" : "Description",
814         "Comment" : "Comment",
815         "Has been fixed." : "This error has been fixed already. However, it might take a couple of days before the map image is updated.",
816         "Comment/Close" : "Comment/Close",
817         "Nickname" : "Nickname",
818         "Add comment" : "Add comment",
819         "Mark as fixed" : "Mark as fixed",
820         "Cancel" : "Cancel",
821         "Create OpenStreetBug" : "Create OpenStreetBug",
822         "Create bug" : "Create bug",
823         "Bug description" : "Bug description",
824         "Create" : "Create",
825         "Permalink" : "Permalink",
826         "Zoom" : "Zoom"
827 });
828
829 OpenLayers.Lang.de = OpenLayers.Util.extend(OpenLayers.Lang.de, {
830         "Fixed Error" : "Behobener Fehler",
831         "Unresolved Error" : "Offener Fehler",
832         "Description" : "Beschreibung",
833         "Comment" : "Kommentar",
834         "Has been fixed." : "Der Fehler wurde bereits behoben. Es kann jedoch bis zu einigen Tagen dauern, bis die Kartenansicht aktualisiert wird.",
835         "Comment/Close" : "Kommentieren/Schließen",
836         "Nickname" : "Benutzername",
837         "Add comment" : "Kommentar hinzufügen",
838         "Mark as fixed" : "Als behoben markieren",
839         "Cancel" : "Abbrechen",
840         "Create OpenStreetBug" : "OpenStreetBug melden",
841         "Create bug" : "Bug anlegen",
842         "Bug description" : "Fehlerbeschreibung",
843         "Create" : "Anlegen",
844         "Permalink" : "Permalink",
845         "Zoom" : "Zoom"
846 });
847
848 OpenLayers.Lang.fr = OpenLayers.Util.extend(OpenLayers.Lang.fr, {
849         "Fixed Error" : "Erreur corrigée",
850         "Unresolved Error" : "Erreur non corrigée",
851         "Description" : "Description",
852         "Comment" : "Commentaire",
853         "Has been fixed." : "Cette erreur a déjà été corrigée. Cependant, il peut être nécessaire d'attendre quelques jours avant que l'image de la carte ne soit mise à jour.",
854         "Comment/Close" : "Commenter/Fermer",
855         "Nickname" : "Surnom",
856         "Add comment" : "Ajouter un commentaire",
857         "Mark as fixed" : "Marquer comme corrigé",
858         "Cancel" : "Annuler",
859         "Create OpenStreetBug" : "Créer OpenStreetBug",
860         "Create bug" : "Ajouter un bug",
861         "Bug description" : "Description du bug",
862         "Create" : "Créer",
863         "Permalink" : "Lien permanent",
864         "Zoom" : "Zoom"
865 });
866
867 OpenLayers.Lang.nl = OpenLayers.Util.extend(OpenLayers.Lang.nl, {
868         "Fixed Error" : "Fout verholpen",
869         "Unresolved Error" : "Openstaande fout",
870         "Description" : "Beschrijving",
871         "Comment" : "Kommentaar",
872         "Has been fixed." : "De fout is al eerder opgelost. Het kan echter nog een paar dagen duren voordat het kaartmateriaal geactualiseerd is.",
873         "Comment/Close" : "Bekommentariëren/Sluiten",
874         "Nickname" : "Gebruikersnaam",
875         "Add comment" : "Kommentaar toevoegen",
876         "Mark as fixed" : "Als opgelost aanmerken",
877         "Cancel" : "Afbreken",
878         "Create OpenStreetBug" : "OpenStreetBug melden",
879         "Create bug" : "Bug melden",
880         "Bug description" : "Foutomschrijving",
881         "Create" : "Aanmaken",
882         "Permalink" : "Permalink",
883         "Zoom" : "Zoom"
884 });
885
886 OpenLayers.Lang.it = OpenLayers.Util.extend(OpenLayers.Lang.it, {
887         "Fixed Error" : "Sbaglio coretto",
888         "Unresolved Error" : "Sbaglio non coretto",
889         "Description" : "Descrizione",
890         "Comment" : "Commento",
891         "Has been fixed." : "Questo sbaglio è già coretto. Forse ci metto qualche giorni per aggiornare anche i quadri.",
892         "Comment/Close" : "Commenta/Chiude",
893         "Nickname" : "Nome",
894         "Add comment" : "Aggiunge commento",
895         "Mark as fixed" : "Marca che è coretto",
896         "Cancel" : "Annulla",
897         "Create OpenStreetBug" : "Aggiunge OpenStreetBug",
898         "Create bug" : "Aggiunge un sbaglio",
899         "Bug description" : "Descrizione del sbaglio",
900         "Create" : "Aggiunge",
901         "Permalink" : "Permalink",
902         "Zoom" : "Zoom"
903 });
904
905 OpenLayers.Lang.ro = OpenLayers.Util.extend(OpenLayers.Lang.ro, {
906         "Fixed Error" : "Eroare rezolvată",
907         "Unresolved Error" : "Eroare nerezolvată",
908         "Description" : "Descriere",
909         "Comment" : "Comentariu",
910         "Has been fixed." : "Această eroare a fost rezolvată. Totuși este posibil să dureze câteva zile până când imaginea hărții va fi actualizată.",
911         "Comment/Close" : "Comentariu/Închide",
912         "Nickname" : "Nume",
913         "Add comment" : "Adaugă comentariu",
914         "Mark as fixed" : "Marchează ca rezolvată",
915         "Cancel" : "Anulează",
916         "Create OpenStreetBug" : "Crează OpenStreetBug",
917         "Create bug" : "Adaugă eroare",
918         "Bug description" : "Descrierea erorii",
919         "Create" : "Adaugă",
920         "Permalink" : "Permalink",
921         "Zoom" : "Zoom"
922 });