Merge remote-tracking branch 'gravitystorm/history'
authorRichard Fairhurst <richard@systemeD.net>
Sun, 25 Mar 2012 15:52:18 +0000 (16:52 +0100)
committerRichard Fairhurst <richard@systemeD.net>
Sun, 25 Mar 2012 15:52:18 +0000 (16:52 +0100)
1  2 
net/systemeD/halcyon/connection/XMLConnection.as
net/systemeD/potlatch2/TagViewer.mxml
net/systemeD/potlatch2/history/HistoryDialog.mxml
potlatch2.mxml

index 30aef5e757b43fdc5f31c30a807ded7bff1e3c6e,74fb3635167fdf7d2e61f4ddea11965a265158d6..6b5d8aeec2fed0af8473c91df9335f208ce4dc1a
@@@ -9,6 -9,7 +9,7 @@@ package net.systemeD.halcyon.connectio
  
        import net.systemeD.halcyon.AttentionEvent;
        import net.systemeD.halcyon.MapEvent;
+       import net.systemeD.halcyon.ExtendedURLLoader;
      import net.systemeD.halcyon.connection.bboxes.*;
  
      /**
            }
  
          private function changesetCreateComplete(event:Event):void {
 -            // response should be a Number changeset id
 -            var id:Number = Number(URLLoader(event.target).data);
 +            var result:String = URLLoader(event.target).data;
 +
 +            if (result.match(/^^\d+$/)) {
 +                // response should be a Number changeset id
 +                var id:Number = Number(URLLoader(event.target).data);
              
 -            // which means we now have a new changeset!
 -            setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
 +                // which means we now have a new changeset!
 +                setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
 +            } else {
 +                var results:XML = XML(result);
 +
 +                throwServerError(results.message);
 +            }
          }
  
          private function changesetCreateError(event:IOErrorEvent):void {
                                function(e:Event):void { 
                        dispatchEvent(new Event(LOAD_COMPLETED));
                                        callback(e);
 -                              }, errorOnMapLoad, mapLoadStatus); // needs error handlers
 +                              }, errorOnTraceLoad, mapLoadStatus); // needs error handlers
              dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
          }
  
 +        private function errorOnTraceLoad(event:Event):void {
 +            trace("Trace load error");
 +            dispatchEvent(new Event(LOAD_COMPLETED));
++              }
++
+         /** Fetch the history for the given entity. The callback function will be given an array of entities of that type, representing the different versions */
+         override public function fetchHistory(entity:Entity, callback:Function):void {
+             if (entity.id >= 0) {
+               var request:URLRequest = new URLRequest(apiBaseURL + entity.getType() + "/" + entity.id + "/history");
+               var loader:ExtendedURLLoader = new ExtendedURLLoader();
+               loader.addEventListener(Event.COMPLETE, loadedHistory);
+               loader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad); //needs error handlers
+               loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
+               loader.info['callback'] = callback; //store the callback so we can use it later
+               loader.load(request);
+               dispatchEvent(new Event(LOAD_STARTED));
+             } else {
+               // objects created locally only have one state, their current one
+               callback([entity]);
+             }
+         }
+         private function loadedHistory(event:Event):void {
+             var _xml:XML = new XML(ExtendedURLLoader(event.target).data);
+             var results:Array = [];
+             var dummyConn:Connection = new Connection("dummy", null, null);
+             dispatchEvent(new Event(LOAD_COMPLETED));
+             // only one type of entity should be returned, but this handles any
+             for each(var nodeData:XML in _xml.node) {
+                 var newNode:Node = new Node(
+                     dummyConn,
+                     Number(nodeData.@id),
+                     uint(nodeData.@version),
+                     parseTags(nodeData.tag),
+                     true,
+                     Number(nodeData.@lat),
+                     Number(nodeData.@lon),
+                     Number(nodeData.@uid),
+                     nodeData.@timestamp,
+                     nodeData.@user
+                     );
+                 results.push(newNode);
+             }
+             for each(var wayData:XML in _xml.way) {
+                 var nodes:Array = [];
+                 for each(var nd:XML in wayData.nd) {
+                   nodes.push(new Node(dummyConn,Number(nd.@ref), NaN, null, false, NaN, NaN));
+                 }
+                 var newWay:Way = new Way(
+                     dummyConn,
+                     Number(wayData.@id),
+                     uint(wayData.@version),
+                     parseTags(wayData.tag),
+                     true,
+                     nodes,
+                     Number(wayData.@uid),
+                     wayData.@timestamp,
+                     wayData.@user
+                     );
+                 results.push(newWay);
+             }
+             for each(var relData:XML in _xml.relation) {
+                 trace("relation history not implemented");
+             }
+             // use the callback we stored earlier, and pass it the results
+             ExtendedURLLoader(event.target).info['callback'](results);
          }
        }
  }
index 1994ce22b2353077cb89e3f4a22efa465bbe28a6,dd1fefb1818bf7edbde868ebf8b1cdf6152399ea..96f733eb7ee8ebc9aff8870e1e57388f240ebe8a
  
  <mx:ViewStack id="sidebar" width="100%" height="100%" creationPolicy="all">
  
 -  <!-- Drag & drop icon panel -->
 -
 -  <mx:VBox id="dndPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
 -    <mx:Text id="dndPanelText" text="{dndPrompt}" width="100%" styleName="helpInfo" />
 -      <mx:Repeater id="dndRep" dataProvider="{MapFeatures.getInstance().getCategoriesForType('point')}" styleName="dndRepeater">
 -              <mx:HBox width="100%" styleName="dndPanelCategory">
 -                      <mx:Label text="{dndRep.currentItem.name}:" styleName="dndPanelCategoryLabel"/>
 -              </mx:HBox>
 -
 -              <mx:TileList dataProvider="{dndRep.currentItem.getFeaturesForType('point', true)}" width="100%" height="1"
 -                                   rowHeight="32" columnWidth="32" updateComplete="resizePOIGrid(event)" styleName="dndPanelTileList">
 -                      <mx:itemRenderer>
 -                              <fx:Component>
 -                                      <mx:VBox toolTip="{data.name}">
 -                                              <fx:Script><![CDATA[
 -                                                      import mx.events.DragEvent;
 -                                                      import mx.managers.DragManager;
 -                                                      import mx.core.DragSource;
 -
 -                                                      private function dragPOI(event:MouseEvent, tags:Array):void {
 -                                                              if (outerDocument.controller.map.dragstate==outerDocument.controller.map.DRAGGING) return;
 -
 -                                                              // Get the drag initiator component from the event object.
 -                                                              var dragInitiator:Image = event.currentTarget as Image;
 -                                                              var dragSource:DragSource = new DragSource();
 -                                                              dragSource.addData(tags, 'tags');
 -                                                              dragSource.addData(event.target.parent.parent.parent.parent, 'container');
 -
 -                                                              var dragProxy:Image = new Image();
 -                                                              dragProxy.source = dragInitiator.source;
 -                                                              dragProxy.width = dragInitiator.width;   // must set width and height explicitly
 -                                                              dragProxy.height = dragInitiator.height; // for non-embedded images
 -                                                              DragManager.doDrag(dragInitiator, dragSource, event, dragProxy);
 -                                                      }
 -                                              ]]></fx:Script>
 -                                              <mx:Image id="foo" source="{data.dndimage}" height="24" width="24" mouseMove="dragPOI(event, data.tags)" toolTip="{data.name}" />
 -                                      </mx:VBox>
 -                              </fx:Component>
 -                      </mx:itemRenderer>
 -              </mx:TileList>
 -      </mx:Repeater>
 -  </mx:VBox>
 +  <sidepanel:DragAndDropPanel id="dndPanel" />
  
    <!-- Standard tagging panel -->
  
@@@ -37,7 -78,7 +37,7 @@@
          </mx:VBox>
        </mx:VBox>
        <mx:VBox width="100%" height="100%" label="Advanced" id="advancedContainer" initialize="checkAdvanced()" verticalGap="1">
-         <mx:Label id="advancedID" click="openEntityPage()">
+         <mx:Label id="advancedID" click="new HistoryDialog().init(selectedEntity);">
            <mx:htmlText><![CDATA[<i>No Selection</i>]]></mx:htmlText>
          </mx:Label>
  
        import net.systemeD.halcyon.MapPaint;
        import net.systemeD.potlatch2.EditController;
        import net.systemeD.potlatch2.mapfeatures.*;
+       import net.systemeD.potlatch2.history.HistoryDialog;
        import net.systemeD.potlatch2.mapfeatures.editors.*;
        import net.systemeD.potlatch2.utils.*;
        import net.systemeD.controls.CollapsiblePanel;
        private var tabIcons:Object= { Basic:tabIconBasic, Details:tabIconDetails, Address:tabIconAddress, Walk:tabIconWalk, Cycle:tabIconCycle, 
                                         Transport:tabIconTransport, Restrictions:tabIconRestrictions};
  
 -      [Bindable]
 -      public var dndPrompt:String="Add new points by dragging them onto the map";
 -
        private var editorStackTabNavigator:TabNavigator;
        private var editorStackAccordion:Accordion;
        [Bindable] private var editorStack:Container;
        private var connection:Connection;
        private var currentCategorySelector:CategorySelector;
        private var categorySelectors:Object = {};      // hash of categorySelectors for each limitType
 +      private var categorySelectorEntity:Entity;      // entity used to draw the categorySelector
        private var feature:Feature = null;
  
        private var rowData:Object;             // relation membership reference, needed so it's accessible from relation actions menu
                if (entities.length==0) {
                        // Nothing selected, so show drag-and-drop panel
                        sidebar.selectedChild = dndPanel;
 +                      selectedEntity=null;
  
                } else if (entities.length==1) {
                        // Single entity selected, so show tag panel
 +                      if (firstSelected!=null && selectedEntity!=firstSelected) { 
 +                              firstSelected.addEventListener(Connection.TAG_CHANGED, tagChanged, false, 0, true);
 +                      }
                        selectedEntity=firstSelected;
                        connection=firstSelected.connection;
 -                      if (selectedEntity!=null) { selectedEntity.addEventListener(Connection.TAG_CHANGED, tagChanged); }
                        if (advancedID!=null) { setupAdvanced(firstSelected); }
                        if (firstSelected is Relation) { stack.addChild(membersVBox); }
              if (selectedEntity is Marker && connection is BugConnection) {
                        && xor(!controller.map.getLayerForEntity(entities[0]).isBackground, !controller.map.getLayerForEntity(entities[1]).isBackground) ) {
              backgroundMergePanelContents.init(entities);
              sidebar.selectedChild = backgroundMergePanel;
 +                      // ** FIXME: do we need to set selectedEntity here?
  
                } else if(isMultipleEditable(entities)) {
                        selectedEntity = new EntityCollection(entities);
 -                      selectedEntity.addEventListener(Connection.TAG_CHANGED, tagChanged);
 +                      selectedEntity.addEventListener(Connection.TAG_CHANGED, tagChanged, false, 0, true);
              sidebar.selectedChild = multiplePanel;
                        setupMultiAdvanced(selectedEntity);
                        connection=entities[0].connection;
                } else {
                        //The selection contains elements which can't be edited all together.
                        sidebar.selectedChild = multipleInvalidPanel;
 +                      selectedEntity=null;
                }
                UIComponent.resumeBackgroundProcessing();
        }
  
        private function refreshFeatureIcon():void {
            var oldFeature:Feature = feature;
 +          var oldEntity:Entity = categorySelectorEntity;
 +
            feature = selectedEntity == null ? null : mapFeatures.findMatchingFeature(selectedEntity);
 -          if ( feature != oldFeature ) {
 -              if ( oldFeature != null )
 -                  oldFeature.removeEventListener("imageChanged", featureImageChanged);
 -              if ( feature != null )
 -                  feature.addEventListener("imageChanged", featureImageChanged);
 +          if (oldFeature==feature && categorySelectorEntity==selectedEntity) {
 +              updateCategoryImageAndText(selectedEntity,feature);
 +              return;
            }
 -          setCategorySelector(selectedEntity, feature);
 +
 +          categorySelectorEntity=selectedEntity;
 +          if ( oldFeature != null ) { oldFeature.removeEventListener("imageChanged", featureImageChanged); }
 +          if ( feature != null )    { feature.addEventListener("imageChanged", featureImageChanged); }
 +                setCategorySelector(selectedEntity, feature);
        }
  
        private function featureImageChanged(event:Event):void {
            setCategorySelector(selectedEntity, feature);
        }
  
 +
        /** Set the icon, categorySelector and help text for the current entity. */
        private function setCategorySelector(entity:Entity, feature:Feature):void {
                        // Remove the "user has selected something" event listener from previous categorySelector,
                        currentCategorySelector=categorySelectors[lt];
                        currentCategorySelector.addEventListener("selectedType", changeFeatureType, false, 0, true);
  
 -                      // Update surrounding icon/text display
 +                      updateCategoryImageAndText(entity,feature);
 +                      currentCategorySelector.setSelectedFeature(feature);
 +
 +                      // Set it as the popup, and make sure it's visible
 +                      popupChange.popUp=currentCategorySelector;
 +                      currentCategorySelector.visible=true;
 +      }
 +
 +      private function updateCategoryImageAndText(entity:Entity, feature:Feature):void {
                        if (feature) {
                                iconImage.source = feature.image;
                                iconText.htmlText = feature.htmlDetails(entity);
                                popupChange.label = feature.name;
                                helpLabel.visible = feature.hasHelpURL();
 -                              currentCategorySelector.setSelectedFeature(feature);
                        } else {
                                iconImage.source = null;
                                popupChange.label = "unknown";
                                } else {
                                        iconText.htmlText = "<b>No tags set</b><br/><font size='10pt'>Please use the menu below to define what this "+entity.getType()+" is</font>";
                                }
 -                              currentCategorySelector.setSelectedFeature(null);
                        }
 -
 -                      // Set it as the popup, and make sure it's visible
 -                      popupChange.popUp=currentCategorySelector;
 -                      currentCategorySelector.visible=true;
 -      }
 +        }
  
          private function isMultipleEditable(entities:Array):Boolean {
                for each(var entity:Entity in entities) {
                navigateToURL(new URLRequest(feature.helpURL), "potlatch_help");
        }
  
-       /** Open up a new browser page showing OSM's view of the current entity. */
-       public function openEntityPage():void {
-           if (selectedEntity != null && selectedEntity.id >= 0) {
-               // This is slightly hard-coded, but not drastically. The ../s could be changed for string manipulation of the apiBase
-               var urlBase:String = connection.apiBase + '../../browse/'
-               navigateToURL(new URLRequest(urlBase+selectedEntity.getType()+'/'+selectedEntity.id), "potlatch_browse");
-           }
-       }
        public function addToRelation():void {
            new RelationSelectPanel().init(selectedEntity,new Object());
        }
            UIComponent.resumeBackgroundProcessing();
        }
  
 -      private function resizePOIGrid(event:Event):void {
 -              var rows:Number=event.target.dataProvider.length/event.target.columnCount;
 -              if (rows!=Math.floor(rows)) { rows=Math.floor(rows+1); }
 -              event.target.height=rows*(event.target.rowHeight+1);
 -      }
 -
    ]]></fx:Script>
  </mx:VBox>
  
index 0000000000000000000000000000000000000000,1d995810a8839dbe2a99fe7f26ba5f0fc6fd874d..6296b29135d663d7f83187d26713773026a0df14
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,257 +1,256 @@@
 -    of the entity, it displays synthetic history based on the entity's children. For example, the intermediate states
 -    of a way while the nodes are rearranged are show.
+ <?xml version="1.0" encoding="utf-8"?>
+ <!---
+     The History Dialog displays information about the history of an entity. As well as showing the different versions
 -                      <s:Button label="Contact Mapper"
 -                          click="parentDocument.message(data)"/>
++    of the entity, it displays synthetic history based on its children. For example, the intermediate states
++    of a way while the nodes are rearranged are shown.
+ -->
+ <s:TitleWindow
+         xmlns:fx="http://ns.adobe.com/mxml/2009"
+         xmlns:mx="library://ns.adobe.com/flex/mx"
+         xmlns:s="library://ns.adobe.com/flex/spark"
+         xmlns:help="net.systemeD.potlatch2.help.*"
+         title="History for {entity.getType()} {entity.id}"
+         width="600" height="400">
+   <s:layout>
+     <s:VerticalLayout />
+   </s:layout>
+   <s:VGroup width="100%" height="100%">
+     <s:HGroup width="100%" horizontalAlign="center" verticalAlign="middle" paddingTop="5">
+       <s:RichText text="Loading data..." visible="{ entityStates.length == 0 }" />
+     </s:HGroup>
+       <s:DataGrid dataProvider="{entityStates}" width="100%" height="100%" enabled="{ entityStates != null }">
+         <s:columns>
+           <s:ArrayList>
+             <s:GridColumn editable="false" dataField="version" headerText="version" width="60">
+               <s:itemRenderer>
+                 <fx:Component>
+                   <s:DefaultGridItemRenderer textAlign="center" />
+                 </fx:Component>
+               </s:itemRenderer>
+             </s:GridColumn>
+             <s:GridColumn editable="false" dataField="timestamp" headerText="timestamp" />
+             <s:GridColumn editable="false" dataField="user" headerText="username" />
+             <s:GridColumn editable="false">
+               <s:itemRenderer>
+                 <fx:Component>
+                   <s:GridItemRenderer>
+                     <s:VGroup horizontalAlign="center" verticalAlign="middle" width="100%" height="100%">
 -    <s:Button label="Revert" enabled="false" styleName="titleWindowButton" />
++                      <s:Button label="Contact Mapper" click="parentDocument.parentDocument.message(data.user)"/>
+                     </s:VGroup>
+                   </s:GridItemRenderer>
+                 </fx:Component>
+               </s:itemRenderer>
+             </s:GridColumn>
+           </s:ArrayList>
+         </s:columns>
+       </s:DataGrid>
+   </s:VGroup>
+   <s:controlBarContent>
 -    public function message(entity:Entity):void {
 -        if (entity.user != null) {
++    <!-- <s:Button label="Revert" enabled="false" styleName="titleWindowButton" /> -->
+     <s:Spacer width="100%"/>
+     <s:Button label="More Details..." enabled="{entity.id >= 0}" click="openEntityPage()" styleName="titleWindowButton" />
+     <s:Button label="Cancel" click="PopUpManager.removePopUp(this);" styleName="titleWindowButton" />
+   </s:controlBarContent>
+   <fx:Script><![CDATA[
+     import mx.managers.PopUpManager;
+     import mx.core.Application;
+     import mx.core.FlexGlobals;
+     import mx.events.CloseEvent;
+     import flash.events.Event;
+     import com.adobe.utils.ArrayUtil;
+     import net.systemeD.halcyon.connection.*
+     // This is the entity that we're requesting the history for.
+     [Bindable]
+     private var entity:Entity;
+     // These are the various states that the entity as been in - so is a list
+     // of Nodes (all with the same id) or Ways etc
+     [Bindable]
+     private var entityStates:ArrayList = new ArrayList();
+     // store intermediate states for ways
+     private var wayStates:Array; // an array of ways
+     private var wayNodeStates:Array; // an array of arrays of nodes
+     // the number of outstanding asynchronous node history requests,
+     // so we know when all have been fetched
+     private var pendingNodeFetches:uint;
+     public function init(e:Entity):void {
+         if (e == null) {return;}
+         PopUpManager.addPopUp(this, Application(FlexGlobals.topLevelApplication), true);
+         PopUpManager.centerPopUp(this);
+         this.addEventListener(CloseEvent.CLOSE, historyDialog_close);
+         entity = e;
+         fetchHistory();
+     }
+     private function historyDialog_close(evt:CloseEvent):void {
+         PopUpManager.removePopUp(this);
+     }
+     /** Open up a new browser page showing OSM's view of the current entity. */
+     private function openEntityPage():void {
+         if (entity != null && entity.id >= 0) {
+             // This is slightly hard-coded, but not drastically. The ../s could be changed for string manipulation of the apiBase
+             var urlBase:String = entity.connection.apiBase + '../../browse/';
+             navigateToURL(new URLRequest(urlBase+entity.getType()+'/'+entity.id), "potlatch_browse");
+         }
+     }
+     private function fetchHistory():void {
+         if (entity is Node) {
+             entity.connection.fetchHistory(entity, processNode);
+         } else if (entity is Way) {
+             entity.connection.fetchHistory(entity, processWay);
+         } else {
+             // not implemented
+         }
+     }
+     private function processNode(results:Array):void {
+         // Simply copy the nodes into the states array
+         // todo sorting or somesuch
+         entityStates.source = results.reverse();
+     }
+     private function processWay(results:Array):void {
+         // This is much more complicated that nodes.
+         // In potlatch(2) we show the user the number of different states, bearing in mind
+         // node movements (and tag changes?).
+         wayStates = results;
+         // figure out the list of nodes that have ever been involved in the way, and fetch them.
+         // pendingNode will store each one, and trigger an event when they are all downloaded.
+         wayNodeStates = [];
+         addEventListener("pendingNodesAllFetched", processWayStates);
+         var nodes:Object = {};
+         var count:uint = 0;
+         for each(var oldWay:Way in results) {
+             for (var i:uint = 0; i< oldWay.length; i++) {
+                 var node:Node = oldWay.getNode(i);
+                 if(!nodes[node.id]) {
+                     nodes[node.id] = node;
+                     count++;
+                 }
+             }
+         }
+         pendingNodeFetches = count;
+         for each (var n:Node in nodes) {
+             entity.connection.fetchHistory(n, pendingNode);
+         }
+     }
+     // Callback for fetching a node history as part of a way; when there's no outstanding
+     // nodes remaining this will trigger an event.
+     private function pendingNode(results:Array):void {
+         wayNodeStates.push(results)
+         pendingNodeFetches--;
+         trace("fetched node "+results[0].id+" , "+pendingNodeFetches+" more to go");
+         if (pendingNodeFetches == 0) {
+             dispatchEvent(new Event("pendingNodesAllFetched"));
+         }
+     }
+     private function processWayStates(e:Event):void {
+         // we now have all the node histories for
+         // for each node that has ever been part of the way.
+         // Build a list of every timestamp of interest, along with who
+         // the person was that triggered that timestamp (either from a way version
+         // change, or from changing a node.
+         var revdates:Array = [];
+         var revusers:Object = {};
+         for each (var way:Way in wayStates) {
+             revdates.push(way.timestamp);
+             revusers[way.timestamp] = way.user;
+         }
+         for each (var nodeStates:Array in wayNodeStates) {
+             for each (var node:Node in nodeStates) {
+                 revdates.push(node.timestamp);
+                 revusers[node.timestamp] = node.user;
+             }
+         }
+         // sort the dates and remove duplicates and those before the first version of the way
+         revdates = revdates.sort();
+         revdates = ArrayUtil.createUniqueCopy(revdates); // (corelib) de-duplicates
+         revdates = revdates.filter(function(e:*, i:int, arr:Array):Boolean { return e >= wayStates[0].timestamp});
+         var version:int = 1;
+         var subversion:int = 0;
+         var es:Array = []; // entityStates
+         for each (var revdate:String in revdates) {
+           var entitystate:Object = {};
+           var w:Way = getEntityAtDate(wayStates, revdate) as Way;
+           if (w.version == version) {
+               subversion++;
+           } else {
+               version = w.version;
+               subversion = 1;
+           }
+           //for (i = 0, i < w.length; i++) {
+           //    var generalNode:Node = w.getNode(i);
+           //    var specificNode:Node = getEntityAtDate(wayNodeStates[generalNode.id], revdate);
+           //    where was I going with this? Oh, yes, it'll be needed for building the object to revert to.
+           //}
+           entitystate.version = String(version) + "." + String(subversion);
+           entitystate.timestamp = revdate;
+           entitystate.user = revusers[revdate];
+           es.push(entitystate)
+         }
+         entityStates.source = es.reverse();
+     }
+     // given a list of entities sorted with oldest first, find the last version before that date.
+     private function getEntityAtDate(list:Array, date:String):Entity {
+         trace("find for date : "+date);
+         for(var i:int = list.length-1; i >= 0; i--) {
+             var entity:Entity = list[i];
+             trace (entity.timestamp + " : " + date);
+             if (entity.timestamp <= date) {
+                 trace("returning version " + entity.version);
+                 return entity;
+             }
+         }
+         trace("ERROR");
+         return null;
+     }
 -            navigateToURL(new URLRequest(urlBase+entity.user), "potlatch_message");
++    public function message(user:String):void {
++        if (user) {
+             var urlBase:String = entity.connection.apiBase + '../../message/new/';
++            navigateToURL(new URLRequest(urlBase+user), "potlatch_message");
+         }
+     }
+     ]]>
+   </fx:Script>
+ </s:TitleWindow>
diff --combined potlatch2.mxml
index 051c78c3b22dfb2db4bc5f6ff76608454e202168,87c1e5c1f3c95e941833598ed395539555c98d56..3fcc6f063da868fa0835ace42eb7e5f2c9377830
@@@ -99,9 -99,9 +99,8 @@@
                import net.systemeD.potlatch2.collections.*;
                import net.systemeD.potlatch2.controller.*;
                import net.systemeD.potlatch2.help.*;
 -              import net.systemeD.potlatch2.options.*;
 +              import net.systemeD.potlatch2.dialogs.*;
                import net.systemeD.potlatch2.utils.*;
--        import net.systemeD.potlatch2.mygpx.*;
                import net.systemeD.controls.FloatingAlert;
                import net.systemeD.controls.Spinner;
                import mx.managers.PopUpManager;
  
          public var theMap:Map;
          public var theController:EditController;
 -              public var yahoo:Yahoo;
                public var trackLoader:TrackLoader;
                public var toolbox:Toolbox;
          public var bugLoader:BugLoader;
                private function startInit():void {
                        var loader:Loader = new Loader();
                        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, startApp);
 -                      loader.load(new URLRequest("FontLibrary.swf"));
 +            if (loaderInfo.parameters['font_library']) {
 +                          loader.load(new URLRequest(loaderInfo.parameters['font_library']));
 +            } else {
 +                          loader.load(new URLRequest("FontLibrary.swf"));
 +            }
  
              if (loaderInfo.parameters['locale'] && loaderInfo.parameters['locale']!='en_US') {
                  // we don't bother with localisation if the user is en_US, as P2 is in English by default
                  Globals.vars.locale = loaderInfo.parameters['locale'];
 +                Globals.vars.locale_paths = loaderInfo.parameters['locale_paths'];
                  dispatchEvent(new LocaleEvent(LocaleEvent.LOAD_LOCALE, loaderInfo.parameters['locale']));
                  dispatchEvent(new LocaleEvent(LocaleEvent.LOAD_LOCALE, 'en_US')); // think en_US is default
                  resourceManager.localeChain = [loaderInfo.parameters['locale'], 'en_US'];
                        theMap.backdrop=b;
              theMap.updateSize(w,h);
                        theMap.addEventListener(MapEvent.SCALE, scaleHandler);
 -
 -                      yahoo=new Yahoo(theMap);
 -                      yahoo.hide();
 -                      _root.addChild(yahoo);
                        _root.addChild(theMap);
  
                        // Initialise stylesheets
                        theMap.tileset.blocks=[/google\./i];    // hard-coded block on Google tiles
                        theMap.tileset.setDimming(params['background_dim']    ==null ? true  : params['background_dim']);
                        theMap.tileset.setSharpen(params['background_sharpen']==null ? false : params['background_sharpen']);
 -                      Imagery.instance().init(theMap, overlay, yahoo);
 +                      Imagery.instance().init(theMap, overlay);
                        Imagery.instance().addEventListener(MapEvent.BUMP, bumpHandler);
                        Imagery.instance().addEventListener(CollectionEvent.SELECT,
                                function(e:CollectionEvent):void { theMap.tileset.init(e.data, e.data!=''); }
                  var gpx_url:String = loaderInfo.parameters['gpx_url'];
  
                  var connection:Connection = new Connection(name, gpx_url, null, null);
 -                var gpx:GpxImporter=new GpxImporter(connection, theMap, [gpx_url],
 +                var gpx:GpxImporter=new GpxImporter(connection, theMap,
                                                  function(success:Boolean,message:String=null):void {
                                                      if (!success) return;
                                                      var paint:MapPaint = theMap.addLayer(connection, "stylesheets/gpx.css");
                                                      paint.updateEntityUIs(false, false);
                                                      dispatchEvent(new Event("layers_changed"));
                                                  }, false);
 +                              gpx.importFromRemoteFiles([gpx_url]);
                        }
  
                        // create GPS trackloader