1 <?xml version="1.0" encoding="utf-8"?>
3 The TagViewer is the container for the entire sidebar. It has a number of components, and subpanels, in a ViewStack that change
4 depending on what is selected, i.e. the ControllerState.
6 @see net.systemeD.potlatch2.controller.ControllerState
9 xmlns:fx="http://ns.adobe.com/mxml/2009"
10 xmlns:mx="library://ns.adobe.com/flex/mx"
11 xmlns:flexlib="flexlib.containers.*"
12 xmlns:controls="net.systemeD.controls.*"
13 xmlns:potlatch2="net.systemeD.potlatch2.*"
14 xmlns:sidepanel="net.systemeD.potlatch2.panels.*"
15 horizontalScrollPolicy="off"
16 backgroundColor="white"
18 initialize="loadFeatures()">
20 <mx:ViewStack id="sidebar" width="100%" height="100%" minHeight="0" creationPolicy="all">
22 <sidepanel:DragAndDropPanel id="dndPanel" />
24 <!-- Standard tagging panel -->
26 <mx:VBox id="tagsPanel" width="100%" height="100%" creationPolicy="auto" tabChildren="true">
27 <mx:ViewStack id="stack" width="100%" height="100%" change="tagsPanel.tabChildren=(event.newIndex==0)">
28 <mx:VBox width="100%" height="100%" label="Simple" id="editorContainer" creationComplete="initEditorStackUIs()" styleName="dndPanelVbox">
29 <mx:VBox width="100%" verticalGap="1" styleName="dndTagHeader">
30 <mx:HBox width="100%" id="iconContainer" styleName="featureSelector">
31 <mx:Image id="iconImage"/>
32 <mx:Text condenseWhite="true" width="100%" id="iconText" styleName="dndIconText"/>
34 <mx:HBox width="100%">
35 <mx:PopUpButton id="popupChange" openAlways="true" width="100%" styleName="dndTagPopUpMenu" />
36 <mx:LinkButton icon="@Embed('../../../embedded/information.svg')" click="openDescription()" id="helpLabel" styleName="helpInfo"/>
38 <mx:HBox width="100%" id="summaryContainer" visible="false">
39 <mx:Text condenseWhite="true" width="100%" id="featureSummary" link="togglePanel()" />
43 <mx:VBox width="100%" height="100%" label="Advanced" id="advancedContainer" initialize="checkAdvanced()" verticalGap="1">
44 <potlatch2:TagGrid id="advancedTagGrid" width="100%" height="75%" />
46 <mx:HBox horizontalAlign="right" width="100%">
47 <mx:LinkButton label="Delete" click="advancedTagGrid.removeTag()" id="advancedDeleteButton" enabled="{advancedTagGrid.selectedItem != null? true : false}"/>
48 <mx:LinkButton label="Add" click="advancedTagGrid.addNewTag()" id="advancedAddButton"/>
51 <mx:DataGrid editable="true" width="100%" height="25%" id="relationsGrid"
52 doubleClickEnabled="true"
53 itemDoubleClick="editRelation(relationsGrid.selectedItem.id)"
54 doubleClick="if (event.target.parent==relationsGrid) { addToRelation(); }">
56 <mx:DataGridColumn editable="false" dataField="description" headerText="Relation" minWidth="100">
59 <mx:Text x="4" y="0" width="100%" selectable="false" htmlText="{outerDocument.adaptHTMLDescription(data.description)}" height="20"/>
63 <mx:DataGridColumn editable="false" dataField="id_idx" headerText="ID"/>
64 <mx:DataGridColumn editable="true" dataField="role" headerText="Role">
65 <mx:itemEditor><fx:Component><mx:TextInput restrict=" -" /></fx:Component></mx:itemEditor>
67 <mx:DataGridColumn width="40" minWidth="40" editable="false">
70 <mx:HBox horizontalAlign="center" verticalAlign="middle" paddingLeft="4">
71 <mx:PopUpButton arrowButtonWidth="12" paddingLeft="0" paddingRight="0" width="12" height="12"
72 open="{outerDocument.updateRelationMenu(event);}"
73 creationComplete="{outerDocument.createRelationMenu(PopUpButton(event.target));}"/>
74 <mx:Image source="@Embed('../../../embedded/delete_small.svg')"
75 click='event.stopPropagation();outerDocument.removeFromRelation(data.id, data.index);'
76 buttonMode="true" useHandCursor="true" width="12" height="12" />
81 <mx:DataGridColumn minWidth="4" /><!-- Dummy column forces Flex to render previous one at proper width -->
85 <mx:HBox horizontalAlign="right" width="100%">
86 <mx:LinkButton label="Remove from" click="removeFromRelation(relationsGrid.selectedItem.id, relationsGrid.selectedItem.index)"
87 enabled="{relationsGrid.selectedItem != null? true : false}"/>
88 <mx:LinkButton label="Add to" click="addToRelation()"/>
93 <mx:VBox width="100%" height="100%" label="Members" id="membersVBox" initialize="checkMembers()" verticalGap="1">
94 <mx:Label id="membersText" text="Relation Members"/>
95 <mx:DataGrid editable="true" width="100%" height="100%" id="membersGrid"
96 dragEnabled="true" dragMoveEnabled="true" dropEnabled="true">
98 <mx:DataGridColumn editable="false" dataField="type" headerText="Type"/>
99 <mx:DataGridColumn editable="false" dataField="id" headerText="ID"/>
100 <mx:DataGridColumn editable="true" dataField="role" headerText="Role">
101 <mx:itemEditor><fx:Component><mx:TextInput restrict=" -" /></fx:Component></mx:itemEditor>
109 <mx:HBox width="100%">
110 <mx:LinkBar dataProvider="{stack}"/>
111 <mx:Spacer width="100%"/>
113 icon="@Embed('../../../embedded/view_tabbed.png')"
114 disabledIcon="@Embed('../../../embedded/view_tabbed_disabled.png')"
115 click="setEditorStackUI(true)" id="tabNavigatorLabel" paddingTop="6"
116 toolTip="Show in tabs"
117 enabled="{editorStack is Accordion && stack.selectedIndex==0}" />
119 icon="@Embed('../../../embedded/view_accordion.png')"
120 disabledIcon="@Embed('../../../embedded/view_accordion_disabled.png')"
121 click="setEditorStackUI(false)" id="accordionLabel" paddingTop="6"
122 toolTip="Show in sliding windows"
123 enabled="{editorStack is TabNavigator && stack.selectedIndex==0}" />
126 <mx:HBox width="100%">
127 <mx:Label id="entityDisplayID" click="new HistoryDialog().init(selectedEntity);" paddingLeft="4" paddingBottom="3">
128 <mx:htmlText><![CDATA[<i>No Selection</i>]]></mx:htmlText>
130 <mx:PopUpButton id="entitySelectButton" arrowButtonWidth="12" paddingLeft="0" paddingRight="0"
131 width="12" height="12" enabled="false"
132 creationComplete="{createEntityMenu(PopUpButton(event.target));}" />
137 <!-- Multiple selection -->
139 <mx:VBox id="multiplePanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
140 <potlatch2:TagGrid id="multiAdvancedTagGrid" width="100%" height="75%" />
141 <mx:HBox horizontalAlign="right" width="100%">
142 <mx:LinkButton label="Delete" click="multiAdvancedTagGrid.removeTag()" enabled="{multiAdvancedTagGrid.selectedItem != null? true : false}"/>
143 <mx:LinkButton label="Add" click="multiAdvancedTagGrid.addNewTag()" />
146 <mx:DataGrid editable="true" width="100%" height="25%" id="multiRelationsGrid"
147 doubleClickEnabled="true"
148 itemDoubleClick="editRelation(multiRelationsGrid.selectedItem.id)"
149 doubleClick="if (event.target.parent==multiRelationsGrid) { addToRelation(); }">
151 <mx:DataGridColumn editable="false" dataField="description" headerText="Relation">
154 <mx:Text x="4" y="0" width="100%" selectable="false" htmlText="{outerDocument.adaptHTMLDescription(data.description)}" height="20"/>
158 <mx:DataGridColumn editable="false" dataField="id_idx" headerText="ID"/>
159 <mx:DataGridColumn editable="true" dataField="role" headerText="Role">
160 <mx:itemEditor><fx:Component><mx:TextInput restrict=" -" /></fx:Component></mx:itemEditor>
162 <mx:DataGridColumn width="40" minWidth="40" editable="false">
165 <mx:HBox horizontalAlign="center" verticalAlign="middle">
166 <mx:PopUpButton arrowButtonWidth="12" paddingLeft="0" paddingRight="0" width="12" height="12"
167 open="{outerDocument.updateRelationMenu(event);}"
168 creationComplete="{outerDocument.createRelationMenu(PopUpButton(event.target));}"/>
169 <mx:Image source="@Embed('../../../embedded/delete_small.svg')"
170 click='event.stopPropagation();outerDocument.removeFromRelation(data.id);'
171 buttonMode="true" useHandCursor="true" width="12" height="12" />
178 <mx:HBox horizontalAlign="right" width="100%">
179 <mx:LinkButton label="Remove from" click="removeFromRelation(multiRelationsGrid.selectedItem.id)"
180 enabled="{multiRelationsGrid.selectedItem != null? true : false}"/>
181 <mx:LinkButton label="Add to" click="addToRelation()"/>
186 <!-- Multiple selection but items cannot be edited -->
188 <mx:VBox id="multipleInvalidPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
189 <mx:Text id="multipleInvalidPanelText" text="You have selected multiple items." width="100%" styleName="helpInfo" />
192 <!-- Generic marker panel -->
194 <mx:VBox id="markerPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
195 <sidepanel:MarkerPanel id="markerPanelContents" width="100%"/>
198 <!-- Bug editing panel -->
200 <mx:VBox id="bugPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
201 <sidepanel:BugPanel id="bugPanelContents" width="100%"/>
204 <!-- Background layer selection -->
206 <mx:VBox id="backgroundPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
207 <sidepanel:BackgroundPanel id="backgroundPanelContents" width="100%"/>
210 <!-- merge tags from background layer -->
212 <mx:VBox id="backgroundMergePanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
213 <sidepanel:BackgroundMergePanel id="backgroundMergePanelContents" width="100%" />
219 import net.systemeD.halcyon.connection.*;
220 import net.systemeD.halcyon.MapPaint;
221 import net.systemeD.potlatch2.EditController;
222 import net.systemeD.potlatch2.mapfeatures.*;
223 import net.systemeD.potlatch2.history.HistoryDialog;
224 import net.systemeD.potlatch2.mapfeatures.editors.*;
225 import net.systemeD.potlatch2.utils.*;
226 import net.systemeD.controls.CollapsiblePanel;
228 import mx.collections.*;
229 import mx.containers.VBox;
230 import mx.containers.HBox;
231 import mx.containers.TabNavigator;
232 import mx.containers.Accordion;
235 import mx.managers.PopUpManager;
236 import mx.controls.Menu;
237 import mx.controls.Alert;
238 import flash.geom.Point;
240 import mx.events.DragEvent;
241 import mx.managers.DragManager;
242 import mx.core.DragSource;
243 import mx.controls.TabBar;
244 import spark.components.Form;
245 import spark.layouts.FormLayout;
246 // import flexlib.containers.SuperTabNavigator;
248 [Bindable] [Embed(source="../../../embedded/tab_basic.png" )] private var tabIconBasic:Class;
249 [Bindable] [Embed(source="../../../embedded/tab_address.png" )] private var tabIconAddress:Class;
250 [Bindable] [Embed(source="../../../embedded/tab_cycle.png" )] private var tabIconCycle:Class;
251 [Bindable] [Embed(source="../../../embedded/tab_details.png" )] private var tabIconDetails:Class;
252 [Bindable] [Embed(source="../../../embedded/tab_restrictions.png")] private var tabIconRestrictions:Class;
253 [Bindable] [Embed(source="../../../embedded/tab_transport.png" )] private var tabIconTransport:Class;
254 [Bindable] [Embed(source="../../../embedded/tab_walk.png" )] private var tabIconWalk:Class;
255 private var tabIcons:Object= { Basic:tabIconBasic, Details:tabIconDetails, Address:tabIconAddress, Walk:tabIconWalk, Cycle:tabIconCycle,
256 Transport:tabIconTransport, Restrictions:tabIconRestrictions};
258 private var editorStackTabNavigator:TabNavigator;
259 private var editorStackAccordion:Accordion;
260 [Bindable] private var editorStack:Container;
262 public var mapFeatures:MapFeatures;
263 public var controller:EditController;
264 private var selectedEntity:Entity;
265 private var relatedEntities:Array;
266 private var connection:Connection;
267 private var currentCategorySelector:CategorySelector;
268 private var categorySelectors:Object = {}; // hash of categorySelectors for each limitType
269 private var categorySelectorEntity:Entity; // entity used to draw the most recent categorySelector
270 private var categorySelectorLimitType:String; // limit type used to draw the most recent categorySelector
271 private var feature:Feature = null;
273 private var rowData:Object; // relation membership reference, needed so it's accessible from relation actions menu
275 /** Set the entity for the tag viewer, and refresh all the UI
276 * If autoselect is true, then the tag viewer may choose to show a 'parent' multipolygon element instead
278 public function setEntity(entities:Array, layer:MapPaint=null, autoselect:Boolean=true):void {
279 UIComponent.suspendBackgroundProcessing();
282 var firstSelected:Entity=null;
283 if (entities.length==1) { firstSelected=entities[0]; }
285 if (selectedEntity!=firstSelected && selectedEntity!=null) {
286 selectedEntity.removeEventListener(Connection.TAG_CHANGED, tagChanged);
287 selectedEntity.removeEventListener(Connection.ADDED_TO_RELATION, addedToRelation);
288 selectedEntity.removeEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation);
289 if (selectedEntity is EntityCollection) EntityCollection(selectedEntity).releaseListeners();
292 if (entities.length==0) {
293 // Nothing selected, so show drag-and-drop panel
294 sidebar.selectedChild = dndPanel;
297 } else if (entities.length==1) {
298 // Single entity selected, so show tag panel
299 var lastSelected:Entity = selectedEntity;
301 relatedEntities = firstSelected.getRelatedEntities();
302 selectedEntity = relatedEntities[0];
304 selectedEntity = firstSelected;
306 if (lastSelected!=selectedEntity) { relatedEntities[0].addEventListener(Connection.TAG_CHANGED, tagChanged, false, 0, true); }
308 updateEntityMenuDataProvider();
309 connection=firstSelected.connection;
310 if (entityDisplayID!=null) {
311 setupAdvanced(selectedEntity);
312 updateEntityDisplay(selectedEntity);
314 if (selectedEntity is Relation) { stack.addChild(membersVBox); }
315 else if (membersVBox.parent==stack) { stack.removeChild(membersVBox); }
316 if (selectedEntity is Marker && connection is BugConnection) {
317 bugPanelContents.init(selectedEntity, BugConnection(connection));
318 sidebar.selectedChild = bugPanel;
319 } else if (selectedEntity is Marker) {
320 markerPanelContents.init(selectedEntity, layer);
321 sidebar.selectedChild = markerPanel;
322 } else if (connection is SnapshotConnection) {
323 backgroundPanelContents.init(selectedEntity);
324 sidebar.selectedChild = backgroundPanel;
326 refreshFeatureIcon();
328 sidebar.selectedChild = tagsPanel;
331 } else if (entities.length==2
332 && xor(entities[0].connection is SnapshotConnection, entities[1].connection is SnapshotConnection)
333 && xor(!controller.map.getLayerForEntity(entities[0]).isBackground, !controller.map.getLayerForEntity(entities[1]).isBackground) ) {
334 backgroundMergePanelContents.init(entities);
335 sidebar.selectedChild = backgroundMergePanel;
336 // ** FIXME: do we need to set selectedEntity here?
338 } else if(isMultipleEditable(entities)) {
339 selectedEntity = new EntityCollection(entities);
340 selectedEntity.addEventListener(Connection.TAG_CHANGED, tagChanged, false, 0, true);
341 sidebar.selectedChild = multiplePanel;
342 setupMultiAdvanced(selectedEntity);
343 connection=entities[0].connection;
346 //The selection contains elements which can't be edited all together.
347 sidebar.selectedChild = multipleInvalidPanel;
350 UIComponent.resumeBackgroundProcessing();
353 private function xor(a:Boolean, b:Boolean):Boolean {
354 return ( a || b) && !(a && b);
357 private function refreshFeatureIcon():void {
358 var oldFeature:Feature = feature;
359 var oldEntity:Entity = categorySelectorEntity;
361 feature = selectedEntity == null ? null : mapFeatures.findMatchingFeature(selectedEntity);
362 if (oldFeature==feature &&
363 categorySelectorEntity==selectedEntity &&
364 categorySelectorLimitType==limitType(selectedEntity)) {
365 updateCategoryImageAndText(selectedEntity,feature);
369 categorySelectorEntity=selectedEntity;
370 categorySelectorLimitType=limitType(selectedEntity);
371 if ( oldFeature != null ) { oldFeature.removeEventListener("imageChanged", featureImageChanged); }
372 if ( feature != null ) { feature.addEventListener("imageChanged", featureImageChanged); }
373 setCategorySelector(selectedEntity, feature);
376 private function featureImageChanged(event:Event):void {
377 setCategorySelector(selectedEntity, feature);
381 /** Set the icon, categorySelector and help text for the current entity. */
382 private function setCategorySelector(entity:Entity, feature:Feature):void {
383 // Remove the "user has selected something" event listener from previous categorySelector,
384 // and make it invisible because Flex is crap at updating click areas otherwise
385 if (currentCategorySelector) {
386 currentCategorySelector.removeEventListener("selectedType", changeFeatureType);
387 currentCategorySelector.visible=false;
390 // Have we cached the categorySelector for this limitType? If not, create one
391 var lt:String=limitType(entity);
392 if (!categorySelectors[lt]) {
393 categorySelectors[lt]=new CategorySelector();
394 categorySelectors[lt].setLimitTypes(lt);
396 currentCategorySelector=categorySelectors[lt];
397 currentCategorySelector.addEventListener("selectedType", changeFeatureType, false, 0, true);
399 updateCategoryImageAndText(entity,feature);
400 currentCategorySelector.setSelectedFeature(feature);
402 // Set it as the popup, and make sure it's visible
403 popupChange.popUp=currentCategorySelector;
404 currentCategorySelector.visible=true;
407 private function updateCategoryImageAndText(entity:Entity, feature:Feature):void {
409 iconImage.source = feature.image;
410 iconText.htmlText = feature.htmlDetails(entity);
411 popupChange.label = feature.name;
412 helpLabel.visible = feature.hasHelpURL();
415 iconImage.source = null;
416 popupChange.label = "unknown";
417 helpLabel.visible = false;
419 iconText.htmlText = "<i>Nothing selected</i>";
421 } else if (entity.hasTags() || entity.parentRelations.length>0) {
422 iconText.htmlText = "<b>Not recognised</b><br/><font size='10pt'>Try looking at the tags under the advanced properties</font>";
423 setSummary(tagSummary(entity));
425 iconText.htmlText = "<b>No tags set</b><br/><font size='10pt'>Please use the menu below to define what this "+entity.getType()+" is</font>";
431 /** Construct an HTML string that summarises the tags and relation memberships of an object. */
432 private function tagSummary(entity:Entity):String {
434 var tags:Object=entity.getTagsCopy();
435 var len:uint=entity.getTagList().length;
439 expl.push("<b>"+tags['name']+"</b>"); delete tags['name']; len--;
441 for (var k:String in tags) {
442 if (nounKeys.indexOf(k)>-1) {
443 expl.push("<b>"+k+"</b>="+tags[k]); len--;
448 case 1: expl.push("and 1 other tag"); break;
450 default: expl.push("and "+len+" other tags"); break;
454 t=entity.getTagList().toString();
459 for each (var rel:Relation in entity.parentRelations) {
460 tags=rel.getTagsHash();
461 if (tags['type'] && tags['name']) { expl.push("<b>"+tags['type']+"</b> ("+tags['name']+")"); }
462 else if (tags['type']) { expl.push("<b>"+tags['type']+"</b>"); }
463 else if (tags['name']) { expl.push(tags['name']); }
464 else if (tags['ref' ]) { expl.push(tags['ref']); }
465 else { expl.push('unknown'); }
468 // Assemble complete string
469 return "<p>Tags: "+t+(expl.length ? "<br>Relations: "+expl.join('; ') : '')+" <b>(<a href='event:openAdvanced'>edit</a>)</b></p>";
472 /** List of 'noun' keys (i.e. those that define the type of the object, rather than its properties). FIXME: should be defined in a config file. */
473 private static var nounKeys:Array=['amenity','barrier','boundary','building','construction','highway','historic','landuse','leisure',
474 'man_made','natural','office','place','power','railway','service','shop','tourism','traffic_calming','waterway'];
476 private function setSummary(text:String):void {
477 featureSummary.htmlText=text;
478 summaryContainer.visible=(text!='');
481 private function isMultipleEditable(entities:Array):Boolean {
482 for each(var entity:Entity in entities) {
483 if(!(entity is Node || entity is Way))
489 private function limitType(entity:Entity):String {
490 if (entity is Node ) return "point";
491 else if (entity is Way ) return Way(entity).isArea() ? "area" : "line";
492 else if (entity is Relation) return Relation(entity).getRelationType()=='multipolygon' ? "area" : "relation";
496 private var tabComponents:Object = {};
497 private var subpanelComponents:Object = {};
499 private function initialiseEditors():void {
500 editorStack.removeAllChildren();
501 if ( selectedEntity == null || feature == null )
504 var basicEditorBox:VBox = createEditorBox();
505 basicEditorBox.label = "Basic";
506 basicEditorBox.icon=tabIconBasic;
507 editorStack.addChild(basicEditorBox);
509 var tabs:Object = {};
510 var tabList:Array = [];
512 var subpanels:Object = {};
513 subpanelComponents = {};
515 // First create the tabs
516 for each (var factory:EditorFactory in feature.editors) {
517 var category:String = factory.category;
519 var tab:VBox = tabs[category];
521 tab = createEditorBox();
522 tab.label = category;
523 if (tabIcons[category]) tab.icon=tabIcons[category];
524 tabs[category] = tab;
530 // Put the tabs on-screen in order
531 tabList.sort(sortCategories,16);
532 for each (tab in tabList) {
533 editorStack.addChild(tab);
534 tabComponents[tab] = [];
537 // Then add the individual editors to them
538 for each (factory in feature.editors) {
540 // Add to basic editor box first
541 if ( factory.presence.isEditorPresent(factory, selectedEntity, null) ) {
542 var editor:DisplayObject = factory.createEditorInstance(selectedEntity);
543 if (editor) Form(basicEditorBox.getChildByName("form")).addElement(UIComponent(editor));
546 // Then prepare to add to category panel
547 category=factory.category;
548 if (factory.category=='') continue;
549 var catEditor:DisplayObject = factory.createEditorInstance(selectedEntity);
550 if (!catEditor) continue;
553 // Create subcategory panel if needed
554 if (factory.subcategory) {
555 var subcategory:String = factory.subcategory;
556 if (!subpanels[category]) { subpanels[category]={}; }
557 var subpanel:CollapsiblePanel = subpanels[category][subcategory];
559 subpanel=new CollapsiblePanel(false);
560 subpanel.percentWidth=100;
561 subpanel.styleName="subcategoryPanel";
562 subpanel.title=subcategory;
563 subpanels[category][subcategory]=subpanel;
564 addConstrainedForm(subpanel);
565 tabComponents[tab].push(subpanel);
567 Form(subpanel.getChildByName("form")).addElement(UIComponent(catEditor));
569 tabComponents[tab].push(catEditor);
574 // ** FIXME: Order probably shouldn't be hard-coded, but configurable
575 private static var categoryOrder:Array=["Restrictions","Transport","Cycle","Walk","Address","Details"]; // reverse order
576 private function sortCategories(a:VBox,b:VBox):int {
577 var a1:int=categoryOrder.indexOf(a.label);
578 var a2:int=categoryOrder.indexOf(b.label);
579 if (a1<a2) { return 1; }
580 else if (a1>a2) { return -1; }
584 private function createEditorBox():VBox {
585 var box:VBox = new VBox();
586 box.percentWidth = 100;
587 box.percentHeight = 100;
588 box.styleName = "dndEditorContainer";
589 addConstrainedForm(box);
593 private function addConstrainedForm(parentObject:DisplayObjectContainer):void {
594 var form:Form = new Form();
596 form.percentWidth=100;
597 form.addEventListener(mx.events.ResizeEvent.RESIZE, formResizeHandler, false, 0, true);
598 parentObject.addChild(form);
599 if (parentObject.width>0) form.maxWidth=parentObject.width;
602 private function formResizeHandler(e:Event):void {
603 var form:Form=Form(e.target);
604 if (form.parent.width>0) form.maxWidth=form.parent.width;
607 private function ensureEditorsPopulated(tab:VBox):void {
608 var components:Array = tabComponents[tab];
609 var form:Form=Form(tab.getChildByName("form"));
610 if ( components == null || tab == null || form.numElements >= components.length ) return;
611 for each (var component:DisplayObject in components ) {
612 form.addElement(UIComponent(component));
616 private function initEditorStackUIs():void {
617 editorStackTabNavigator = new TabNavigator();
618 editorStackTabNavigator.creationPolicy="auto";
619 editorStackTabNavigator.percentWidth=100;
620 editorStackTabNavigator.percentHeight=100;
621 editorStackTabNavigator.styleName="dndStackTab";
623 editorStackAccordion = new Accordion();
624 editorStackAccordion.percentWidth=100;
625 editorStackAccordion.percentHeight=100;
626 editorStackAccordion.creationPolicy="auto";
627 editorStackAccordion.styleName="dndStackAccordion";
628 /* FIXME: the accordion icons should be right-aligned. See:
629 http://www.kristoferjoseph.com/blog/2008/11/06/positioning-the-flex-accordion-header-icon
630 http://blog.flexexamples.com/2007/09/13/changing-text-alignment-in-an-flex-accordion-header/
633 setEditorStackUI(true);
636 private function setEditorStackUI(isTabbed:Boolean):void {
637 var children:Array=[]; var i:uint;
640 // blank existing component
641 editorStack.removeEventListener("change",editorStackUIChange);
642 editorStack.removeEventListener("updateComplete",editorStackUIUpdate);
643 editorStack.removeAllChildren();
644 editorContainer.removeChildAt(1);
647 // replace with new component
648 editorStack = (isTabbed ? editorStackTabNavigator : editorStackAccordion) as Container;
649 editorContainer.addChild(editorStack);
651 // re-add children and listeners
653 editorStack.addEventListener("change",editorStackUIChange);
654 editorStack.addEventListener("updateComplete",editorStackUIUpdate);
657 private function editorStackUIChange(event:Event):void {
658 ensureEditorsPopulated(IndexChangedEvent(event).relatedObject as VBox);
661 private function editorStackUIUpdate(event:Event):void {
662 if (editorStack is TabNavigator) {
663 var e:TabNavigator = editorStack as TabNavigator;
664 if (e.selectedIndex<0) { return; }
665 for (var i:uint=0; i<e.numChildren; i++) {
666 e.getTabAt(i).selected=(i==e.selectedIndex);
672 public function togglePanel():void {
673 if (stack.selectedChild==editorContainer && selectedEntity!=null) {
674 stack.selectedChild=advancedContainer;
675 } else if (stack.selectedChild!=editorContainer) {
676 stack.selectedChild=editorContainer;
680 public function selectAdvancedPanel():void {
682 stack.selectedChild=advancedContainer;
685 private function checkAdvanced():void {
686 if ( selectedEntity != null ) {
687 setupAdvanced(selectedEntity);
688 updateEntityDisplay(selectedEntity);
692 private var listeningToRelations:Array = [];
694 private function updateEntityDisplay(entity:Entity):void {
695 if ( entity == null ) {
696 entityDisplayID.htmlText = "";
698 var entityText:String = "xx";
699 if ( entity is Node ) entityText = "Node";
700 else if ( entity is Way ) entityText = "Way";
701 else if ( entity is Relation ) entityText = "Relation";
702 entityDisplayID.htmlText = entityText+": <b>"+entity.id+"</b> "+(entity.status ? entity.status : '');
706 [bindable] private var entityMenuList:ArrayCollection = new ArrayCollection([]);
708 // Create drop-down entity select menu (for multipolygons)
709 private function createEntityMenu(button:PopUpButton):void {
710 var menu:Menu = new Menu();
711 menu.dataProvider = entityMenuList;
713 menu.addEventListener("itemClick", selectEntityMenu);
716 // Update entries in entity select menu
717 private function updateEntityMenuDataProvider():void {
718 entityMenuList.removeAll();
719 for each (var e:Entity in relatedEntities) {
720 entityMenuList.addItem({ label: e.getType()+" "+e.id, data: e });
722 entitySelectButton.enabled = relatedEntities.length>1;
725 // An entity has been selected from the drop-down menu
726 private function selectEntityMenu(event:MenuEvent):void {
727 selectedEntity = event.item.data;
728 setEntity([selectedEntity],null,false);
731 private function setupAdvanced(entity:Entity):void {
732 if (!advancedTagGrid) advancedContainer.createComponentsFromDescriptors(); // if Flex hasn't created it, force it
733 advancedTagGrid.init(entity);
734 removeRelationListeners();
736 if ( entity == null ) {
737 relationsGrid.dataProvider = null;
739 resetRelationsGrid(entity);
740 entity.addEventListener(Connection.ADDED_TO_RELATION, addedToRelation);
741 entity.addEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation);
745 private function setupMultiAdvanced(entity:Entity):void {
746 multiAdvancedTagGrid.init(entity);
747 resetRelationsGrid(entity);
748 entity.addEventListener(Connection.ADDED_TO_RELATION, addedToRelation, false, 0, true);
749 entity.addEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation, false, 0, true);
752 public function addNewTag():void {
753 if (sidebar.selectedChild==multiplePanel) { multiAdvancedTagGrid.addNewTag(); }
754 else if (stack.selectedChild==advancedContainer) { advancedTagGrid.addNewTag(); }
757 private function addedToRelation(event:RelationMemberEvent):void {
758 resetRelationsGrid(selectedEntity);
759 refreshFeatureIcon();
762 private function removedFromRelation(event:RelationMemberEvent):void {
763 resetRelationsGrid(selectedEntity);
764 refreshFeatureIcon();
767 private function removeRelationListeners():void {
768 for each( var rel:Relation in listeningToRelations ) {
769 rel.removeEventListener(Connection.TAG_CHANGED, relationTagChanged);
770 rel.removeEventListener(Connection.RELATION_MEMBER_ADDED, entityRelationMemberChanged);
771 rel.removeEventListener(Connection.RELATION_MEMBER_REMOVED, entityRelationMemberChanged);
773 listeningToRelations = [];
774 if (relationsGrid) relationsGrid.removeEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged);
775 if (multiRelationsGrid) multiRelationsGrid.removeEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged);
778 private function resetRelationsGrid(entity:Entity):void {
779 removeRelationListeners();
781 var instance:DataGrid=relationsGrid;
782 if (entity is EntityCollection) instance=multiRelationsGrid;
783 var memberships:Array = entity.getRelationMemberships();
784 for each (var m:Object in memberships) {
785 m.relation.addEventListener(Connection.TAG_CHANGED, relationTagChanged);
786 m.relation.addEventListener(Connection.RELATION_MEMBER_ADDED, entityRelationMemberChanged);
787 m.relation.addEventListener(Connection.RELATION_MEMBER_REMOVED, entityRelationMemberChanged);
788 listeningToRelations.push(m.relation);
790 instance.dataProvider = memberships;
791 instance.addEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged, false, -100);
794 private function relationRoleChanged(event:DataGridEvent):void {
795 if (!selectedEntity) return;
796 if (event.dataField != 'role') { return; } // shouldn't really happen
798 var relations:Array=event.currentTarget.dataProvider.toArray();
799 var props:Object=relations[event.rowIndex];
801 var relation:Relation=props['relation'];
802 var newRole:String=event.itemRenderer.data['role'];
804 if (selectedEntity is EntityCollection) {
805 if (newRole==EntityCollection.DIFFERENT) return;
806 for each (var entity:Entity in EntityCollection(selectedEntity).entities) {
807 var indexes:Array=relation.findEntityMemberIndexes(entity);
808 for each (var index:int in indexes) {
809 relation.setMember(index, new RelationMember(entity,newRole), MainUndoStack.getGlobalStack().addAction);
813 relation.setMember(props['index'], new RelationMember(selectedEntity,newRole), MainUndoStack.getGlobalStack().addAction);
817 private function relationTagChanged(event:TagEvent):void {
818 if (!selectedEntity) return;
819 resetRelationsGrid(selectedEntity);
820 refreshFeatureIcon();
823 private function entityRelationMemberChanged(event:RelationMemberEvent):void {
824 if (!selectedEntity) return;
825 resetRelationsGrid(selectedEntity);
828 private function checkMembers():void {
829 if (selectedEntity is Relation) {
830 setupMembers(selectedEntity as Relation);
834 private function setupMembers(rel:Relation):void {
835 var members:Array = [];
836 for (var i:int=0 ; i<rel.length; i++) {
837 var props:Object = {};
838 var member:RelationMember = rel.getMember(i);
839 props["id"] = member.entity.id;
840 props["type"] = member.entity.getType();
841 props["role"] = member.role;
845 membersGrid.dataProvider = members;
846 membersGrid.dataProvider.addEventListener('collectionChange', membersChange);
849 private function membersChange(event:Event):void {
850 // Dropping all the members and re-adding them isn't exactly optimal
851 // but is at least robust for any kind of change.
852 // Figuring out a better way is someone else's FIXME
854 var rel:Relation = selectedEntity as Relation;
855 var action:CompositeUndoableAction = new CompositeUndoableAction("Rearrange relation members for "+rel);
858 for (var i:int=rel.length-1 ; i>=0; i--) {
859 rel.removeMemberByIndex(i, action.push);
862 // add members in new order
863 for each(var memberObject:Object in membersGrid.dataProvider) {
865 var id:Number = memberObject.id;
866 switch (memberObject.type) {
867 case 'node': e = connection.getNode(id); break;
868 case 'way': e = connection.getWay(id); break;
869 case 'relation': e = connection.getRelation(id); break;
871 rel.appendMember(new RelationMember(e, memberObject.role), action.push);
873 MainUndoStack.getGlobalStack().addAction(action);
876 private function editRelation(id:Number):void {
877 var panel:RelationEditorPanel = RelationEditorPanel(
878 PopUpManager.createPopUp(Application(FlexGlobals.topLevelApplication), RelationEditorPanel, true));
879 panel.setRelation(connection.getRelation(id));
880 PopUpManager.centerPopUp(panel);
883 /** Create relation actions menu */
885 public function createRelationMenu(button:PopUpButton):void {
886 var menu:Menu = new Menu();
887 var dp:Object = [ {label: "Select all members"},
888 {label: "Deselect all members"},
889 {label: "Add selection to this relation", enabled: false},
890 {label: "Delete relation"},
891 {label: "Assign to F1"},
892 {label: "Assign to F2"},
893 {label: "Assign to F3"},
894 {label: "Assign to F4"} ];
895 menu.dataProvider = dp;
896 menu.addEventListener("itemClick", selectRelationMenu);
900 /** Enable 'add selection to...' entry only if some of the selection isn't in the relation.
901 Called each time the menu is clicked. */
903 public function updateRelationMenu(event:Event):void {
904 rowData=event.target.parent.data; // this makes it accessible from selectRelationMenu
905 var menu:Menu = Menu(event.target.popUp);
906 var enable:Boolean = false;
907 if (selectedEntity != null && selectedEntity is EntityCollection) {
908 // Enable only if some entities aren't a member of the relation
909 enable=!rowData.universal;
911 if (enable==menu.dataProvider[2].enabled) return;
912 menu.dataProvider[2].enabled=enable;
913 menu.invalidateList();
916 /** Do the action selected in the relation actions menu */
918 public function selectRelationMenu(event:MenuEvent):void {
919 var rel:Relation=rowData.relation;
921 var controller:EditController=FlexGlobals.topLevelApplication.theController;
922 switch (event.index) {
923 case 0: // Select all members
924 entities=selectedEntity.entities.concat(rel.memberEntities);
925 entities=entities.filter(function(e:*, i:int, arr:Array):Boolean { return arr.indexOf(e) == i } ); // remove duplicates
926 // ** FIXME: This is a really horrible way of changing the controller state
927 controller.setState(controller.findStateForSelection(entities));
930 case 1: // Deselect all members
931 entities=selectedEntity.entities;
932 entities=entities.filter(function(e:*, i:int, arr:Array):Boolean { return !e.hasParent(rel) } );
933 controller.setState(controller.findStateForSelection(entities));
936 case 2: // Add selection to this relation
937 var undo:CompositeUndoableAction=new CompositeUndoableAction("Remove selection from relations");
938 for each (var entity:Entity in selectedEntity.entities) {
939 if (!entity.hasParent(rel)) {
940 rel.appendMember(new RelationMember(entity,''), undo.push);
943 MainUndoStack.getGlobalStack().addAction(undo);
946 case 3: // Delete relation
947 var warning:String="This relation has # members. Deleting it will affect all of them and erase this $. Are you really sure?";
948 warning=warning.replace("#",rel.length).replace("$",rel.getRelationType());
949 Alert.show(warning,"Are you sure?",Alert.YES | Alert.CANCEL,null,
950 function(event:CloseEvent):void {
951 if (event.detail==Alert.CANCEL) return;
952 rel.remove(MainUndoStack.getGlobalStack().addAction);
953 } , null, Alert.CANCEL);
956 case 4: // Function key assignment
960 FunctionKeyManager.instance().setKey(event.index-3, 'Add to relation',String(rel.id));
965 private function tagChanged(event:TagEvent):void {
966 refreshFeatureIcon();
969 public function loadFeatures():void {
970 mapFeatures = MapFeatures.getInstance();
971 stack.removeChild(membersVBox); // remove by default, will be added if relation
974 /** Open up a new browser page, showing the help page as defined in Map Features XML file .*/
975 public function openDescription():void {
976 if ( feature != null && feature.hasHelpURL() )
977 navigateToURL(new URLRequest(feature.helpURL), "potlatch_help");
980 /** Slightly poshify the description by making it HTML.
981 In theory we could have embedded images here, but htmlText is completely broken. **/
982 public function adaptHTMLDescription(text:String):String {
983 text=text.replace(/^route (\w+) /,"<b>$1</b> ");
984 text=text.replace(/ncn (\d+)/,"<font color='#FF0000'>$1</font>");
985 text=text.replace(/rcn (\d+)/,"<font color='#00CCCC'>$1</font>");
986 text=text.replace(/lcn (\d+)/,"<font color='#0000FF'>$1</font>");
990 public function addToRelation():void {
991 new RelationSelectPanel().init(selectedEntity,new Object());
994 public function removeFromRelation(id:Number, index:int=-1):void {
995 var rel:Relation=connection.getRelation(id);
997 rel.removeMemberByIndex(index, MainUndoStack.getGlobalStack().addAction);
998 } else if (selectedEntity is EntityCollection) {
999 var undo:CompositeUndoableAction=new CompositeUndoableAction("Remove selection from relations");
1000 for each (var e:Entity in EntityCollection(selectedEntity).entities) rel.removeMember(e,undo.push);
1001 MainUndoStack.getGlobalStack().addAction(undo);
1005 public function changeFeatureType(event:Event):void {
1006 if ( selectedEntity == null )
1009 UIComponent.suspendBackgroundProcessing();
1010 var newFeature:Feature = currentCategorySelector.selectedType;
1011 var undoStack:Function = MainUndoStack.getGlobalStack().addAction;
1012 var action:CompositeUndoableAction = new CompositeUndoableAction(
1013 "Set "+selectedEntity.getType()+" "+selectedEntity.id+" to "+newFeature.name);
1014 selectedEntity.suspend();
1016 // build a list of tags that are editable in new feature
1017 var editableTags:Array = new Array();
1018 for each( var editor:EditorFactory in newFeature.editors ) {
1019 if ( editor is SingleTagEditorFactory ) {
1020 var singleTagEditor:SingleTagEditorFactory = editor as SingleTagEditorFactory;
1021 editableTags.push(singleTagEditor.key);
1025 // remove tags from the current feature
1026 // (but don't drop multipolygon tags)
1027 if ( feature != null ) {
1028 for each( var oldtag:Object in feature.tags ) {
1029 if (oldtag["k"]=='type' && oldtag["v"]=='multipolygon' && selectedEntity is Relation) { continue; }
1030 if ( editableTags.indexOf(oldtag["k"]) < 0 ) {
1031 selectedEntity.setTag(oldtag["k"], null, action.push);
1036 // set tags for new feature
1037 if ( newFeature != null ) {
1038 for each( var newtag:Object in newFeature.tags ) {
1039 selectedEntity.setTag(newtag["k"], newtag["v"], action.push);
1043 selectedEntity.resume();
1045 popupChange.close();
1046 initialiseEditors();
1047 UIComponent.resumeBackgroundProcessing();