Merge remote-tracking branch 'gravitystorm/history'
[potlatch2.git] / net / systemeD / potlatch2 / history / HistoryDialog.mxml
1 <?xml version="1.0" encoding="utf-8"?>
2 <!---
3     The History Dialog displays information about the history of an entity. As well as showing the different versions
4     of the entity, it displays synthetic history based on its children. For example, the intermediate states
5     of a way while the nodes are rearranged are shown.
6 -->
7 <s:TitleWindow
8         xmlns:fx="http://ns.adobe.com/mxml/2009"
9         xmlns:mx="library://ns.adobe.com/flex/mx"
10         xmlns:s="library://ns.adobe.com/flex/spark"
11         xmlns:help="net.systemeD.potlatch2.help.*"
12         title="History for {entity.getType()} {entity.id}"
13         width="600" height="400">
14
15   <s:layout>
16     <s:VerticalLayout />
17   </s:layout>
18
19   <s:VGroup width="100%" height="100%">
20     <s:HGroup width="100%" horizontalAlign="center" verticalAlign="middle" paddingTop="5">
21       <s:RichText text="Loading data..." visible="{ entityStates.length == 0 }" />
22     </s:HGroup>
23       <s:DataGrid dataProvider="{entityStates}" width="100%" height="100%" enabled="{ entityStates != null }">
24         <s:columns>
25           <s:ArrayList>
26             <s:GridColumn editable="false" dataField="version" headerText="version" width="60">
27               <s:itemRenderer>
28                 <fx:Component>
29                   <s:DefaultGridItemRenderer textAlign="center" />
30                 </fx:Component>
31               </s:itemRenderer>
32             </s:GridColumn>
33             <s:GridColumn editable="false" dataField="timestamp" headerText="timestamp" />
34             <s:GridColumn editable="false" dataField="user" headerText="username" />
35             <s:GridColumn editable="false">
36               <s:itemRenderer>
37                 <fx:Component>
38                   <s:GridItemRenderer>
39                     <s:VGroup horizontalAlign="center" verticalAlign="middle" width="100%" height="100%">
40                       <s:Button label="Contact Mapper" click="parentDocument.parentDocument.message(data.user)"/>
41                     </s:VGroup>
42                   </s:GridItemRenderer>
43                 </fx:Component>
44               </s:itemRenderer>
45             </s:GridColumn>
46           </s:ArrayList>
47         </s:columns>
48       </s:DataGrid>
49   </s:VGroup>
50
51
52   <s:controlBarContent>
53     <!-- <s:Button label="Revert" enabled="false" styleName="titleWindowButton" /> -->
54     <s:Spacer width="100%"/>
55     <s:Button label="More Details..." enabled="{entity.id >= 0}" click="openEntityPage()" styleName="titleWindowButton" />
56     <s:Button label="Cancel" click="PopUpManager.removePopUp(this);" styleName="titleWindowButton" />
57   </s:controlBarContent>
58
59   <fx:Script><![CDATA[
60
61     import mx.managers.PopUpManager;
62     import mx.core.Application;
63     import mx.core.FlexGlobals;
64     import mx.events.CloseEvent;
65     import flash.events.Event;
66     import com.adobe.utils.ArrayUtil;
67
68     import net.systemeD.halcyon.connection.*
69
70     // This is the entity that we're requesting the history for.
71     [Bindable]
72     private var entity:Entity;
73
74     // These are the various states that the entity as been in - so is a list
75     // of Nodes (all with the same id) or Ways etc
76     [Bindable]
77     private var entityStates:ArrayList = new ArrayList();
78
79     // store intermediate states for ways
80     private var wayStates:Array; // an array of ways
81     private var wayNodeStates:Array; // an array of arrays of nodes
82
83     // the number of outstanding asynchronous node history requests,
84     // so we know when all have been fetched
85     private var pendingNodeFetches:uint;
86
87     public function init(e:Entity):void {
88         if (e == null) {return;}
89
90         PopUpManager.addPopUp(this, Application(FlexGlobals.topLevelApplication), true);
91         PopUpManager.centerPopUp(this);
92         this.addEventListener(CloseEvent.CLOSE, historyDialog_close);
93
94         entity = e;
95         fetchHistory();
96     }
97
98     private function historyDialog_close(evt:CloseEvent):void {
99         PopUpManager.removePopUp(this);
100     }
101
102     /** Open up a new browser page showing OSM's view of the current entity. */
103     private function openEntityPage():void {
104         if (entity != null && entity.id >= 0) {
105             // This is slightly hard-coded, but not drastically. The ../s could be changed for string manipulation of the apiBase
106             var urlBase:String = entity.connection.apiBase + '../../browse/';
107             navigateToURL(new URLRequest(urlBase+entity.getType()+'/'+entity.id), "potlatch_browse");
108         }
109     }
110
111     private function fetchHistory():void {
112         if (entity is Node) {
113             entity.connection.fetchHistory(entity, processNode);
114         } else if (entity is Way) {
115             entity.connection.fetchHistory(entity, processWay);
116         } else {
117             // not implemented
118         }
119     }
120
121     private function processNode(results:Array):void {
122         // Simply copy the nodes into the states array
123         // todo sorting or somesuch
124         entityStates.source = results.reverse();
125     }
126
127     private function processWay(results:Array):void {
128         // This is much more complicated that nodes.
129         // In potlatch(2) we show the user the number of different states, bearing in mind
130         // node movements (and tag changes?).
131
132         wayStates = results;
133
134         // figure out the list of nodes that have ever been involved in the way, and fetch them.
135         // pendingNode will store each one, and trigger an event when they are all downloaded.
136         wayNodeStates = [];
137         addEventListener("pendingNodesAllFetched", processWayStates);
138
139         var nodes:Object = {};
140         var count:uint = 0;
141
142         for each(var oldWay:Way in results) {
143             for (var i:uint = 0; i< oldWay.length; i++) {
144                 var node:Node = oldWay.getNode(i);
145                 if(!nodes[node.id]) {
146                     nodes[node.id] = node;
147                     count++;
148                 }
149             }
150         }
151
152         pendingNodeFetches = count;
153
154         for each (var n:Node in nodes) {
155             entity.connection.fetchHistory(n, pendingNode);
156         }
157     }
158
159     // Callback for fetching a node history as part of a way; when there's no outstanding
160     // nodes remaining this will trigger an event.
161     private function pendingNode(results:Array):void {
162         wayNodeStates.push(results)
163         pendingNodeFetches--;
164         trace("fetched node "+results[0].id+" , "+pendingNodeFetches+" more to go");
165         if (pendingNodeFetches == 0) {
166             dispatchEvent(new Event("pendingNodesAllFetched"));
167         }
168     }
169
170     private function processWayStates(e:Event):void {
171         // we now have all the node histories for
172         // for each node that has ever been part of the way.
173
174         // Build a list of every timestamp of interest, along with who
175         // the person was that triggered that timestamp (either from a way version
176         // change, or from changing a node.
177         var revdates:Array = [];
178         var revusers:Object = {};
179
180         for each (var way:Way in wayStates) {
181             revdates.push(way.timestamp);
182             revusers[way.timestamp] = way.user;
183         }
184
185         for each (var nodeStates:Array in wayNodeStates) {
186             for each (var node:Node in nodeStates) {
187                 revdates.push(node.timestamp);
188                 revusers[node.timestamp] = node.user;
189             }
190         }
191
192         // sort the dates and remove duplicates and those before the first version of the way
193
194         revdates = revdates.sort();
195         revdates = ArrayUtil.createUniqueCopy(revdates); // (corelib) de-duplicates
196         revdates = revdates.filter(function(e:*, i:int, arr:Array):Boolean { return e >= wayStates[0].timestamp});
197
198         var version:int = 1;
199         var subversion:int = 0;
200         var es:Array = []; // entityStates
201
202         for each (var revdate:String in revdates) {
203           var entitystate:Object = {};
204
205           var w:Way = getEntityAtDate(wayStates, revdate) as Way;
206
207           if (w.version == version) {
208               subversion++;
209           } else {
210               version = w.version;
211               subversion = 1;
212           }
213
214           //for (i = 0, i < w.length; i++) {
215           //    var generalNode:Node = w.getNode(i);
216           //    var specificNode:Node = getEntityAtDate(wayNodeStates[generalNode.id], revdate);
217           //    where was I going with this? Oh, yes, it'll be needed for building the object to revert to.
218           //}
219
220           entitystate.version = String(version) + "." + String(subversion);
221           entitystate.timestamp = revdate;
222           entitystate.user = revusers[revdate];
223           es.push(entitystate)
224         }
225
226         entityStates.source = es.reverse();
227     }
228
229     // given a list of entities sorted with oldest first, find the last version before that date.
230     private function getEntityAtDate(list:Array, date:String):Entity {
231         trace("find for date : "+date);
232         for(var i:int = list.length-1; i >= 0; i--) {
233             var entity:Entity = list[i];
234             trace (entity.timestamp + " : " + date);
235             if (entity.timestamp <= date) {
236                 trace("returning version " + entity.version);
237                 return entity;
238             }
239         }
240         trace("ERROR");
241         return null;
242     }
243
244     public function message(user:String):void {
245         if (user) {
246             var urlBase:String = entity.connection.apiBase + '../../message/new/';
247             navigateToURL(new URLRequest(urlBase+user), "potlatch_message");
248         }
249     }
250     ]]>
251   </fx:Script>
252
253
254 </s:TitleWindow>
255
256