]> dev.renevier.net Git - syp.git/blobdiff - js/admin.js
fixes CHANGES.txt
[syp.git] / js / admin.js
index 0709878efef7476b55ea339c6e93b5166df4bdd3..71daf2c7cc93ca4ecf6ad97dfa64ed98b9de34a8 100644 (file)
 /* Copyright (c) 2009 Arnaud Renevier, Inc, published under the modified BSD
  * license. */
 
-OpenLayers.Control.SelectDragFeature = 
-    OpenLayers.Class (OpenLayers.Control.SelectFeature, {
-
-    lastPixel: null,
-    dragFeature: null,
+/*
+ * Fix canvas rendering engine race condition. See js/syp.js for more explanation.
+ */
+OpenLayers.Renderer.Canvas.prototype = OpenLayers.Util.extend({
+    needsRedraw: false,
+    imagesLoading: 0,
+}, OpenLayers.Renderer.Canvas.prototype);
+OpenLayers.Renderer.Canvas.prototype.oldRedraw = OpenLayers.Renderer.Canvas.prototype.redraw;
+OpenLayers.Renderer.Canvas.prototype.redraw = function() {
+    if (this.imagesLoading > 0) {
+        this.needsRedraw = true;
+        return;
+    }
+    OpenLayers.Renderer.Canvas.prototype.oldRedraw.apply(this, arguments);
+}
+OpenLayers.Renderer.Canvas.prototype.drawExternalGraphic = function(pt, style) {
+    var img = new Image();
+    img.src = style.externalGraphic;
+       
+    if(style.graphicTitle) {
+        img.title=style.graphicTitle;           
+    }
 
-    startPixel : null,
+    var width = style.graphicWidth || style.graphicHeight;
+    var height = style.graphicHeight || style.graphicWidth;
+    width = width ? width : style.pointRadius*2;
+    height = height ? height : style.pointRadius*2;
+    var xOffset = (style.graphicXOffset != undefined) ?
+        style.graphicXOffset : -(0.5 * width);
+   var yOffset = (style.graphicYOffset != undefined) ?
+       style.graphicYOffset : -(0.5 * height);
+   var opacity = style.graphicOpacity || style.fillOpacity;
+       
+   var context = { img: img, 
+                   x: (pt[0]+xOffset), 
+                   y: (pt[1]+yOffset), 
+                   width: width, 
+                   height: height, 
+                   canvas: this.canvas };
+
+   var self = this;
+   this.imagesLoading++;
+   img.onerror = function() {
+       self.imagesLoading--;
+       if ((self.imagesLoading == 0) && (self.needsRedraw)) {
+           self.needsRedraw = false;
+           self.redraw();
+       }
+   }
+   img.onload = OpenLayers.Function.bind( function() {
+       self.imagesLoading--;
+       if ((self.imagesLoading == 0) && (self.needsRedraw)) {
+           self.needsRedraw = false;
+           self.redraw();
+       } else {
+            this.canvas.drawImage(this.img, this.x, 
+                             this.y, this.width, this.height);
+       }
+   }, context);   
+}
+// drag feature with tolerance
+OpenLayers.Control.SypDragFeature = OpenLayers.Class (OpenLayers.Control.DragFeature, {
+    startPixel: null,
+    dragStart: null,
     pixelTolerance : 0,
     timeTolerance: 300,
-    dragStart: null, 
-
-    onComplete: function (feature, pixel) {},
-    onCancel: function (feature, pixel) {},
-
-    initialize: function (layers, options) {
-        var callbacks = {
-            over: this.overFeature,
-            out: this.outFeature
-        };
-        this.callbacks = OpenLayers.Util.extend(callbacks, this.callbacks);
-
-        OpenLayers.Control.SelectFeature.prototype.initialize.apply(this,
-                                                                    arguments);
-
-        var handlers = {
-            drag: new OpenLayers.Handler.Drag(
-                this, OpenLayers.Util.extend({
-                    down: this.downFeature,
-                    move: this.moveFeature,
-                    up: this.upFeature,
-                    out: this.cancel,
-                    done: this.doneDragging
-                })
-            )
-        };
-        this.handlers = OpenLayers.Util.extend(handlers, this.handlers);
-    },
-
-    doneDragging: function (pixel) {
-        var passesTimeTolerance = 
-            (new Date ()).getTime () > this.dragStart + this.timeTolerance; 
-        var xDiff = this.startPixel.x - pixel.x;
-        var yDiff = this.startPixel.y - pixel.y;
-        var passesPixelTolerance = Math.sqrt(Math.pow(xDiff,2) + Math.pow(yDiff,2))
-                                     > this.pixelTolerance;
-        if(passesTimeTolerance && passesPixelTolerance){
-            this.onComplete(this.dragFeature, pixel);
-        } else {
-            var res = this.map.getResolution ();
-            this.dragFeature.geometry.move(res * (this.startPixel.x - pixel.x),
-                                           res * (pixel.y - this.startPixel.y));
-            this.onCancel(this.dragFeature,pixel);
-        }
-
-        this.layer.drawFeature(this.dragFeature, "select");
-    },
 
-    cancel: function () {
-        this.handlers.drag.deactivate ();
-        this.over = false;
+    downFeature: function(pixel) {
+        OpenLayers.Control.DragFeature.prototype.downFeature.apply(this, arguments);
+        this.dragStart = (new Date()).getTime(); 
+        this.startPixel = pixel; 
     },
 
-    clickFeature: function (feature) {
-        OpenLayers.Control.SelectFeature.prototype.clickFeature.apply(this,
-                                                                      arguments);
-        if (Admin.Utils.indexOf(this.layer.selectedFeatures, feature) == -1) {
-            this.dragDisable ();
+    doneDragging: function(pixel) {
+        OpenLayers.Control.DragFeature.prototype.doneDragging.apply(this, arguments);
+        // Check tolerance. 
+        var passesTimeTolerance =  
+                    (new Date()).getTime() > this.dragStart + this.timeTolerance; 
+
+        var xDiff = this.startPixel.x - pixel.x; 
+        var yDiff = this.startPixel.y - pixel.y; 
+
+        var passesPixelTolerance =  
+        Math.sqrt(Math.pow(xDiff,2) + Math.pow(yDiff,2)) > this.pixelTolerance; 
+
+        if(passesTimeTolerance && passesPixelTolerance){ 
+            this.onComplete(this.feature, pixel);    
+        } else { 
+            var feature = this.feature; 
+            var res = this.map.getResolution(); 
+            this.feature.geometry.move(res * (this.startPixel.x - this.lastPixel.x), 
+                    res * (this.lastPixel.y - this.startPixel.y)); 
+            this.layer.drawFeature(this.feature); 
         }
+        this.layer.drawFeature(this.feature, "select");
     },
 
-    upFeature: function (pixel) {
-    },
-
-    moveFeature: function (pixel) {
-        if (Admin.Utils.indexOf(this.layer.selectedFeatures,
-                                this.dragFeature) != -1) {
-            var res = this.map.getResolution ();
-            this.dragFeature.geometry.move(res * (pixel.x - this.lastPixel.x),
-                                   res * (this.lastPixel.y - pixel.y));
-            this.layer.drawFeature(this.dragFeature, "temporary");
-            this.lastPixel = pixel;
-        }
-    },
-
-    downFeature: function (pixel) {
-        this.handlers.feature.down = pixel;
-
-        if (Admin.Utils.indexOf(this.layer.selectedFeatures,
-                                this.dragFeature) != -1) {
-            this.dragStart = (new Date ()).getTime ();
-            this.startPixel = pixel;
-            this.lastPixel = pixel;
-        } 
-    },
-
-    deactivate: function () {
-        this.dragDisable ();
-        return OpenLayers.Control.SelectFeature.prototype.deactivate.apply(
-            this, arguments
-        );
+    moveFeature: function(pixel) {
+        OpenLayers.Control.DragFeature.prototype.moveFeature.apply(this, arguments);
+        this.layer.drawFeature(this.feature, "temporary");
     },
 
     overFeature: function (feature) {
-        if (Admin.Utils.indexOf(this.layer.selectedFeatures, feature) != -1) {
-            this.dragEnable(feature);
-        }
-        OpenLayers.Control.SelectFeature.prototype.overFeature.apply(this,
-                                                                     arguments);
-    },
-
-    outFeature: function (feature) {
-        this.dragDisable ();
-        OpenLayers.Control.SelectFeature.prototype.outFeature.apply(this,
-                                                                    arguments);
-    },
-
-    select: function (feature) {
-        this.dragEnable(feature);
-        OpenLayers.Control.SelectFeature.prototype.select.apply(this,
-                                                                arguments);
-    },
-
-    dragDisable: function () {
-        this.over = false;
-        if(!this.handlers.drag.dragging) {
-            this.handlers.drag.deactivate ();
-            OpenLayers.Element.removeClass(
-                this.map.viewPortDiv, this.displayClass + "Over"
-            );
-            this.dragFeature = null;
-        }
-    },
-
-    dragEnable: function (feature) {
-        if(!this.handlers.drag.dragging) {
-            this.dragFeature = feature;
-            this.handlers.drag.activate ();
-            this.over = true;
-            OpenLayers.Element.addClass(this.map.viewPortDiv, this.displayClass + "Over");
-        } else {
-            if(this.dragFeature.id == feature.id) {
-                this.over = true;
-            } else {
-                this.over = false;
-            }
+        // can only drag and drop currently selected feature
+        if (feature != Admin.currentFeature) {
+            return;
         }
+        OpenLayers.Control.DragFeature.prototype.overFeature.apply(this, arguments);
     },
 
-    setMap: function (map) {
-        this.handlers.drag.setMap(map);
-        OpenLayers.Control.SelectFeature.prototype.setMap.apply(this, arguments);
-    },
-
-    CLASS_NAME: "OpenLayers.Control.SelectDragFeature"
+    CLASS_NAME: "OpenLayers.Control.SypDragFeature"
 });
 
 var Admin = {
-    Settings: {
-        MARKER_ICON: "openlayers/img/marker-blue.png",
-        MARKER_ICON_HEIGHT: 25,
-        MARKER_SELECT_ICON: "openlayers/img/marker-green.png",
-        MARKER_SELECT_ICON_HEIGHT: 25,
-        MARKER_TEMPORARY_ICON: "openlayers/img/marker-gold.png",
-        MARKER_TEMPORARY_ICON_HEIGHT: 25
+    Markers: {
+        ICON: "media/marker-normal.png",
+        SELECT_ICON: "media/marker-selected.png",
+        TEMPORARY_ICON: "media/marker-temp.png",
+        HEIGHT: 25
     },
 
     map: null,
     baseLayer: null,
     dataLayer: null,
-    selectControl: null,
-    clickControl: null,
+    selFeatureControl: null,
+    moveFeatureControl: null,
+    addFeatureControl: null,
+
+    currentFeature: null,
+    currentFeatureLocation: null,
 
     init: function () {
         this.map = new OpenLayers.Map ("map", {
@@ -184,385 +143,293 @@ var Admin = {
          });
 
          this.baseLayer = this.createBaseLayer ();
-         this.dataLayer = this.createDataLayer ();
-         this.map.addLayers([this.baseLayer, this.dataLayer]);
-
-         this.selectControl = this.createSelectDragControl ();
-         this.map.addControl(this.selectControl);
-         this.selectControl.activate ();
-
-         this.clickControl = this.createClickControl ();
-         this.map.addControl(this.clickControl);
-
-         var centerBounds = new OpenLayers.Bounds ();
-
-         var mapProj = this.map.getProjectionObject();
-         var sypOrigProj = new OpenLayers.Projection("EPSG:4326");
-
-         var bottomLeft = new OpenLayers.LonLat(sypOrig[0],sypOrig[1]);
-         bottomLeft = bottomLeft.transform(sypOrigProj, mapProj);
-         var topRight = new OpenLayers.LonLat(sypOrig[2],sypOrig[3])
-         topRight = topRight.transform(sypOrigProj, mapProj);
+         this.map.addLayer(this.baseLayer);
+
+         this.map.setCenter(new OpenLayers.LonLat(0, 0), 0);
+         if (sypSettings.loggedUser) {
+            this.dataLayer = this.createDataLayer (sypSettings.loggedUser);
+            this.map.addLayer(this.dataLayer);
+            this.reset();
+         }
+    },
 
-         centerBounds.extend(bottomLeft);
-         centerBounds.extend(topRight);
-         this.map.zoomToExtent(centerBounds);
+    reset: function() {
+        this.addFeatureControl.deactivate();
+        this.moveFeatureControl.deactivate();
+        this.selFeatureControl.activate();
+        this.checkForFeatures();
+        $("#newfeature_button").show().val(SypStrings.AddItem);
+        $("#newfeature_button").unbind("click").click(function () {
+            Admin.addNewFeature();
+        });
     },
 
     createBaseLayer: function () {
         return new OpenLayers.Layer.OSM("OSM");
     },
 
-    createDataLayer: function () {
+    createDataLayer: function (user) {
         var styleMap = new OpenLayers.StyleMap (
                         {"default": {
-                             externalGraphic: this.Settings.MARKER_ICON,
-                             graphicHeight: this.Settings.MARKER_ICON_HEIGHT
-                                                  || 32 
+                             externalGraphic: this.Markers.ICON,
+                             graphicHeight: this.Markers.HEIGHT || 32 
                                 },
                          "temporary": { 
-                             externalGraphic: this.Settings.MARKER_TEMPORARY_ICON,
-                             graphicHeight: this.Settings.MARKER_TEMPORARY_ICON_HEIGHT
-                                                  || 32 
+                             externalGraphic: this.Markers.TEMPORARY_ICON,
+                             graphicHeight: this.Markers.HEIGHT || 32 
                          },
                          "select": { 
-                             externalGraphic: this.Settings.MARKER_SELECT_ICON,
-                             graphicHeight: this.Settings.MARKER_SELECT_ICON_HEIGHT
-                                                  || 32 
+                             externalGraphic: this.Markers.SELECT_ICON,
+                             graphicHeight: this.Markers.HEIGHT || 32 
                     }});
 
-        var layer = new OpenLayers.Layer.GML("KML", "items.php", 
+        var layer = new OpenLayers.Layer.GML("KML", "items.php?from_user=" + encodeURIComponent(user),
            {
             styleMap: styleMap,
             format: OpenLayers.Format.KML, 
             projection: this.map.displayProjection,
             eventListeners: { scope: this,
-                loadend: this.checkForFeatures,
-                featureremoved: this.checkForFeatures,
-                featureadded: this.checkForFeatures
+                loadend: this.dataLayerEndLoad
             }
        });
 
+        // controls
+        this.selFeatureControl = this.createSelectFeatureControl(layer)
+        this.map.addControl(this.selFeatureControl);
+        this.moveFeatureControl = this.createMoveFeatureControl(layer)
+        this.map.addControl(this.moveFeatureControl);
+        this.addFeatureControl = this.createNewfeatureControl();
+        this.map.addControl(this.addFeatureControl);
+
         return layer;
     },
 
-    createSelectDragControl: function () {
-        var control = new OpenLayers.Control.SelectDragFeature(
-                this.dataLayer, {
-                        onSelect: this.onFeatureSelect,
-                        onUnselect: this.onFeatureUnselect,
-                        onComplete: OpenLayers.Function.bind(this.onDragComplete.action,
-                                                             this.onDragComplete),
-                        toggle: true,
-                        clickout: false
-                               });
+    createMoveFeatureControl: function (layer) {
+        var control = new OpenLayers.Control.SypDragFeature(
+                layer, {
+                         });
         return control;
     },
 
-    checkForFeatures: function () {
-        var features = this.dataLayer.features;
-        if (features.length == 0) {
-            $("#modify_howto").css("visibility", "hidden");
-        } else {
-            $("#modify_howto").css("visibility", "visible");
-        }
+    createSelectFeatureControl: function (layer) {
+        var control = new OpenLayers.Control.SelectFeature(
+                layer, {
+                        onSelect: OpenLayers.Function.bind(this.onFeatureSelect, this)
+                         });
+        return control;
     },
 
-    createClickControl: function () {
+    createNewfeatureControl: function () {
         var control = new OpenLayers.Control ();
-        var handler = new OpenLayers.Handler.Click(control, {});
+        var handler = new OpenLayers.Handler.Click(control, {
+                'click': OpenLayers.Function.bind(FeatureMgr.add, FeatureMgr)
+            });
         control.handler = handler;
         return control;
     },
 
-    addMarkerNewImage: function (imgurl) {
-        return function (evt) {
-            FeatureMgr.itemDeleter.lock(imgurl);
-            var pos = this.map.getLonLatFromViewPortPx(evt.xy);
-            var point = new OpenLayers.Geometry.Point(pos.lon, pos.lat);
-            var desc = '<img src="' + imgurl + '">';
-            var feature = new OpenLayers.Feature.Vector(point, {
-                                                               name: '', 
-                                                               description: desc
-                                                               });
-            Admin.dataLayer.addFeatures([feature]);
-            Admin.selectControl.activate ();
-
-            // When adding a feature and then selecting it, we set render style
-            // to "default" and right after, we set it to "select". When
-            // rendering backend is SVG, that triggers a modification of href
-            // attribute right after having inserted image with a different
-            // href. Unfortunately, webkit does not like that (see
-            // webkit#26392). That's why we need to wrap selection in a
-            // timeout.
-            var renderer = Admin.dataLayer.renderer;
-            if (renderer && renderer.CLASS_NAME == "OpenLayers.Renderer.SVG") {
-                window.setTimeout(function () { 
-                    Admin.selectControl.select(feature);
-                    Admin.saveFeature(feature);
-                } ,0);
-            } else {
-                Admin.selectControl.select(feature);
-                Admin.saveFeature(feature);
-            }
-            $("#addphoto_button").val("ajouter une autre image");
+    onFeatureSelect: function (feature) {
+        this.showEditor(feature);
+        FeatureMgr.reset();
+        this.selFeatureControl.deactivate();
+        this.moveFeatureControl.activate();
+    },
+
+    closeEditor: function() {
+        if ($("#editor").css("display") == "none") {
+            return;
+        }
+        if (this.currentFeature && this.currentFeature.layer) {
+            this.selFeatureControl.unselect(this.currentFeature);
         }
+        this.currentFeature = null;
+        this.currentFeatureLocation = null;
+        $("#img").removeAttr('src');
+        $("#img").parent().html($("#img").parent().html());
+        $("#img").parent().show();
+        $("#title, #description").val("");
+        $("#editor").hide();
+        // do it once before hidding and once after hidding to work in all cases
+        $("#title, #description").val(""); 
+        $("#image_file").parent().html($("#image_file").parent().html());
+        $(document).unbind("keydown");
+        this.checkForFeatures();
+        this.reset();
     },
 
-    onFeatureSelect: function (feature) {
-        Admin.closeNewimage();
+    showEditor: function (feature) {
+        $("#newfeature_button").hide();
+        userMgr.close();
 
-        $("#img").attr('src', '');
+        if (feature.fid) {
+            $("#delete").show();
+        } else {
+            $("#delete").hide();
+        }
+        $(document).unbind("keydown").keydown(function(e) { 
+            if (e.keyCode == 27) {
+                Admin.cancelCurrentFeature()
+                e.preventDefault();
+            }
+        });
+        this.currentFeature = feature;
+        this.currentFeatureLocation = new OpenLayers.Pixel(feature.geometry.x, feature.geometry.y);
         $("#editor").show();
-        $("#features_success").css("visibility", "hidden");
-
-        // we use the real onclick method otherwise: jquery method would not
-        // execute because of input.change method
-        $("#deletephoto_button").get(0).onclick = function () {
-            Admin.clearChangeTimeouts();
-            var imgurl = $("#img").attr("src");
-            Admin.deleteFeature(feature, imgurl);
-        };
-
-        $("#title_input").val(feature.attributes.name);
-
+        $("#instructions").text(SypStrings.DragDropHowto);
+        $("#title").val(feature.attributes.name);
         var fullDesc = $(feature.attributes.description).parent();
-        $("#desc_input").val(fullDesc.find('p').text());
-        $("#img").attr('src', fullDesc.find('img').attr('src'));
+        $("#description").val(fullDesc.find('p').text());
+        var src = fullDesc.find('img').attr('src');
+        if (src) {
+            $("#img").parent().show();
+            $("#img").attr('src', src);
+            $("#image_file").parent().hide();
+            $("#image_delete").show();
+        } else {
+            $("#img").parent().hide();
+            $("#image_file").parent().show();
+            $("#image_delete").hide();
+        }
+        $("#title").select().focus(); 
+    },
 
-        $(".input").each(function () {
-            this.curVal = this.value;
-        });
+    dataLayerEndLoad: function() {
+        // only set zoom extent once
+        this.dataLayer.events.unregister('loadend', this, this.dataLayerEndLoad);
+        this.dataLayer.events.register('loadend', this, this.checkForFeatures);
 
-         // Change event happens if an input has change if we leave that field.
-         // But we want data to be saved even if user does not blur input field
-         // (for example, he types something in the box and then does not touch
-         // it's computer). So, every 60 seconds, we check if value of input
-         // has changed. More precisely, every 60 seconds, we wait 3 seconds to
-         // see if input value is still changing and if not, it probably means
-         // user is not modifying data anymore. In that case, we save.
+        if (!this.checkForFeatures()) {
+            return;
+        }
 
-        $(".input").focus(function (evt) {
-            var self = this;
-            this.curVal = this.value;
-            this.checkTimer = window.setInterval(function () {
-                 if (self.value != self.curVal) {
-                    $("#features_success").css("visibility", "hidden");
-                    var newVal = self.value;
-                     this.saveTimeout = window.setTimeout(function () {
-                         if (self.value == newVal) {
-                            self.curVal = self.value;
-                            Admin.saveFeature(feature);
-                         }
-                     }, 3 * 1000);
-                 }
-            }, 6 * 1000);
-        })
-
-        $(".input").blur(function (evt) {
-            if (this.checkTimer) {
-                window.clearInterval(this.checkTimer);
-                this.checkTimer = null;
-            }
-            if (this.saveTimeout) {
-                window.clearTimeout(this.saveTimeout);
-                this.saveTimeout = null;
-            }
-        });
+        var map = this.map;
+        var orig = this.Utils.mbr (this.dataLayer);
+        var centerBounds = new OpenLayers.Bounds();
 
-        $(".input").change(function (evt) {
-            if (this.curVal != this.value) {
-                $("#features_success").css("visibility", "hidden");
-                this.curVal = this.value;
-                Admin.saveFeature(feature);
-            }
-        });
+        var mapProj = map.getProjectionObject();
+        var sypOrigProj = new OpenLayers.Projection("EPSG:4326");
 
-        $("#title_input").blur()
-        $("#title_input").focus()
-        $("#title_input").select()
+        var bottomLeft = new OpenLayers.LonLat(orig[0],orig[1]);
+        bottomLeft = bottomLeft.transform(sypOrigProj, mapProj);
+        var topRight = new OpenLayers.LonLat(orig[2],orig[3])
+        topRight = topRight.transform(sypOrigProj, mapProj);
 
-        $("#dragdrop_howto").css("visibility", "visible");
+        centerBounds.extend(bottomLeft);
+        centerBounds.extend(topRight);
+        map.zoomToExtent(centerBounds);
     },
 
-    onFeatureUnselect: function (feature) {
-        Admin.closeEditor();
-        $("#features_connect_error").hide();
-        $("#deletephoto_button").get(0).onclick = null;
-
-        // if user unselects feature, save modifications without waiting
-        var needsSaving = Admin.onDragComplete.timeout ? true: false;
-        $(".input").each(function () {
-            if (this.value != this.curVal) {
-                needsSaving = true;
-            }
-            if (this.checkTimer) {
-                window.clearInterval(this.checkTimer);
-                this.checkTimer = null;
-            }
-            if (this.saveTimeout) {
-                window.clearTimeout(this.saveTimeout);
-                this.saveTimeout = null;
-            }
-        });
-
-        if (needsSaving) {
-            $("#features_success").css("visibility", "hidden");
-            Admin.saveFeature(feature);
+    checkForFeatures: function () {
+        var features = this.dataLayer.features;
+        if (features.length != 0) {
+            $("#instructions").text(SypStrings.SelectHowto);
         }
-
-        window.clearTimeout(Admin.onDragComplete.timeout);
-        Admin.onDragComplete.timeout = null;
+        return !!features.length;
     },
 
     addNewFeature: function () {
-        this.selectControl.unselectAll();
-        $("#features_success").css("visibility", "hidden");
-        $("#features_connect_error").hide();
-        $("#addphoto_button").attr("disabled", "disabled");
-        $("#newimage").show();
-        $("#file_form").show();
-        $("#file_form").get(0).reset();
-        $("#newimage_input").change(function () {
-            $("#newimage_error").hide();
-            if (OpenLayers.Util.getBrowserName() == "msie") {
-                if ($("#file_form").find('input[type="submit"]').length == 0) {
-                    $("#file_form").append(
-                        '<div class="center" style="margin-top: 15px">' + 
-                        '<input type="submit" class="center"><div>');
-                    $('#file_form > div > input[type="submit"]').focus();
-                    $("#file_form").one("submit", function () {
-                        $("#file_form").find('input[type="submit"]').
-                                        parent().remove();
-                        $("#newimage_throbber").css("visibility", "visible");
-                    });
-                }
-            } else {
-                $("#file_form").submit();
-                $("#newimage_throbber").css("visibility", "visible");
-            }
-            $("#fileframe").one("load", FeatureMgr.fileFrameLoad);
-        });
-        // works in webkit and in ie
-        // XXX: we want to call
-        // the real click method of newimage_input, not jquery click method
-        // because jquery click method prevents change listener to be called
-        // click event is running.
-        if (OpenLayers.Util.getBrowserName() != "msie")  {
-            // XXX: in ie, it prevents submiting form
-            $("#newimage_input").get(0).click();
-        }
-        // works in opera
-        $("#newimage_input").focus(); 
-    },
+        userMgr.close();
 
-    closeNewimage: function () {
-        if ($("#newimage").css("display") == "none") {
-            return;
+        function cancel() {
+            $(document).unbind("keydown");
+            Admin.reset()
         }
-        $("#newimage_input").unbind('change');
-        $("#fileframe").unbind('load');
-        $("#addphoto_button").removeAttr("disabled");
-        $("#newimage_error").hide();
-        $("#newimage_throbber").css("visibility", "hidden").show();
-        $("#newimage_input").val('');
-        FeatureMgr.itemDeleter.add($("#newimage_preview").attr("src"));
-        $("#newimage").hide();
-        $("#newimage_preview").removeAttr("src");
-        $("#newimage_preview").hide();
-        $("#modify_howto").css("visibility", "visible");
-        $("#newimage_warn").hide();
-        this.clickControl.handler.callbacks.click = null;
-        this.clickControl.deactivate();
-        this.selectControl.activate();
-    },
-
-    closeEditor: function () {
-        $("#editor").hide();
-        $(".input").unbind('change');
-        $(".input").unbind('focus');
-        $(".input").unbind('blur');
-        $("#dragdrop_howto").css("visibility", "hidden");
-    },
-
-    clearChangeTimeouts: function () {
-        $(".input").each(function (){
-            if (this.checkTimer) {
-                window.clearInterval(this.checkTimer);
-                this.checkTimer = null;
-            }
-            if (this.saveTimeout) {
-                window.clearTimeout(this.saveTimeout);
-                this.saveTimeout = null;
+        $(document).unbind("keydown").keydown(function(e) { 
+            if (e.keyCode == 27) {
+                e.preventDefault();
+                cancel();
             }
         });
-    },
 
-    onDragComplete: {
-        timeout: null,
-        // we wait 3 seconds before saving in case user drags marker again
-        action: function (feature, pixel) {
-            if (this.timeout) {
-                window.clearTimeout(this.timeout);
-            }
-            var self = this;
-            this.timeout = window.setTimeout(function () {
-                self.timeout = null;
-                Admin.saveFeature(feature);
-            }, 3000);
-        }
-    },
+        $("#newfeature_button").val(SypStrings.Cancel);
+        $("#newfeature_button").unbind("click").click(cancel);
 
-    saveFeature: function (feature) {
-        var imgurl = $("#img").attr("src");
-        var title = $("#title_input").val();
-        var description = $("#desc_input").val();
-
-        feature.attributes.name = this.Utils.escapeHTML(title);
-        feature.attributes.description = "<p>" + 
-                                          this.Utils.escapeHTML(description) +
-                                          "</p>" +
-                                          "<img class=\"" +
-                                          $("#img").attr("class") +
-                                          "\"" +
-                                          " src=\"" + imgurl + "\">";
-
-        var x = feature.geometry.x;
-        var y = feature.geometry.y;
-
-        var mapProj = feature.layer.map.getProjectionObject();
-        var lonlat = new OpenLayers.LonLat(x, y).
-                             transform(mapProj,
-                                       new OpenLayers.Projection("EPSG:4326"));
-
-        FeatureMgr.itemDeleter.unlock(imgurl);
-        FeatureMgr.saveFeature(feature, imgurl, title, description, lonlat);
+        $("#instructions").text(SypStrings.AddHowto);
+        this.selFeatureControl.deactivate();
+        this.addFeatureControl.activate();
+        FeatureMgr.reset();
     },
 
-    deleteFeature: function (feature, imgurl) {
-        Admin.dataLayer.destroyFeatures([feature]);
-        FeatureMgr.deleteFeature(imgurl);
+    cancelCurrentFeature: function() {
+        if (AjaxMgr.running) {
+            return false;
+        }
+        var feature = this.currentFeature;
+        if (feature) {
+            if (feature.fid) {
+                FeatureMgr.move (feature, this.currentFeatureLocation);
+            } else {
+                this.dataLayer.removeFeatures([feature]);
+            }
+        }
+        this.closeEditor();
+        return true;
     },
 
     reloadLayer: function (layer) {
         layer.destroyFeatures();
         layer.loaded = false;
         layer.loadGML();
-        this.closeEditor();
-    },
-
-    connectErrorMsg: function (textStatus) {
-        if (textStatus == undefined) {
-            textStatus = "inconnue";
-        }
-        return "Une erreur de type " +
-                textStatus + 
-                " est survenue lors de la connexion au serveur." + 
-                " Veuillez réessayer ou contacter l'administrateur du site.";
     },
 
     Utils: {
+        /* minimum bounds rectangle containing all feature locations.
+         * FIXME: if two features are close, but separated by 180th meridian,
+         * their mbr will span the whole earth. Actually, 179° lon and -170°
+         * lon are considerated very near.
+         */
+        mbr: function (layer) {
+            var features = [];
+            var map = layer.map;
+
+            var mapProj = map.getProjectionObject();
+            var sypOrigProj = new OpenLayers.Projection("EPSG:4326");
+
+            for (var i =0; i < layer.features.length; i++) {
+                if (layer.features[i].cluster) {
+                    features = features.concat(layer.features[i].cluster);
+                } else {
+                    features = features.concat(layer.features);
+                }
+            }
+
+            var minlon = 180;
+            var minlat = 88;
+            var maxlon = -180;
+            var maxlat = -88;
+
+            if (features.length == 0) {
+                // keep default values
+            } else if (features.length == 1) {
+                // in case there's only one feature, we show an area of at least 
+                // 4 x 4 degrees
+                var pos = features[0].geometry.getBounds().getCenterLonLat().clone();
+                var lonlat = pos.transform(mapProj, sypOrigProj);
+
+                minlon = Math.max (lonlat.lon - 2, -180);
+                maxlon = Math.min (lonlat.lon + 2, 180);
+                minlat = Math.max (lonlat.lat - 2, -90);
+                maxlat = Math.min (lonlat.lat + 2, 90);
+            } else {
+                for (var i = 0; i < features.length; i++) {
+                    var pos = features[i].geometry.getBounds().getCenterLonLat().clone();
+                    var lonlat = pos.transform(mapProj, sypOrigProj);
+                    minlon = Math.min (lonlat.lon, minlon);
+                    minlat = Math.min (lonlat.lat, minlat);
+                    maxlon = Math.max (lonlat.lon, maxlon);
+                    maxlat = Math.max (lonlat.lat, maxlat);
+                }
+            }
+
+            return [minlon, minlat, maxlon, maxlat];
+
+        },
+
+
         escapeHTML: function (str) {
+            if (!str) {
+                return "";
+            }
             return str.
              replace(/&/gm, '&amp;').
              replace(/'/gm, '&#39;').
@@ -585,291 +452,767 @@ var Admin = {
     }
 }
 
+var FeatureMgr = {
+    reset: function() {
+        this.commError("");
+    },
+
+    add: function(evt) {
+        var map = Admin.map;
+        var pos = map.getLonLatFromViewPortPx(evt.xy);
+        feature = this.update (null, pos, "", "", "");
+        Admin.addFeatureControl.deactivate();
+        Admin.selFeatureControl.select(feature);
+    },
+
+    move: function (feature, aLocation) {
+        if (!feature || !aLocation) {
+            return;
+        }
+        var curLoc = feature.geometry;
+        feature.geometry.move(aLocation.x - curLoc.x, aLocation.y - curLoc.y);
+        feature.layer.drawFeature(feature); 
+    },
+
+    update: function(feature, lonlat, imgurl, title, description) {
+        var point = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
+        if (!feature) {
+            feature = new OpenLayers.Feature.Vector(point);
+            Admin.dataLayer.addFeatures([feature]);
+        } else {
+            this.move (feature, point);
+        }
+        feature.attributes.name = title;
+        feature.attributes.description = "<p>" + Admin.Utils.escapeHTML(description) + "</p>"
+                                + "<img src=\"" + imgurl + "\">"
+        return feature;
+    },
+
+    del: function (feature) {
+        var form = $("#feature_delete");
+        form.find('input[name="fid"]').val(feature.fid);
+        AjaxMgr.add({
+            form: form,
+            oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
+            throbberid: "editor_throbber"
+        });
+    },
+
+    save: function (feature) {
+        var x = feature.geometry.x;
+        var y = feature.geometry.y;
+
+        var mapProj = feature.layer.map.getProjectionObject();
+        var lonlat = new OpenLayers.LonLat(x, y).
+                                    transform(mapProj,
+                                              new OpenLayers.Projection("EPSG:4326"));
+        var form = $("#feature_update");
+        form.find('input[name="lon"]').val(lonlat.lon);
+        form.find('input[name="lat"]').val(lonlat.lat);
+        form.find('input[name="fid"]').val(feature.fid);
+        form.find('input[name="keep_img"]').val(
+            $("#img").attr("src") ? "yes": "no"
+        );
+
+        if (feature.fid) {
+            form.find('input[name="request"]').val("update");
+        } else {
+            form.find('input[name="request"]').val("add");
+        }
+        AjaxMgr.add({
+            form: form,
+            oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
+            throbberid: "editor_throbber"
+        });
+    },
+
+    ajaxReply: function (data) {
+        if (!data) {
+            this.commError(SypStrings.ServerError);
+            return;
+        }
+
+        var xml = new OpenLayers.Format.XML().read(data);
+        if (!xml.documentElement) {
+            this.commError(SypStrings.UnconsistentError);
+            $("title").focus();
+            return;
+        }
+
+        switch (xml.documentElement.nodeName.toLowerCase()) {
+            case "error":
+                switch (xml.documentElement.getAttribute("reason")) {
+                    case "unauthorized":
+                        pwdMgr.reset();
+                        $("#cookie_warning").show();
+                        this.reset();
+                        Admin.cancelCurrentFeature();
+                        Admin.reset();
+                        userMgr.uninit();
+                    break;
+                    case "server":
+                        this.commError(SypStrings.ServerError);
+                        $("title").focus();
+                    break;
+                    case "unreferenced":
+                        this.commError(SypStrings.UnreferencedError);
+                        Admin.reloadLayer(Admin.dataLayer);
+                        Admin.closeEditor();
+                    break;
+                    case "nochange":
+                        this.commError(SypStrings.NochangeError);
+                        Admin.closeEditor();
+                    break;
+                    case "request":
+                        this.commError(SypStrings.RequestError);
+                        $("title").focus();
+                    break;
+                    case "toobig":
+                        this.commError(SypStrings.ToobigError);
+                        $("#image_file").parent().html($("#image_file").parent().html());
+                        $("#image_file").focus();
+                    break;
+                    case "notimage":
+                        this.commError(SypStrings.NotimageError);
+                        $("#image_file").parent().html($("#image_file").parent().html());
+                        $("#image_file").focus();
+                    break;
+                    default:
+                        this.commError(SypStrings.UnconsistentError);
+                        $("title").focus();
+                    break;
+                }
+            break;
+            case "success":
+                switch (xml.documentElement.getAttribute("request")) {
+                    case "del":
+                        this.commSuccess(SypStrings.DelSucces);
+                        var someFeature = false;
+                        var self = this;
+                        $.each($(xml).find("FEATURE,feature"), function () {
+                             someFeature = true;
+                             var id = parseFloat($(this).find("ID:first,id:first").text());
+                             if ((id === null) || isNaN (id)) {
+                                return;;
+                             }
+                             var features = Admin.dataLayer.features;
+                             for (var idx = 0; idx < features.length; idx++) {
+                                 if (features[idx].fid == id) {
+                                     Admin.dataLayer.removeFeatures([features[idx]]);
+                                 }
+                             }
+                        });
+                        if (someFeature == false) {
+                            this.commError(SypStrings.UnconsistentError);
+                        } else {
+                            Admin.closeEditor();
+                        }
+                    break;
+                    case "update":
+                    case "add":
+                        var someFeature = false;
+                        var self = this;
+                        $.each($(xml).find("FEATURE,feature"), function () {
+                                someFeature = true;
+                                var id = parseFloat($(this).find("ID:first,id:first").text());
+                                if ((id === null) || isNaN (id)) {
+                                    return;;
+                                }
+
+                                var lon = parseFloat($(this).find("LON:first,lon:first").text());
+                                if ((typeof (lon) != "number") || isNaN (lon) ||
+                                        (lon < -180) || (lon > 180)) {
+                                    return;;
+                                }
+
+                                var lat = parseFloat($(this).find("LAT:first,lat:first").text());
+                                if ((typeof (lat) != "number") || isNaN (lat) ||
+                                        (lat < -90) || (lat > 90)) {
+                                    return;;
+                                }
+
+                                var mapProj = Admin.map.getProjectionObject();
+                                var lonlat = new OpenLayers.LonLat (lon, lat).
+                                                transform( new OpenLayers.Projection("EPSG:4326"), mapProj);
+
+                                var imgurl = $(this).find("IMGURL:first,imgurl:first").text();
+                                var title = $(this).find("HEADING:first,heading:first").text();
+                                var description = $(this).find("DESCRIPTION:first,description:first").text();
+
+                                feature = self.update (Admin.currentFeature, lonlat, imgurl, title, description); 
+                                feature.fid = id;
+                        });
+
+                        if (someFeature == false) {
+                            this.commError(SypStrings.UnconsistentError);
+                        } else {
+                            this.commSuccess(SypStrings.UpdateSucces);
+                            Admin.closeEditor();
+                        }
+
+                    break;
+                    default:
+                        this.commError(SypStrings.UnconsistentError);
+                   break;
+                }
+            break;
+            default:
+                this.commError(SypStrings.UnconsistentError);
+            break;
+        }
+    },
+
+    commSuccess: function (message) {
+        $("#server_comm").text(message);
+        $("#server_comm").removeClass("error success").addClass("success");
+    },
+
+    commError: function (message) {
+        $("#server_comm").text(message);
+        $("#server_comm").removeClass("error success").addClass("error");
+    }
+}
+
+/* maintains a queue of ajax queries, so I'm sure they all execute in the same
+ * order they were defined */
+var AjaxMgr = {
+    _queue: [],
+
+    running: false,
+
+    add: function(query) {
+        this._queue.push(query);
+        if (this._queue.length > 1) {
+            return;
+        } else {
+            this._runQuery(query);
+        }
+    },
+
+    _runQuery: function(query) {
+        var self = this;
+        $('#api_frame').one("load", function() {
+            self.running = false;
+            self._reqEnd();
+            if (query.throbberid) {
+                $("#" + query.throbberid).css("visibility", "hidden");
+            }
+            if (typeof (query.oncomplete) == "function") {
+                var body = null;
+                try {
+                    if (this.contentDocument) {
+                        body = this.contentDocument.body;
+                    } else if (this.contentWindow) {
+                        body = this.contentWindow.document.body;
+                    } else {
+                        body = document.frames[this.id].document.body;
+                    }
+                } catch (e) {}
+                    if (body) {
+                        query.oncomplete(body.innerHTML);
+                    } else {
+                        query.oncomplete(null);
+                    }
+            }
+        });
+        query.form.attr("action", "api.php");
+        query.form.attr("target", "api_frame");
+        query.form.attr("method", "post");
+        this.running = true;
+        query.form.get(0).submit();
+        if (query.throbberid) {
+            $("#" + query.throbberid).css("visibility", "visible");
+        }
+        if (typeof (query.onsend) == "function") {
+            query.onsend();
+        }
+    },
+
+    _reqEnd: function() {
+        this._queue.shift();
+        if (this._queue.length > 0) {
+            this._reqEnd(this._queue[0]);
+        }
+    }
+}
+
 var pwdMgr = {
 
     init: function () {
         $("#login_form").submit(this.submit);
-        $("#user_pwd").focus().select();
+        $("#user").focus().select();
+    },
+
+    reset: function() {
+        this.commError ("");
+        $("#login_area").show();
+        $("#password").val("");
+        $("#user").val(sypSettings.loggedUser).focus().select();
     },
 
     submit: function () {
-        // removes focus from #user_pwd before disabling it. Otherwise, opera
-        // prevents re-focusing it after re-enabling it.
-        $("#user_pwd").blur(); 
-        $("#login_submit, #user_pwd").attr("disabled", "disabled");
-        $("#login_connect_error, #login_password_error").hide();
-        $("#pwd_throbber").css("visibility", "visible");
-        
-        var req = {
-            type: this.method,
-            url: this.action,
-            data:  { user_pwd: this.user_pwd.value },
-            success: pwdMgr.postSuccessCallback,
-            error: pwdMgr.postErrorCallback,
-            timeout: 10000
-        };
-
-        AjaxMgr.add(req);
+        try {
+            pwdMgr.commError("");
+            var req = {
+                form:  $("#login_form"),
+                throbberid: "pwd_throbber",
+                onsend: function() {
+                    $("#login_error").hide();
+
+                    // we need a timeout; otherwise those fields will not be submitted
+                    window.setTimeout(function() { 
+                            // removes focus from #password before disabling it. Otherwise, opera
+                            // prevents re-focusing it after re-enabling it.
+                            $("#user, #password").blur(); 
+                            $("#login_submit, #user, #password").attr("disabled", "disabled");
+                    }, 0)
+                },
+                oncomplete: OpenLayers.Function.bind(pwdMgr.ajaxReply, pwdMgr)
+            };
+            AjaxMgr.add(req);
+        } catch(e) {}
         return false;
     },
 
-    postErrorCallback: function (data, textStatus) {
-        $("#pwd_throbber").css("visibility", "hidden");
-        $("#login_submit, #user_pwd").removeAttr("disabled");
-        var errorText = Admin.connectErrorMsg(textStatus);
-        $("#login_connect_error").text(errorText).show();
-        $("#user_pwd").focus().select();
+    ajaxReply: function (data) {
+        // here, we need a timeout because onsend timeout sometimes has not been triggered yet
+        window.setTimeout(function() {
+            $("#login_submit, #user, #password").removeAttr("disabled");
+        }, 0);
+
+        if (!data) {
+            this.commError(SypStrings.ServerError);
+            $("#login_error").show();
+            window.setTimeout(function() {
+                    $("#user").focus().select();
+            }, 0);
+            return;
+        }
+
+        var xml = new OpenLayers.Format.XML().read(data);
+        if (!xml.documentElement) {
+            this.commError(SypStrings.UnconsistentError);
+            $("#login_error").show();
+            window.setTimeout(function() {
+                    $("#user").focus().select();
+            }, 0);
+        }
+
+        switch (xml.documentElement.nodeName.toLowerCase()) {
+            case "error":
+                switch (xml.documentElement.getAttribute("reason")) {
+                    case "server":
+                        this.commError(SypStrings.ServerError);
+                    break;
+                    case "unauthorized":
+                        this.commError(SypStrings.UnauthorizedError);
+                    break;
+                    case "request":
+                        this.commError(SypStrings.RequestError);
+                    break;
+                    default:
+                        this.commError(SypStrings.UnconsistentError);
+                    break;
+                }
+                $("#login_error").show();
+                window.setTimeout(function() {
+                        $("#user").focus().select();
+                }, 0);
+            break;
+            case "success":
+                $("#login_area").hide();
+
+                user = $(xml).find("USER,user").text();
+                sypSettings.loggedUser = user;
+
+                if (sypSettings.loggedUser == "admin")  {
+                    userMgr.init();
+                }
+
+                if (Admin.selFeatureControl) {
+                    Admin.selFeatureControl.destroy();
+                }
+                if (Admin.moveFeatureControl) {
+                    Admin.moveFeatureControl.destroy();
+                }
+                if (Admin.addFeatureControl) {
+                    Admin.addFeatureControl.destroy();
+                }
+                if (Admin.dataLayer) {
+                    Admin.dataLayer.destroy();
+                }
+
+                Admin.dataLayer = Admin.createDataLayer(user);
+                Admin.map.addLayer(Admin.dataLayer);
+                Admin.reset();
+
+            break;
+            default:
+                this.commError(SypStrings.UnconsistentError);
+            break;
+        }
     },
 
-    postSuccessCallback: function (data) {
-        $("#pwd_throbber").css("visibility", "hidden");
-        $("#login_submit, #user_pwd").removeAttr("disabled");
-        if (data == "access allowed") {
-            $("#login_area").hide();
+    commError: function (message) {
+        $("#login_error").text(message);
+        if (message) {
+            $("#login_error").show();
         } else {
-            $("#login_password_error").show();
-            $("#user_pwd").focus().select();
+            $("#login_error").hide();
         }
     }
 }
 
-var FeatureMgr = {
-    saveFeature: function (feature, imgurl, title, description, lonlat) {
-        $("#features_success").text("La sauvegarde a été réalisée avec succès");
-        var req = {
-            type: "post",
-            url: "changes.php",
-            data:  { feature_imgurl: imgurl,
-                     feature_title: title,
-                     feature_description: description,
-                     feature_lon: lonlat.lon,
-                     feature_lat: lonlat.lat
-                    },
-            success: this.featureSuccessCallback,
-            error: this.featureErrorCallback,
-            timeout: 10000
-        };
-        AjaxMgr.add(req);
-    },
-
-    deleteFeature: function (imgurl) {
-        $("#features_success").text("La suppression a été réalisée avec succès");
-        var self = this;
+var userMgr = {
+    _adduserDisplayed: false,
+    _changepassDisplayed: false,
 
-        var req = {
-            type: "post",
-            url: "changes.php",
-            data:  { feature_delete: imgurl },
-            success: function (data) { 
-                if (data == "request accepted") {
-                    Admin.closeEditor();
-                    self.itemDeleter.add(imgurl);
-                }
-                self.featureSuccessCallback(data);
-            },
-            error: this.featureErrorCallback,
-            timeout: 10000
-        };
-        AjaxMgr.add(req);
-    },
+    init: function() {
+        $("#user_close").unbind("click").click(function () {
+            userMgr.close()
+        });
 
-    featureSuccessCallback: function (data) {
-        switch (data) {
-            case "request accepted": // do nothing: everything went fine
-                    $("#features_success").css("visibility", "visible");
-                return;
+        $("#change_pass").unbind("click").click(function() {
+            userMgr.toggleChangePass();
+            return false;
+        });
+        $("#changepass").unbind("submit").submit(function() {
+            try {
+                userMgr.changepass();
+            } catch(e) {}
+            return false;
+        });
 
-            case "access denied":
-                $("#login_area, #login_password_error").show();
-                break;
+        if (sypSettings.loggedUser != "admin") {
+            return;
+        }
 
-            case "request error":
-                alert("Le serveur a été victime d'une erreur de requête. Il s'agit probablement d'un bug dans SYP.");
-                break;
+        $("#add_user").show();
+        $("#add_user").unbind("click").click(function () {
+            userMgr.toggleAddUser();
+            return false;
+        });
+        $("#newuser").unbind("submit").submit(function() {
+            try {
+                userMgr.add();
+            } catch(e) {}
+            return false;
+        });
 
-            case "feature unavailable":
-                alert("La photo n'était pas référencée sur le serveur. Il est possible qu'elle ait été supprimée");
-                break;
+    },
 
-                case "server error":
-            default:
-                var text = Admin.connectErrorMsg();
-                $("#features_connect_error").text(text).show();
-                break;
-        }
-        Admin.reloadLayer(Admin.dataLayer);
+    disableForms: function() {
+        $("#newuser_name, #newuser_password, #newuser_password_confirm, #newuser_submit").attr("disabled", "disabled");
+        $("#pass_current, #pass_new, #pass_new_confirm, #pass_submit").attr("disabled", "disabled");
+    },
+
+    enableForms: function() {
+        $("#newuser_name, #newuser_password, #newuser_password_confirm, #newuser_submit").removeAttr("disabled");
+        $("#pass_current, #pass_new, #pass_new_confirm, #pass_submit").removeAttr("disabled");
+    },
+
+    resetForms: function() {
+        $("#newuser_name, #newuser_password, #newuser_password_confirm").val("");
+        $("#pass_current, #pass_new, #pass_new_confirm").val("");
     },
 
-    featureErrorCallback: function (data, textStatus) {
-        var text = Admin.connectErrorMsg(textStatus);
-        $("#features_connect_error").text(text).show();
-        Admin.reloadLayer(Admin.dataLayer);
+    uninit: function() {
+        this.close();
+        $("#add_user").unbind("click");
+        $("#add_user").hide();
+        $("#change_pass").unbind("click");
+        $("#user_close").unbind("click");
+        $("#newuser").unbind("submit");
+        $("#changepass").unbind("submit");
     },
 
-    fileFrameLoad: function () {
-        $("#newimage_throbber").hide();
-        $("#newimage_input").val('');
+    close: function() {
+        this.closeChangePass();
+        this.closeAddUser();
+    },
 
-        var doc;
-        if (this.contentDocument) {
-            var doc = this.contentDocument;
-        } else if (this.contentWindow) {
-            var doc = this.contentWindow.document;
+    toggleChangePass: function() {
+        if (this._changepassDisplayed) {
+            this.closeChangePass();
         } else {
-            var doc = document.frames[this.id].document;
-        }
-        var body = $(doc.body);
-
-        if (body.children().length <= 1) { // error are signaled with a simple
-                                           // string message
-            var resp = body.html();
-            if (resp == "access denied") {
-                $("#login_area, #login_password_error").show();
-                Admin.closeNewimage();
-            } else if (resp == "file too big") {
-                var text = "L'image était trop grande et n'a pas été acceptée " +
-                            "par le serveur. Veuillez réduire sa taille avant " +
-                            "de l'envoyer.";
-                $("#newimage_error").text(text).show();
-                $("#newimage_input").focus(); 
-            } else if (resp == "not an image") {
-                var text = "Le fichier ne semble pas être une image.";
-                $("#newimage_error").text(text).show();
-                $("#newimage_input").focus(); 
-            } else {
-                var text = Admin.connectErrorMsg();
-                $("#newimage_error").text(text).show();
-            }
+            this.showChangePass();
+        }
+    },
 
-        } else { // when image is successfully uploaded, informations are
-                 // passed back in document body
-            var res = body.find('.res');
-            if (res.text() == "request accepted") {
-                $("#newimage_input").unbind('change');
-                $("#fileframe").unbind('load');
-                $("#file_form").hide();
-
-                var imgurl = body.find('.infos > .imgurl').text();
-                $("#newimage_preview").attr("src", imgurl);
-                $("#newimage_preview").css("display", "block");
-                var text = "Pour valider l'ajout de cette image, vous devez la " +
-                           " positionner sur la carte. Cliquez sur la carte pour " +
-                           "positionner le marqueur.";
-                $("#newimage_warn").text(text).show();
-
-                var clickControl = Admin.clickControl;
-                clickControl.handler.callbacks.click = 
-                                            Admin.addMarkerNewImage(imgurl);
-                clickControl.activate();
-
-                Admin.selectControl.deactivate();
-                $("#modify_howto").css("visibility", "hidden");
-            } else {
-                var text = Admin.connectErrorMsg();
-                $("#newimage_error").text(text).show();
+    showChangePass: function() {
+        if (!Admin.cancelCurrentFeature()) {
+            return;
+        }
+        this.closeAddUser();
+
+        $(document).unbind("keydown").keydown(function(e) { 
+            if (e.keyCode == 27) {
+                userMgr.closeChangePass()
+                e.preventDefault();
             }
+        });
+
+        this.resetForms();
+        this.enableForms();
+        $("#user_area, #changepass").show();
+        this.commError("");
+
+        // XXX: setTimeout needed because otherwise, map becomes hidden in IE. Why ??
+        window.setTimeout(function() { 
+            $("#pass_current").focus();
+        }, 0);
+
+        this._changepassDisplayed = true;
+    },
+
+    closeChangePass: function() {
+        if (!this._changepassDisplayed) {
+            return;
         }
+        $("#user_area, #changepass").hide();
+        $(document).unbind("keydown");
+        this._changepassDisplayed = false;
     },
 
-    itemDeleter: {
-        _items: [],
-        _locks: [],
+    changepass: function() {
+        var newpass = $("#pass_new").val();
+        var newpass_confirm = $("#pass_new_confirm").val();
+        if (newpass != newpass_confirm) {
+            this.commError(SypStrings.userPasswordmatchError);
+            $("#pass_new").focus().select();
+            return;
+        }
 
-        _pushUnique: function (arr, item) {
-            if (Admin.Utils.indexOf(arr, item) == -1) {
-                arr.push(item);
-            }
-        },
+        if (!newpass) {
+            this.commError(SypStrings.emptyPasswordError);
+            $("#pass_new").focus().select();
+            return;
+        }
 
-        add: function (imgurl) {
-            if (!imgurl) {
-                return;
-            }
-            if (Admin.Utils.indexOf(this._locks, imgurl) == -1) {
-                this._pushUnique(this._items, imgurl);
-                var brName = OpenLayers.Util.getBrowserName();
-                // unload event does not work in opera, and webkit does not
-                // support xmlHttpRequests in unload, so we just don't buffer
-                // those requests.
-                if (brName == "opera" || brName == "safari") { 
-                    this.flush();
-                }
+        var curpass = $("#pass_current").val();
+        if (newpass == curpass) {
+            this.commError(SypStrings.changeSamePass);
+            $("#pass_new").focus().select();
+            return;
+        }
+
+        this.commError("");
+
+        AjaxMgr.add({
+            form: $("#changepass"),
+            oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
+            throbberid: "user_throbber",
+            onsend: function() { 
+                // we need a timeout; otherwise those fields will not be submitted
+                window.setTimeout(function() {
+                    // removes focus from #password before disabling it. Otherwise, opera
+                    // prevents re-focusing it after re-enabling it.
+                    $("#pass_current, #pass_new, #pass_new_confirm").blur(); 
+                    userMgr.disableForms();
+                }, 0);
             }
-        },
+        });
+    },
 
-        lock: function (imgurl) {
-            this._pushUnique(this._locks, imgurl);
-        },
+    toggleAddUser: function() {
+        if (this._adduserDisplayed) {
+            this.closeAddUser();
+        } else {
+            this.showAddUser();
+        }
+    },
 
-        unlock: function (imgurl) {
-            var idx = Admin.Utils.indexOf(this._locks, imgurl);
-            while (idx != -1) {
-                this._locks.splice(idx, 1);
-                idx = Admin.Utils.indexOf(this._locks, imgurl);
-            }
-        },
+    showAddUser: function() {
+        if (!Admin.cancelCurrentFeature()) {
+            return;
+        }
 
-        flush: function () {
-            if (this._items.length == 0) {
-                return;
-            }
-            var i = 0;
-            var data = {};
-            for (var i = 0; i < this._items.length; i++) {
-                data["imgurl_delete_" + i] = this._items[i];
+        this.closeChangePass();
+
+        $(document).unbind("keydown").keydown(function(e) { 
+            if (e.keyCode == 27) {
+                userMgr.closeAddUser()
+                e.preventDefault();
             }
-            var req = {
-                type: "post",
-                url: "changes.php",
-                data:  data,
-                success: function (data) { 
-                },
-                error: function (data) {
-                }
-            };
-            AjaxMgr.add(req);
-            this._items = [];
-        }
-    }
-}
+        });
 
-/* maintains a queue of ajax queries, so I'm sure they all execute in the same
- * order they were defined */
-var AjaxMgr = {
-    _queue: [],
+        $("#user_area, #newuser").show();
+        this.resetForms();
+        this.enableForms();
+        this.commError("");
 
-    add: function(query) {
-        this._queue.push(query);
-        if (this._queue.length > 1) {
+        // XXX: setTimeout needed because otherwise, map becomes hidden in IE. Why ??
+        window.setTimeout(function() { 
+            $("#newuser_name").focus();
+        }, 0);
+
+        this._adduserDisplayed = true;
+    },
+
+    closeAddUser: function() {
+        if (!this._adduserDisplayed) {
             return;
-        } else {
-            this._runQuery(query);
         }
+        $("#user_area, #newuser").hide();
+        $(document).unbind("keydown");
+        this._adduserDisplayed = false;
     },
 
-    _runQuery: function(query) {
-        var self = this;
-        $.ajax({
-            type: query.type,
-            url: query.url,
-            data: query.data,
-            success: function(data) {
-                self._reqEnd();
-                query.success.call(query, data);
-            },
-            error: function(data, textStatus) {
-                self._reqEnd();
-                query.error.call(query, data, textStatus);
-            },
-            timeout: query.timeout
+    add: function() {
+        var newuser_name = $("#newuser_name").val();
+        if (!newuser_name) {
+            this.commError(SypStrings.newUserNonameError);
+            $("#newuser_name").focus();
+            return;
+        }
+
+        var newuser_pass = $("#newuser_password").val();
+        var newuser_pass_confirm = $("#newuser_password_confirm").val();
+        if (newuser_pass != newuser_pass_confirm) {
+            this.commError(SypStrings.userPasswordmatchError);
+            $("#newuser_password").focus().select();
+            return;
+        }
+
+        if (!newuser_pass) {
+            this.commError(SypStrings.emptyPasswordError);
+            $("#pass_new").focus().select();
+            return;
+        }
+
+        this.commError("");
+
+        AjaxMgr.add({
+            form: $("#newuser"),
+            oncomplete: OpenLayers.Function.bind(this.ajaxReply, this),
+            throbberid: "user_throbber",
+            onsend: function() { 
+                // we need a timeout; otherwise those fields will not be submitted
+                window.setTimeout(function() {
+                    // removes focus from #password before disabling it. Otherwise, opera
+                    // prevents re-focusing it after re-enabling it.
+                    $("#newuser_name, #newuser_password, #newuser_password_confirm").blur(); 
+                    userMgr.disableForms();
+                }, 0);
+            }
         });
     },
 
-    _reqEnd: function() {
-        this._queue.shift();
-        if (this._queue.length > 0) {
-            this._reqEnd(this._queue[0]);
+    ajaxReply: function (data) {
+        if (!data) {
+            // here, we need a timeout because onsend timeout sometimes has not been triggered yet
+            var self = this;
+            window.setTimeout(function() {
+                self.enableForms();
+             }, 0);
+            this.commError(SypStrings.ServerError);
+            return;
+        }
+
+        var xml = new OpenLayers.Format.XML().read(data);
+        if (!xml.documentElement) {
+            // here, we need a timeout because onsend timeout sometimes has not been triggered yet
+            var self = this;
+            window.setTimeout(function() {
+                self.enableForms();
+             }, 0);
+            this.commError(SypStrings.UnconsistentError);
+            return;
+        }
+
+        var needFormEnabling = true;
+        var focusEl = null;
+
+        switch (xml.documentElement.nodeName.toLowerCase()) {
+            case "error":
+                switch (xml.documentElement.getAttribute("reason")) {
+                    case "unauthorized":
+                        pwdMgr.reset();
+                        $("#cookie_warning").show();
+                        Admin.reset();
+                        this.uninit();
+                    break;
+                    case "server":
+                        this.commError(SypStrings.ServerError);
+                        if (this._adduserDisplayed) {
+                            focusEl = $("#newuser_name");
+                        } else if (this._changepassDisplayed) {
+                            focusEl = $("#pass_current");
+                        }
+                    break;
+                    case "request":
+                        this.commError(SypStrings.RequestError);
+                        if (this._adduserDisplayed) {
+                            focusEl = $("#newuser_name");
+                        } else if (this._changepassDisplayed) {
+                            focusEl = $("#pass_current");
+                        }
+                    break;
+                    case "wrongpass":
+                        this.commError(SypStrings.changePassBadPass);
+                        focusEl = $("#pass_current");
+                    break;
+                    case "newuser_exists":
+                        this.commError(SypStrings.newUserExistsError);
+                        focusEl = $("#newuser_name");
+                    break;
+                    default:
+                        this.commError(SypStrings.UnconsistentError);
+                        if (this._adduserDisplayed) {
+                            focusEl = $("#newuser_name");
+                        } else if (this._changepassDisplayed) {
+                            focusEl = $("#pass_current");
+                        }
+                    break;
+                }
+            break;
+            case "success":
+                switch (xml.documentElement.getAttribute("request")) {
+                    case "newuser":
+                        this.commSuccess(SypStrings.newUserSuccess);
+                        needFormEnabling = false;
+                    break;
+                    case "changepass":
+                        this.commSuccess(SypStrings.changePassSuccess);
+                        needFormEnabling = false;
+                    break;
+                    default:
+                        this.commError(SypStrings.UnconsistentError);
+                        focusEl = $("newuser_name");
+                    break;
+                }
+            break;
+            default:
+                this.commError(SypStrings.UnconsistentError);
+                focusEl = $("newuser_name");
+            break;
         }
+
+        if (needFormEnabling) {
+            // here, we need a timeout because onsend timeout sometimes has not been triggered yet
+            var self = this;
+            window.setTimeout(function() {
+                self.enableForms();
+                if (focusEl) {
+                    focusEl.select().focus();
+                }
+             }, 0);
+        } else {
+            if (focusEl) {
+                focusEl.focus().select();
+            }
+        }
+
+    },
+
+    commSuccess: function (message) {
+        $("#user_comm").text(message);
+        $("#user_comm").removeClass("error success").addClass("success");
+    },
+
+    commError: function (message) {
+        $("#user_comm").text(message);
+        $("#user_comm").removeClass("error success").addClass("error");
     }
 }
 
@@ -877,15 +1220,34 @@ $(window).load(function () {
     // if using .ready, ie triggers an error when trying to access
     // document.namespaces
     pwdMgr.init();
-    $("#newimage_close").click(function () {
-        Admin.closeNewimage();
-    });
-    $("#addphoto_button").click(function () {
+    $("#newfeature_button").click(function () {
         Admin.addNewFeature();
     });
+    $("#editor_close").click(function () {
+        Admin.cancelCurrentFeature()
+    });
+    $("#feature_update").submit(function() {
+        try {
+            FeatureMgr.save(Admin.currentFeature);
+        } catch(e) {}
+        return false;
+    });
+    $("#feature_delete").submit(function() {
+        try {
+            FeatureMgr.del(Admin.currentFeature);
+        } catch(e) {}
+        return false;
+    });
+    $("#image_delete").click(function() {
+            $("#img").removeAttr('src');
+            // needs to rebuild element otherwise some browsers still
+            // display image.
+            $("#img").parent().html($("#img").parent().html());
+            $("#img").parent().hide();
+            $("#image_delete").hide();
+            $("#image_file").parent().show();
+    });
+
+    userMgr.init();
     Admin.init();
 });
-$(window).unload(function () {
-    FeatureMgr.itemDeleter.add($("#newimage_preview").attr("src"));
-    FeatureMgr.itemDeleter.flush();
-});