Show changeset comments in the history dialogue
[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="640" 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="Date" />
34             <s:GridColumn editable="false" dataField="diff" headerText="Changes" width="200" showDataTips="true" />
35             <s:GridColumn editable="false">
36               <s:itemRenderer><fx:Component><s:GridItemRenderer>
37                 <s:VGroup horizontalAlign="center" verticalAlign="middle" width="100%" height="100%">
38                   <s:Label text="{data.user}" 
39                       color="0x336699"  textDecoration="underline" buttonMode="true" 
40                       click="parentDocument.parentDocument.userPage(data.user)"/>
41                 </s:VGroup>
42               </s:GridItemRenderer></fx:Component></s:itemRenderer>
43             </s:GridColumn>
44             <s:GridColumn editable="false" dataField="comment" headerText="Comment" width="200" showDataTips="true" />
45           </s:ArrayList>
46         </s:columns>
47       </s:DataGrid>
48   </s:VGroup>
49
50
51   <s:controlBarContent>
52     <!-- <s:Button label="Revert" enabled="false" styleName="titleWindowButton" /> -->
53     <s:Spacer width="100%"/>
54     <s:Button label="More Details..." enabled="{entity.id >= 0}" click="openEntityPage()" styleName="titleWindowButton" />
55     <s:Button label="Cancel" click="PopUpManager.removePopUp(this);" styleName="titleWindowButton" />
56   </s:controlBarContent>
57
58   <fx:Script><![CDATA[
59
60     import mx.managers.PopUpManager;
61     import mx.core.Application;
62     import mx.core.FlexGlobals;
63     import mx.events.CloseEvent;
64     import flash.events.Event;
65     import com.adobe.utils.ArrayUtil;
66
67     import net.systemeD.halcyon.connection.*
68
69     // This is the entity that we're requesting the history for.
70     [Bindable]
71     private var entity:Entity;
72
73     // These are the various states that the entity as been in - so is a list
74     // of Nodes (all with the same id) or Ways etc
75     [Bindable]
76     private var entityStates:ArrayList = new ArrayList();
77
78     // store intermediate states
79     private var entityStateList:Array; // an array of objects
80     private var wayNodeStates:Array; // an array of arrays of nodes
81
82     /** Changesets used in the history. */
83     private var changesets:Array=[];
84     private var pendingChangesetFetches:uint;
85
86     // the number of outstanding asynchronous node history requests,
87     // so we know when all have been fetched
88     private var pendingNodeFetches:uint;
89
90     public function init(e:Entity):void {
91         if (e == null) {return;}
92
93         PopUpManager.addPopUp(this, Application(FlexGlobals.topLevelApplication), true);
94         PopUpManager.centerPopUp(this);
95         this.addEventListener(CloseEvent.CLOSE, historyDialog_close);
96
97         entity = e;
98         fetchHistory();
99     }
100
101     private function historyDialog_close(evt:CloseEvent):void {
102         PopUpManager.removePopUp(this);
103     }
104
105     /** Open up a new browser page showing OSM's view of the current entity. */
106     private function openEntityPage():void {
107         if (entity != null && entity.id >= 0) {
108             // This is slightly hard-coded, but not drastically. The ../s could be changed for string manipulation of the apiBase
109             var urlBase:String = entity.connection.apiBase + '../../browse/';
110             navigateToURL(new URLRequest(urlBase+entity.getType()+'/'+entity.id), "potlatch_browse");
111         }
112     }
113
114     private function fetchHistory():void {
115         if (entity is Node) {
116             entity.connection.fetchHistory(entity, processNode);
117         } else if (entity is Way) {
118             entity.connection.fetchHistory(entity, processWay);
119         } else {
120             // not implemented
121         }
122     }
123
124         private function processNode(results:Array):void {
125                 entityStateList = results;
126                 for each(var node:Node in results) {
127                         changesets.push(node.lastChangeset);
128                 }
129                 fetchChangesets();
130         }
131         
132         private function displayNodeResults(e:Event):void {
133                 var arr:Array=[];
134                 var oldNode:Node;
135                 for each (var node:Node in entityStateList) {
136                         var changes:Array=[];
137                         if (node.version==1) {
138                                 changes.push("Created");
139                                 for (var k:String in node.getTagsHash()) { changes.push(k+"="+node.getTag(k)); }
140                         } else {
141                                 if (node.lat!=oldNode.lat || node.lon!=oldNode.lon) changes.push("Moved");
142                                 diffTags(oldNode,node,changes);
143                                 // do something with visible?
144                         }
145                         arr.push({
146                                 version: node.version,
147                                 timestamp: node.timestamp.replace('T',' ').replace('Z',''),
148                                 user: node.user,
149                                 diff: changes.length==0 ? "None" : changes.join('; '),
150                                 comment: entity.connection.getChangeset(node.lastChangeset).comment
151                         });
152                         oldNode=node;
153                 }
154         entityStates.source = arr.reverse();
155     }
156
157         /** Describe tag differences between two objects. */
158         private function diffTags(oldObj:Entity,newObj:Entity,changes:Array):void {
159                 var k:String;
160                 for (k in oldObj.getTagsHash()) {
161                         // - if old tag not in new, add "Deleted"
162                         if (!newObj.getTag(k)) { changes.push("Deleted "+k); }
163                         // - if old tag in new but different, add "Changed"
164                         else if (newObj.getTag(k)!=oldObj.getTag(k)) { changes.push("Changed "+k+"="+newObj.getTag(k)); }
165                 }
166                 for (k in newObj.getTagsHash()) {
167                         // - if new tag not in old, add "Added"
168                         if (!oldObj.getTag(k)) { changes.push("Added "+k+"="+newObj.getTag(k)); }
169                 }
170         }
171
172     private function processWay(results:Array):void {
173         // This is much more complicated that nodes.
174         // In potlatch(2) we show the user the number of different states, bearing in mind
175         // node movements (and tag changes?).
176
177         entityStateList = results;
178
179         // figure out the list of nodes that have ever been involved in the way, and fetch them.
180         // pendingNode will store each one, and trigger an event when they are all downloaded.
181         wayNodeStates = [];
182         addEventListener("pendingNodesAllFetched", fetchChangesets);
183
184         var nodes:Object = {};
185         var count:uint = 0;
186
187         for each(var oldWay:Way in results) {
188             for (var i:uint = 0; i< oldWay.length; i++) {
189                 var node:Node = oldWay.getNode(i);
190                 if(!nodes[node.id]) {
191                     nodes[node.id] = node;
192                     count++;
193                 }
194             }
195             changesets.push(oldWay.lastChangeset);
196         }
197
198         pendingNodeFetches = count;
199
200         for each (var n:Node in nodes) {
201             entity.connection.fetchHistory(n, pendingNode);
202         }
203     }
204
205     // Callback for fetching a node history as part of a way; when there's no outstanding
206     // nodes remaining this will trigger an event.
207     private function pendingNode(results:Array):void {
208         wayNodeStates.push(results)
209         for each (var n:Node in results) changesets.push(n.lastChangeset);
210         pendingNodeFetches--;
211         if (pendingNodeFetches == 0) {
212             dispatchEvent(new Event("pendingNodesAllFetched"));
213         }
214     }
215
216         /** Work out which changesets were used, and fetch details for all of them. */
217         private function fetchChangesets(e:Event=null):void {
218                 changesets=ArrayUtil.createUniqueCopy(changesets);
219                 pendingChangesetFetches=changesets.length;
220                 entity.connection.addEventListener(Connection.LOAD_COMPLETED, pendingChangeset);
221                 for each (var id:Number in changesets) entity.connection.loadEntityByID("changeset",id);
222         addEventListener("pendingChangesetsAllFetched", (entity is Way) ? processWayStates : displayNodeResults);
223         }
224         
225         private function pendingChangeset(e:Event):void {
226         pendingChangesetFetches--;
227         if (pendingChangesetFetches == 0) dispatchEvent(new Event("pendingChangesetsAllFetched"));
228         }
229
230     private function processWayStates(e:Event):void {
231         // we now have all the node histories for
232         // for each node that has ever been part of the way.
233
234         // Build a list of every timestamp of interest, along with who
235         // the person was that triggered that timestamp (either from a way version
236         // change, or from changing a node.
237         var revdates:Array = [];
238         var revusers:Object = {};
239         var revdiffs:Object = {};
240                 var revchangesets:Object = {};
241
242         var oldWay:Way;
243         for each (var way:Way in entityStateList) {
244             // assemble diff
245             var changes:Array=[];
246             if (way.version==1) {
247                 changes.push("Created");
248                 for (var k:String in way.getTagsHash()) { changes.push(k+"="+way.getTag(k)); }
249             } else {
250                 var oldNodes:Array=oldWay.nodeList;
251                 var nodes:Array=way.nodeList;
252                 if (!ArrayUtil.arraysAreEqual(oldNodes,nodes)) {
253                     if      (includes(oldNodes,nodes)) { changes.push("Shortened"); }
254                     else if (includes(nodes,oldNodes)) { changes.push("Extended"); }
255                     else if (ArrayUtil.arraysAreEqual(oldNodes,nodes.reverse())) { changes.push("Reversed"); }
256                     else { changes.push("Nodes changed"); }
257                 }
258                 diffTags(oldWay,way,changes);
259             }
260
261             // push entity state
262             revdates.push(way.timestamp);
263             revdiffs[way.timestamp] = changes;
264             revusers[way.timestamp] = way.user;
265             revchangesets[way.timestamp] = entity.connection.getChangeset(way.lastChangeset);
266             oldWay=way;
267         }
268
269         for each (var nodeStates:Array in wayNodeStates) {
270             for each (var node:Node in nodeStates) {
271                 revdates.push(node.timestamp);
272                 revusers[node.timestamp] = node.user;
273                 revchangesets[node.timestamp] = entity.connection.getChangeset(node.lastChangeset);
274                 addRevDiff(revdiffs,node.timestamp,"(Node edit)");
275             }
276         }
277
278         // sort the dates and remove duplicates and those before the first version of the way
279
280         revdates = revdates.sort();
281         revdates = ArrayUtil.createUniqueCopy(revdates); // (corelib) de-duplicates
282         revdates = revdates.filter(function(e:*, i:int, arr:Array):Boolean { return e >= entityStateList[0].timestamp});
283
284         var version:int = 1;
285         var subversion:int = 0;
286         var es:Array = []; // entityStates
287
288         for each (var revdate:String in revdates) {
289           var entitystate:Object = {};
290
291           var w:Way = getEntityAtDate(entityStateList, revdate) as Way;
292
293           if (w.version == version) {
294               subversion++;
295           } else {
296               version = w.version;
297               subversion = 1;
298           }
299
300           //for (i = 0, i < w.length; i++) {
301           //    var generalNode:Node = w.getNode(i);
302           //    var specificNode:Node = getEntityAtDate(wayNodeStates[generalNode.id], revdate);
303           //    where was I going with this? Oh, yes, it'll be needed for building the object to revert to.
304           //}
305
306           es.push({
307               version: String(version) + "." + String(subversion),
308               timestamp: revdate.replace('T',' ').replace('Z',''),
309               user: revusers[revdate],
310               diff: revdiffs[revdate].length==0 ? "None" : revdiffs[revdate].join('; '),
311               comment: revchangesets[revdate].comment
312           });
313         }
314
315         entityStates.source = es.reverse();
316     }
317
318         /** Add a diff description to the hash for a given timestamp, unless it's a dupe. */
319         private function addRevDiff(revdiffs:Object,timestamp:String,diff:String):void {
320                 if (!revdiffs[timestamp]) { revdiffs[timestamp]=[diff]; return; }
321                 if (revdiffs[timestamp].indexOf(diff)>-1) return;
322                 revdiffs[timestamp].push(diff);
323         }
324
325         /** Find out whether one array is a subscript of another.
326             Bit of a hack, but will be fine as long as the array generates unique toString()s with no # in them! */
327         private function includes(array1:Array,array2:Array):Boolean {
328                 if (array1.length<array2.length) return false;
329                 return (array1.join('#').indexOf(array2.join('#'))>-1);
330         }
331
332     /** Given a list of entities sorted with oldest first, find the last version before that date. */
333     private function getEntityAtDate(list:Array, date:String):Entity {
334         for(var i:int = list.length-1; i >= 0; i--) {
335             var entity:Entity = list[i];
336             if (entity.timestamp <= date) {
337                 return entity;
338             }
339         }
340         return null;
341     }
342
343     public function userPage(user:String):void {
344         if (user) {
345             var urlBase:String = entity.connection.apiBase + '../../user/';
346             navigateToURL(new URLRequest(urlBase+user), "potlatch_user");
347         }
348     }
349     ]]>
350   </fx:Script>
351
352
353 </s:TitleWindow>
354
355