///////////////////////////////////////////////////////////////////////////
// Copyright (c) 2010-2011 Esri. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///////////////////////////////////////////////////////////////////////////
package com.esri.viewer.utils
{

import com.esri.ags.components.IdentityManager;
import com.esri.ags.events.PortalEvent;
import com.esri.ags.portal.Portal;
import com.esri.ags.portal.supportClasses.PortalGroup;
import com.esri.ags.portal.supportClasses.PortalItem;
import com.esri.ags.portal.supportClasses.PortalQueryParameters;
import com.esri.ags.portal.supportClasses.PortalQueryResult;
import com.esri.viewer.AppEvent;
import com.esri.viewer.ConfigData;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.utils.Dictionary;

import mx.resources.ResourceManager;
import mx.rpc.AsyncResponder;
import mx.rpc.Fault;
import mx.rpc.events.FaultEvent;
import mx.utils.ObjectUtil;

public class PortalBasemapAppender extends EventDispatcher
{
    private const PORTAL_BASEMAP_APPENDER:String = "PortalBasemapAppender";

    private var configData:ConfigData;
    private var portalURL:String;

    private var portalItemOrder:Array;
    private var portalItemToLabel:Dictionary;
    private var processedArcGISBasemaps:Array;
    private var totalBasemaps:int;
    private var totalPossibleArcGISBasemaps:int;

    private var comparableDefaultBasemapObjects:Array;
    private var defaultBasemapTitle:String;

    private var cultureCode:String;

    public function PortalBasemapAppender(portalURL:String, configData:ConfigData)
    {
        this.portalURL = portalURL;
        this.configData = configData;
    }

    public function fetchAndAppendPortalBasemaps():void
    {
        const idMgrEnabled:Boolean = IdentityManager.instance.enabled;
        var portal:Portal = new Portal();
        // the Portal constructor enables the IdentityManager so restore it back to what it was
        IdentityManager.instance.enabled = idMgrEnabled;

        portal.addEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.addEventListener(FaultEvent.FAULT, portal_faultHandler);

        cultureCode = toCultureCode(ResourceManager.getInstance().localeChain[0]);
        portal.load(portalURL, cultureCode);
    }

    protected function portal_loadHandler(event:PortalEvent):void
    {
        var portal:Portal = event.target as Portal;
        portal.removeEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.removeEventListener(FaultEvent.FAULT, portal_faultHandler);

        comparableDefaultBasemapObjects = getComparableBasemapObjects(portal.info.defaultBasemap);
        var queryParams:PortalQueryParameters = PortalQueryParameters.forQuery(portal.info.basemapGalleryGroupQuery);
        portal.queryGroups(queryParams, new AsyncResponder(portal_queryGroupsResultHandler, portal_queryGroupsFaultHandler, portal));
    }

    protected function portal_queryGroupsResultHandler(queryResult:PortalQueryResult, portal:Portal):void
    {
        if (queryResult.results.length > 0)
        {
            var portalGroup:PortalGroup = queryResult.results[0];
            var queryParams:PortalQueryParameters = PortalQueryParameters.forItemsInGroup(portalGroup.id).withLimit(50).withSortField("name");
            portal.queryItems(queryParams, new AsyncResponder(portal_queryItemsResultHandler, portal_queryItemsFaultHandler));
        }
        else
        {
            dispatchComplete();
        }
    }

    private function portal_queryItemsResultHandler(queryResult:PortalQueryResult, token:Object = null):void
    {
        const resultItems:Array = queryResult.results;
        totalPossibleArcGISBasemaps = resultItems.length;
        portalItemOrder = [];
        portalItemToLabel = new Dictionary(true);
        processedArcGISBasemaps = [];
        totalBasemaps = configData.basemaps.length;
        for each (var portalItem:PortalItem in resultItems)
        {
            portalItemOrder.push(portalItem);
            portalItem.getJSONData(new AsyncResponder(portalItem_getJSONDataResultHandler,
                                                      portalItem_getJSONDataFaultHandler,
                                                      portalItem));
        }
    }

    private function portalItem_getJSONDataResultHandler(itemData:Object, item:PortalItem):void
    {
        createBasemapLayerObjectFromWebMapItemAndData(item, itemData);
        if (isDefaultBasemap(itemData.baseMap))
        {
            defaultBasemapTitle = itemData.baseMap.title;
        }
        updateTotalArcGISBasemaps();
    }

    private function createBasemapLayerObjectFromWebMapItemAndData(item:PortalItem, itemData:Object):void
    {
        if (!itemData)
        {
            return;
        }

        var basemapObject:Object = itemData.baseMap;
        var basemapLayerObjects:Array = basemapObject.baseMapLayers;
        if (!(basemapObject && basemapLayerObjects))
        {
            return;
        }

        var title:String = basemapObject.title;
        var iconURL:String = item.thumbnailURL;
        var existingBasemapLayerObject:Object = findBasemapLayerObjectById(title);
        if (existingBasemapLayerObject)
        {
            existingBasemapLayerObject.icon = iconURL;
            return;
        }

        portalItemToLabel[item] = title;
        var basemapLayerObject:Object = basemapLayerObjects[0];
        addBasemapLayerObject(baseMapLayerObjectToLayerXML(title,
                                                           basemapLayerObject,
                                                           iconURL));

        var totalBaseMapLayers:int = basemapLayerObjects.length;
        if (totalBaseMapLayers > 1)
        {
            basemapLayerObject = basemapLayerObjects[1];
            addBasemapLayerObject(baseMapLayerObjectToLayerXML(title,
                                                               basemapLayerObject,
                                                               iconURL));
        }
    }

    private function isDefaultBasemap(basemapObject:Object):Boolean
    {
        var comparableBasemapObjects:Array = getComparableBasemapObjects(basemapObject);

        return (ObjectUtil.compare(comparableBasemapObjects, comparableDefaultBasemapObjects) == 0);
    }

    private function getComparableBasemapObjects(basemapObject:Object):Array
    {
        var basemapLayerObjects:Array = basemapObject.baseMapLayers;
        var comparableBasemapObjects:Array = [];
        var comparableBasemapLayerObject:Object;

        for each (var basemapLayerObject:Object in basemapLayerObjects)
        {
            comparableBasemapLayerObject = {};

            if (basemapLayerObject.url)
            {
                comparableBasemapLayerObject.url = basemapLayerObject.url;
            }
            if (basemapLayerObject.type)
            {
                comparableBasemapLayerObject.type = basemapLayerObject.type;
            }

            comparableBasemapObjects.push(comparableBasemapLayerObject);
        }

        return comparableBasemapObjects;
    }

    private function findBasemapLayerObjectById(id:String):Object
    {
        var layerObjectResult:Object;

        var basemapLayerObjects:Array = configData.basemaps;
        for each (var layerObject:Object in basemapLayerObjects)
        {
            if (layerObject.layer && (layerObject.layer.id == id))
            {
                layerObjectResult = layerObject;
                break;
            }
        }

        return layerObjectResult;
    }

    private function updateTotalArcGISBasemaps():void
    {
        totalPossibleArcGISBasemaps--;
        if (totalPossibleArcGISBasemaps == 0)
        {
            addArcGISBasemapsToConfig();
            dispatchComplete();
        }
    }

    private function dispatchComplete():void
    {
        dispatchEvent(new Event(Event.COMPLETE));
    }

    private function addArcGISBasemapsToConfig():void
    {
        var hasBasemaps:Boolean = (configData.basemaps.length > 0);

        if (!hasBasemaps)
        {
            if (defaultBasemapTitle)
            {
                setDefaultBasemapVisible();
            }
            else
            {
                setFirstBasemapVisible();
            }
        }

        addBasemapsInOrder();
    }

    private function setDefaultBasemapVisible():void
    {
        for each (var layerObject:Object in processedArcGISBasemaps)
        {
            if (defaultBasemapTitle == layerObject.label)
            {
                layerObject.visible = true;
            }
        }
    }

    private function setFirstBasemapVisible():void
    {
        if (!portalItemOrder || portalItemOrder.length == 0)
        {
            return;
        }

        var firstBasemapLabel:String = portalItemToLabel[portalItemOrder[0]];
        for each (var layerObject:Object in processedArcGISBasemaps)
        {
            if (layerObject.label == firstBasemapLabel)
            {
                layerObject.visible = true;
            }
        }
    }

    private function addBasemapsInOrder():void
    {
        for each (var portalItem:PortalItem in portalItemOrder)
        {
            for each (var layerObject:Object in processedArcGISBasemaps)
            {
                if (layerObject.label == portalItemToLabel[portalItem])
                {
                    configData.basemaps.push(layerObject);
                }
            }
        }
    }

    private function addBasemapLayerObject(layerXML:XML):void
    {
        if (layerXML)
        {
            processedArcGISBasemaps.push(LayerObjectUtil.getLayerObject(layerXML,
                                                                        totalBasemaps++,
                                                                        false,
                                                                        configData.bingKey));
        }
    }

    private function baseMapLayerObjectToLayerXML(title:String, basemapLayerObject:Object, iconURL:String = null):XML
    {
        var layerXML:XML;
        const url:String = basemapLayerObject.url;
        const type:String = basemapLayerObject.type;

        if (url)
        {
            layerXML = createLayerXML(title, "tiled", iconURL, url, basemapLayerObject.opacity, false);
        }
        else if (isNonEsriType(type))
        {
            layerXML = createNonEsriLayerXML(title, iconURL, basemapLayerObject, false, type);
        }

        return layerXML;
    }

    private function createLayerXML(title:String, type:String, iconURL:String, url:String, alpha:Number, visible:Boolean):XML
    {
        return <layer label={title}
                type={type}
                icon={iconURL}
                url={url}
                alpha={alpha}
                visible={visible}/>;
    }

    private function isNonEsriType(type:String):Boolean
    {
        return type == "OpenStreetMap" ||
            (isBingBasemap(type) && hasBingKey());
    }

    private function createNonEsriLayerXML(title:String, iconURL:String, basemapLayerObject:Object, visible:Boolean, type:String):XML
    {
        var layerXML:XML = <layer label={title}
                icon={iconURL}
                type={toViewerNonEsriLayerType(basemapLayerObject.type)}
                alpha={basemapLayerObject.opacity}
                visible={visible}/>;

        if (isBingBasemap(type))
        {
            layerXML.@style = mapBingStyleFromBasemapType(type);
            layerXML.@culture = cultureCode;
        }

        return layerXML;
    }

    private function toViewerNonEsriLayerType(type:String):String
    {
        var viewerType:String;
        if (type == "OpenStreetMap")
        {
            viewerType = "osm";
        }
        else if (isBingBasemap(type))
        {
            viewerType = "bing";
        }

        return viewerType;
    }

    private function isBingBasemap(type:String):Boolean
    {
        return type && type.indexOf('BingMaps') > -1;
    }

    private function hasBingKey():Boolean
    {
        var bingKey:String = configData.bingKey;
        return (bingKey != null && bingKey.length > 0);
    }

    private function mapBingStyleFromBasemapType(type:String):String
    {
        if (type == 'BingMapsAerial')
        {
            return 'aerial';
        }
        else if (type == 'BingMapsHybrid')
        {
            return 'aerialWithLabels';
        }
        else
        {
            //default - BingMapsRoad
            return 'road';
        }
    }

    private function portalItem_getJSONDataFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.dispatch(AppEvent.APP_ERROR,
                          LocalizationUtil.getDefaultString("couldNotFetchBasemapData",
                                                            fault.faultString));
        updateTotalArcGISBasemaps();
    }

    private function portal_queryGroupsFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotQueryPortal"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function portal_queryItemsFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotQueryPortalItems"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function portal_faultHandler(event:FaultEvent):void
    {
        var portal:Portal = event.target as Portal;
        portal.removeEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.removeEventListener(FaultEvent.FAULT, portal_faultHandler);

        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotConnectToPortal"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function toCultureCode(locale:String):String
    {
        return locale ? locale.replace('_', '-') : locale;
    }
}

}

///////////////////////////////////////////////////////////////////////////
// Copyright (c) 2010-2011 Esri. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///////////////////////////////////////////////////////////////////////////
package com.esri.viewer.utils
{

import com.esri.ags.components.IdentityManager;
import com.esri.ags.events.PortalEvent;
import com.esri.ags.portal.Portal;
import com.esri.ags.portal.supportClasses.PortalGroup;
import com.esri.ags.portal.supportClasses.PortalItem;
import com.esri.ags.portal.supportClasses.PortalQueryParameters;
import com.esri.ags.portal.supportClasses.PortalQueryResult;
import com.esri.ags.tasks.JSONTask;
import com.esri.viewer.AppEvent;
import com.esri.viewer.ConfigData;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.net.URLVariables;
import flash.utils.Dictionary;

import mx.resources.ResourceManager;
import mx.rpc.AsyncResponder;
import mx.rpc.Fault;
import mx.rpc.events.FaultEvent;
import mx.utils.ObjectUtil;

public class PortalBasemapAppender extends EventDispatcher
{
    private const PORTAL_BASEMAP_APPENDER:String = "PortalBasemapAppender";

    private var configData:ConfigData;
    private var portalURL:String;

    private var portalItemOrder:Array;
    private var portalItemToLabel:Dictionary;
    private var processedArcGISBasemaps:Array;
    private var totalBasemaps:int;
    private var totalPossibleArcGISBasemaps:int;

    private var comparableDefaultBasemapObjects:Array;
    private var defaultBasemapTitle:String;

    private var cultureCode:String;

    public function PortalBasemapAppender(portalURL:String, configData:ConfigData)
    {
        this.portalURL = portalURL;
        this.configData = configData;
    }

    public function fetchAndAppendPortalBasemaps():void
    {
        const idMgrEnabled:Boolean = IdentityManager.instance.enabled;
        var portal:Portal = new Portal();
        // the Portal constructor enables the IdentityManager so restore it back to what it was
        IdentityManager.instance.enabled = idMgrEnabled;

        portal.addEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.addEventListener(FaultEvent.FAULT, portal_faultHandler);

        cultureCode = toCultureCode(ResourceManager.getInstance().localeChain[0]);
        portal.load(portalURL, cultureCode);
    }

    protected function portal_loadHandler(event:PortalEvent):void
    {
        var portal:Portal = event.target as Portal;
        portal.removeEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.removeEventListener(FaultEvent.FAULT, portal_faultHandler);

        comparableDefaultBasemapObjects = getComparableBasemapObjects(portal.info.defaultBasemap);
        var queryParams:PortalQueryParameters = PortalQueryParameters.forQuery(portal.info.basemapGalleryGroupQuery);
        portal.queryGroups(queryParams, new AsyncResponder(portal_queryGroupsResultHandler, portal_queryGroupsFaultHandler, portal));
    }

    protected function portal_queryGroupsResultHandler(queryResult:PortalQueryResult, portal:Portal):void
    {
        if (queryResult.results.length > 0)
        {
            var portalGroup:PortalGroup = queryResult.results[0];
            var queryParams:PortalQueryParameters = PortalQueryParameters.forItemsInGroup(portalGroup.id).withLimit(50).withSortField("name");
            portal.queryItems(queryParams, new AsyncResponder(portal_queryItemsResultHandler, portal_queryItemsFaultHandler));
        }
        else
        {
            dispatchComplete();
        }
    }

    private function portal_queryItemsResultHandler(queryResult:PortalQueryResult, token:Object = null):void
    {
        const resultItems:Array = queryResult.results;
        totalPossibleArcGISBasemaps = resultItems.length;
        portalItemOrder = [];
        portalItemToLabel = new Dictionary(true);
        processedArcGISBasemaps = [];
        totalBasemaps = configData.basemaps.length;
        for each (var item:PortalItem in resultItems)
        {
            processPortalItem(item);
        }
    }

    private function processPortalItem(item:PortalItem):void
    {
        if (item.type == PortalItem.TYPE_WEB_MAP)
        {
            portalItemOrder.push(item);
            processWebMapPortalItem(item);
        }
        else if (item.type == PortalItem.TYPE_MAP_SERVICE)
        {
            portalItemOrder.push(item);
            processMapServicePortalItem(item);
        }
        else
        {
            updateTotalArcGISBasemaps();
        }
    }

    private function processWebMapPortalItem(item:PortalItem):void
    {
        item.getJSONData(new AsyncResponder(item_getJSONDataResultHandler,
                                            item_getJSONDataFaultHandler,
                                            item));
    }

    private function item_getJSONDataResultHandler(itemData:Object, item:PortalItem):void
    {
        createBasemapLayerObjectFromWebMapItemAndData(item, itemData);
        if (isDefaultBasemap(itemData.baseMap))
        {
            defaultBasemapTitle = itemData.baseMap.title;
        }
        updateTotalArcGISBasemaps();
    }

    private function createBasemapLayerObjectFromWebMapItemAndData(item:PortalItem, itemData:Object):void
    {
        if (!itemData)
        {
            return;
        }

        var basemapObject:Object = itemData.baseMap;
        var basemapLayerObjects:Array = basemapObject.baseMapLayers;
        if (!(basemapObject && basemapLayerObjects))
        {
            return;
        }

        var title:String = basemapObject.title;
        var iconURL:String = item.thumbnailURL;
        var existingBasemapLayerObject:Object = findBasemapLayerObjectById(title);
        if (existingBasemapLayerObject)
        {
            existingBasemapLayerObject.icon = iconURL;
            return;
        }

        portalItemToLabel[item] = title;
        var basemapLayerObject:Object = basemapLayerObjects[0];
        addBasemapLayerObject(baseMapLayerObjectToLayerXML(title,
                                                           basemapLayerObject,
                                                           iconURL));

        var totalBaseMapLayers:int = basemapLayerObjects.length;
        if (totalBaseMapLayers > 1)
        {
            basemapLayerObject = basemapLayerObjects[1];
            addBasemapLayerObject(baseMapLayerObjectToLayerXML(title,
                                                               basemapLayerObject,
                                                               iconURL));
        }
    }

    private function isDefaultBasemap(basemapObject:Object):Boolean
    {
        var comparableBasemapObjects:Array = getComparableBasemapObjects(basemapObject);

        return (ObjectUtil.compare(comparableBasemapObjects, comparableDefaultBasemapObjects) == 0);
    }

    private function getComparableBasemapObjects(basemapObject:Object):Array
    {
        var basemapLayerObjects:Array = basemapObject.baseMapLayers;
        var comparableBasemapObjects:Array = [];
        var comparableBasemapLayerObject:Object;

        for each (var basemapLayerObject:Object in basemapLayerObjects)
        {
            comparableBasemapLayerObject = {};

            if (basemapLayerObject.url)
            {
                comparableBasemapLayerObject.url = basemapLayerObject.url;
            }
            if (basemapLayerObject.type)
            {
                comparableBasemapLayerObject.type = basemapLayerObject.type;
            }

            comparableBasemapObjects.push(comparableBasemapLayerObject);
        }

        return comparableBasemapObjects;
    }

    private function findBasemapLayerObjectById(id:String):Object
    {
        var layerObjectResult:Object;

        var basemapLayerObjects:Array = configData.basemaps;
        for each (var layerObject:Object in basemapLayerObjects)
        {
            if (layerObject.layer && (layerObject.layer.id == id))
            {
                layerObjectResult = layerObject;
                break;
            }
        }

        return layerObjectResult;
    }

    private function updateTotalArcGISBasemaps():void
    {
        totalPossibleArcGISBasemaps--;
        if (totalPossibleArcGISBasemaps == 0)
        {
            addArcGISBasemapsToConfig();
            dispatchComplete();
        }
    }

    private function dispatchComplete():void
    {
        dispatchEvent(new Event(Event.COMPLETE));
    }

    private function addArcGISBasemapsToConfig():void
    {
        var hasBasemaps:Boolean = (configData.basemaps.length > 0);

        if (!hasBasemaps)
        {
            if (defaultBasemapTitle)
            {
                setDefaultBasemapVisible();
            }
            else
            {
                setFirstBasemapVisible();
            }
        }

        addBasemapsInOrder();
    }

    private function setDefaultBasemapVisible():void
    {
        for each (var layerObject:Object in processedArcGISBasemaps)
        {
            if (defaultBasemapTitle == layerObject.label)
            {
                layerObject.visible = true;
            }
        }
    }

    private function setFirstBasemapVisible():void
    {
        if (!portalItemOrder || portalItemOrder.length == 0)
        {
            return;
        }

        var firstBasemapLabel:String = portalItemToLabel[portalItemOrder[0]];
        for each (var layerObject:Object in processedArcGISBasemaps)
        {
            if (layerObject.label == firstBasemapLabel)
            {
                layerObject.visible = true;
            }
        }
    }

    private function addBasemapsInOrder():void
    {
        for each (var portalItem:PortalItem in portalItemOrder)
        {
            for each (var layerObject:Object in processedArcGISBasemaps)
            {
                if (layerObject.label == portalItemToLabel[portalItem])
                {
                    configData.basemaps.push(layerObject);
                }
            }
        }
    }

    private function addBasemapLayerObject(layerXML:XML):void
    {
        if (layerXML)
        {
            processedArcGISBasemaps.push(LayerObjectUtil.getLayerObject(layerXML,
                                                                        totalBasemaps++,
                                                                        false,
                                                                        configData.bingKey));
        }
    }

    private function baseMapLayerObjectToLayerXML(title:String, basemapLayerObject:Object, iconURL:String = null):XML
    {
        var layerXML:XML;
        const url:String = basemapLayerObject.url;
        const type:String = basemapLayerObject.type;

        if (url)
        {
            layerXML = createLayerXML(title, "tiled", iconURL, url, basemapLayerObject.opacity, false);
        }
        else if (isNonEsriType(type))
        {
            layerXML = createNonEsriLayerXML(title, iconURL, basemapLayerObject, false, type);
        }

        return layerXML;
    }

    private function createLayerXML(title:String, type:String, iconURL:String, url:String, alpha:Number, visible:Boolean):XML
    {
        return <layer label={title}
                type={type}
                icon={iconURL}
                url={url}
                alpha={alpha}
                visible={visible}/>;
    }

    private function isNonEsriType(type:String):Boolean
    {
        return type == "OpenStreetMap" ||
            (isBingBasemap(type) && hasBingKey());
    }

    private function createNonEsriLayerXML(title:String, iconURL:String, basemapLayerObject:Object, visible:Boolean, type:String):XML
    {
        var layerXML:XML = <layer label={title}
                icon={iconURL}
                type={toViewerNonEsriLayerType(basemapLayerObject.type)}
                alpha={basemapLayerObject.opacity}
                visible={visible}/>;

        if (isBingBasemap(type))
        {
            layerXML.@style = mapBingStyleFromBasemapType(type);
            layerXML.@culture = cultureCode;
        }

        return layerXML;
    }

    private function toViewerNonEsriLayerType(type:String):String
    {
        var viewerType:String;
        if (type == "OpenStreetMap")
        {
            viewerType = "osm";
        }
        else if (isBingBasemap(type))
        {
            viewerType = "bing";
        }

        return viewerType;
    }

    private function isBingBasemap(type:String):Boolean
    {
        return type && type.indexOf('BingMaps') > -1;
    }

    private function hasBingKey():Boolean
    {
        var bingKey:String = configData.bingKey;
        return (bingKey != null && bingKey.length > 0);
    }

    private function mapBingStyleFromBasemapType(type:String):String
    {
        if (type == 'BingMapsAerial')
        {
            return 'aerial';
        }
        else if (type == 'BingMapsHybrid')
        {
            return 'aerialWithLabels';
        }
        else
        {
            //default - BingMapsRoad
            return 'road';
        }
    }

    private function item_getJSONDataFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.dispatch(AppEvent.APP_ERROR,
                          LocalizationUtil.getDefaultString("couldNotFetchBasemapData",
                                                            fault.faultString));
        updateTotalArcGISBasemaps();
    }

    private function processMapServicePortalItem(item:PortalItem):void
    {
        const urlVars:URLVariables = new URLVariables();
        urlVars.f = "json";
        var mapServiceMetadataRequest:JSONTask = new JSONTask(item.url);
        mapServiceMetadataRequest.execute(
            urlVars, new AsyncResponder(mapServiceRequest_resultHandler,
                                        mapServiceRequest_faultHandler,
                                        item));
    }

    private function mapServiceRequest_resultHandler(serviceMetadata:Object, item:PortalItem):void
    {
        createBasemapLayerObjectFromMapServiceItemAndData(item, serviceMetadata);
        updateTotalArcGISBasemaps();
    }

    private function createBasemapLayerObjectFromMapServiceItemAndData(item:PortalItem, serviceMetadata:Object):void
    {
        if (!serviceMetadata)
        {
            return;
        }

        var layerType:String = getLayerType(serviceMetadata, item);
        if (!layerType)
        {
            return;
        }

        var title:String = item.title;
        var iconURL:String = item.thumbnailURL;
        var existingBasemapLayerObject:Object = findBasemapLayerObjectById(title);
        if (existingBasemapLayerObject)
        {
            existingBasemapLayerObject.icon = iconURL;
            return;
        }

        portalItemToLabel[item] = title;
        addBasemapLayerObject(mapServicePortalItemToLayerXML(item, layerType));
    }

    private function getLayerType(serviceMetadata:Object, item:PortalItem):String
    {
        var layerType:String;

        if (serviceMetadata.singleFusedMapCache)
        {
            layerType = "tiled";
        }
        else if (serviceMetadata.bandCount)
        {
            layerType = "image";
        }
        else if (isNaN(Number(item.url.charAt(item.url.length - 1))))
        {
            layerType = "dynamic";
        }
        else
        {
            layerType = "feature";
        }

        return layerType;
    }

    private function mapServicePortalItemToLayerXML(item:PortalItem, type:String):XML
    {
        const title:String = item.title;
        const iconURL:String = item.thumbnailURL;
        const url:String = item.url;
        return createLayerXML(title, type, iconURL, url, 1, false);
    }

    private function mapServiceRequest_faultHandler(fault:Fault, token:Object = null):void
    {
        if (fault.faultString != "Sign in aborted")
        {
            AppEvent.dispatch(AppEvent.APP_ERROR,
                              LocalizationUtil.getDefaultString("couldNotFetchBasemapData",
                                                                fault.faultString));
        }

        updateTotalArcGISBasemaps();
    }

    private function portal_queryGroupsFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotQueryPortal"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function portal_queryItemsFaultHandler(fault:Fault, token:Object = null):void
    {
        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotQueryPortalItems"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function portal_faultHandler(event:FaultEvent):void
    {
        var portal:Portal = event.target as Portal;
        portal.removeEventListener(PortalEvent.LOAD, portal_loadHandler);
        portal.removeEventListener(FaultEvent.FAULT, portal_faultHandler);

        AppEvent.showError(LocalizationUtil.getDefaultString("couldNotConnectToPortal"), PORTAL_BASEMAP_APPENDER);
        dispatchComplete();
    }

    private function toCultureCode(locale:String):String
    {
        return locale ? locale.replace('_', '-') : locale;
    }
}

}

Support appending basemaps from Portal map-service items.


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.osmf.plugins.traits {
    import org.mangui.hls.HLS;
    import org.mangui.hls.model.AudioTrack;
    import org.mangui.hls.event.HLSEvent;
    import org.osmf.events.AlternativeAudioEvent;
    import org.osmf.media.MediaElement;
    import org.osmf.net.StreamingItem;
    import org.osmf.traits.AlternativeAudioTrait;
    import org.osmf.utils.OSMFStrings;

    CONFIG::LOGGING {
        import org.mangui.hls.utils.Log;
    }
    public class HLSAlternativeAudioTrait extends AlternativeAudioTrait {
        private var _hls : HLS;
        private var _media : MediaElement;
        private var _audioTrackList : Vector.<AudioTrack>;
        private var _numAlternativeAudioStreams : int;
        private var _transitionInProgress : Boolean = false;
        private var _activeTransitionIndex : int = DEFAULT_TRANSITION_INDEX;
        private var _lastTransitionIndex : int = INVALID_TRANSITION_INDEX;

        public function HLSAlternativeAudioTrait(hls : HLS, media : MediaElement) {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait()");
            }
            super(0);
            _hls = hls;
            _audioTrackList = _hls.audioTracks;
            _numAlternativeAudioStreams = _audioTrackList.length - 1;
            _media = media;
            _hls.addEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChangedHandler);
            _hls.addEventListener(HLSEvent.AUDIO_TRACKS_LIST_CHANGE, _audioTrackListChangedHandler);
        }

        override public function dispose() : void {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait:dispose");
            }
            _hls.removeEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChangedHandler);
            _hls.removeEventListener(HLSEvent.AUDIO_TRACKS_LIST_CHANGE, _audioTrackListChangedHandler);
            super.dispose();
        }

        override public function get numAlternativeAudioStreams() : int {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait:numAlternativeAudioStreams:" + _numAlternativeAudioStreams);
            }
            return _numAlternativeAudioStreams;
        }

        override public function getItemForIndex(index : int) : StreamingItem {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:getItemForIndex(" + index + ")");
            }
            if (index <= INVALID_TRANSITION_INDEX || index >= numAlternativeAudioStreams) {
                throw new RangeError(OSMFStrings.getString(OSMFStrings.ALTERNATIVEAUDIO_INVALID_INDEX));
            }

            if (index == DEFAULT_TRANSITION_INDEX) {
                return null;
            }
            var name : String = _audioTrackList[index + 1].title;
            var streamItem : StreamingItem = new StreamingItem("AUDIO", name);
            streamItem.info.label = name;
            return streamItem;
        }

        override protected function endSwitching(index : int) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:endSwitching(" + index + ")");
            }
            if (switching) {
                executeSwitching(_indexToSwitchTo);
            }
            super.endSwitching(index);
        }

        protected function executeSwitching(indexToSwitchTo : int) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:executeSwitching(" + indexToSwitchTo + ")");
            }
            if (_lastTransitionIndex != indexToSwitchTo) {
                _activeTransitionIndex = indexToSwitchTo;
                _transitionInProgress = true;
                _hls.audioTrack = indexToSwitchTo + 1;
            }
        }

        private function _audioTrackChangedHandler(event : HLSEvent) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:_audioTrackChangedHandler");
            }
            _transitionInProgress = false;
            setSwitching(false, _lastTransitionIndex);
        }

        private function _audioTrackListChangedHandler(event : HLSEvent) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:_audioTrackListChangedHandler");
            }
            _audioTrackList = _hls.audioTracks;
            if (_audioTrackList.length > 0) {
                // try to change default Audio Track Title for GrindPlayer ...
                if (_audioTrackList[0].title.indexOf("TS/") == -1) {
                    CONFIG::LOGGING {
                    Log.debug("default audio track title:" + _audioTrackList[0].title);
                    }
                    _media.resource.addMetadataValue("defaultAudioLabel", _audioTrackList[0].title);
                }
            }
            _numAlternativeAudioStreams = _audioTrackList.length - 1;
            if (_numAlternativeAudioStreams > 0) {
                dispatchEvent(new AlternativeAudioEvent(AlternativeAudioEvent.NUM_ALTERNATIVE_AUDIO_STREAMS_CHANGE, false, false, false));
            }
        }
    }
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.osmf.plugins.traits {
    import org.mangui.hls.HLS;
    import org.mangui.hls.model.AudioTrack;
    import org.mangui.hls.event.HLSEvent;
    import org.osmf.events.AlternativeAudioEvent;
    import org.osmf.media.MediaElement;
    import org.osmf.net.StreamingItem;
    import org.osmf.traits.AlternativeAudioTrait;
    import org.osmf.utils.OSMFStrings;

    CONFIG::LOGGING {
        import org.mangui.hls.utils.Log;
    }
    public class HLSAlternativeAudioTrait extends AlternativeAudioTrait {
        private var _hls : HLS;
        private var _media : MediaElement;
        private var _audioTrackList : Vector.<AudioTrack>;
        private var _numAlternativeAudioStreams : int;
        private var _activeTransitionIndex : int = DEFAULT_TRANSITION_INDEX;
        private var _lastTransitionIndex : int = INVALID_TRANSITION_INDEX;

        public function HLSAlternativeAudioTrait(hls : HLS, media : MediaElement) {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait()");
            }
            _hls = hls;
            _audioTrackList = _hls.audioTracks;
            _numAlternativeAudioStreams = _audioTrackList.length - 1;
            super(_numAlternativeAudioStreams);
            _media = media;
            _hls.addEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChangedHandler);
            _hls.addEventListener(HLSEvent.AUDIO_TRACKS_LIST_CHANGE, _audioTrackListChangedHandler);
        }

        override public function dispose() : void {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait:dispose");
            }
            _hls.removeEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChangedHandler);
            _hls.removeEventListener(HLSEvent.AUDIO_TRACKS_LIST_CHANGE, _audioTrackListChangedHandler);
            super.dispose();
        }

        override public function get numAlternativeAudioStreams() : int {
            CONFIG::LOGGING {
            Log.debug("HLSAlternativeAudioTrait:numAlternativeAudioStreams:" + _numAlternativeAudioStreams);
            }
            return _numAlternativeAudioStreams;
        }

        override public function getItemForIndex(index : int) : StreamingItem {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:getItemForIndex(" + index + ")");
            }
            if (index <= INVALID_TRANSITION_INDEX || index >= numAlternativeAudioStreams) {
                throw new RangeError(OSMFStrings.getString(OSMFStrings.ALTERNATIVEAUDIO_INVALID_INDEX));
            }

            if (index == DEFAULT_TRANSITION_INDEX) {
                return null;
            }
            var name : String = _audioTrackList[index + 1].title;
            var streamItem : StreamingItem = new StreamingItem("AUDIO", name);
            streamItem.info.label = name;
            return streamItem;
        }

        override protected function endSwitching(index : int) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:endSwitching(" + index + ")");
            }
            if (switching) {
                executeSwitching(_indexToSwitchTo);
            }
            super.endSwitching(index);
        }

        protected function executeSwitching(indexToSwitchTo : int) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:executeSwitching(" + indexToSwitchTo + ")");
            }
            if (_lastTransitionIndex != indexToSwitchTo) {
                _activeTransitionIndex = indexToSwitchTo;
                _hls.audioTrack = indexToSwitchTo + 1;
            }
        }

        private function _audioTrackChangedHandler(event : HLSEvent) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:_audioTrackChangedHandler");
            }
            setSwitching(false, _activeTransitionIndex);
            _lastTransitionIndex = _activeTransitionIndex;
        }

        private function _audioTrackListChangedHandler(event : HLSEvent) : void {
            CONFIG::LOGGING {
            Log.debug("HLSDynamicStreamTrait:_audioTrackListChangedHandler");
            }
            _audioTrackList = _hls.audioTracks;
            if (_audioTrackList.length > 0) {
                // try to change default Audio Track Title for GrindPlayer ...
                if (_audioTrackList[0].title.indexOf("TS/") == -1) {
                    CONFIG::LOGGING {
                    Log.debug("default audio track title:" + _audioTrackList[0].title);
                    }
                    _media.resource.addMetadataValue("defaultAudioLabel", _audioTrackList[0].title);
                }
            }
            _numAlternativeAudioStreams = _audioTrackList.length - 1;
            if (_numAlternativeAudioStreams > 0) {
                dispatchEvent(new AlternativeAudioEvent(AlternativeAudioEvent.NUM_ALTERNATIVE_AUDIO_STREAMS_CHANGE, false, false, false));
            }
        }
    }
}

flashlsOSMF: fix AlternativeAudioTrait reporting invalid stream index
related to #6


package krewfw.builtin_actor.system {

    import flash.utils.Dictionary;

    import krewfw.core.KrewActor;

    /**
     * Hierarchical Finite State Machine for krewFramework.
     *
     * [Note] 後から動的に登録 State を変えるような使い方は想定していない。
     *        addState はあるが removeState のインタフェースは用意していない。
     *
     *        また、現状 KrewStateMachine に addState を行った後の state に
     *        sub state を足すような書き方にも対応していない。
     *        KrewStateMachine のコンストラクタで一度に定義してしまうことを推奨する。
     */
    //------------------------------------------------------------
    public class KrewStateMachine extends KrewActor {

        /** If trace log is annoying you, set it false. */
        public static var VERBOSE:Boolean = true;

        // key  : state id
        // value: KrewState instance
        private var _states:Dictionary = new Dictionary();

        // States with constructor argument order. Used for default 'next' settings.
        private var _rootStateList:Vector.<KrewState> = new Vector.<KrewState>();

        private var _currentState:KrewState;

        private var _listenMap:Dictionary = new Dictionary();

        //------------------------------------------------------------
        /**
         * Usage:
         * <pre>
         *     var fsm:KrewStateMachine = new KrewStateMachine([
         *         {
         *             id: "state_1",  // First element will be an initial state.
         *             enter: onEnterFunc,
         *             next: "state_2"  // Default next state is next element of this Array.
         *         },
         *
         *         new YourCustomState(),  // Instead of Object, KrewState instance is OK.
         *
         *         {
         *             id: "state_2",
         *             children: [  // State can contain sub States.
         *                 { id: "state_2_a" },
         *                 { id: "state_2_b" },
         *                 {
         *                     id : "state_2_c",
         *                     listen: {event: "event_1", to: "state_4"},
         *                     guard : guardFunc
         *                 },
         *                 {
         *                     id: "state_2_d",
         *                     listen: [  // Array is also OK.
         *                         {event: "event_2", to: "state_1"},
         *                         {event: "event_3", to: "state_2"}
         *                     ]
         *                 }
         *             ]
         *         },
         *         {
         *             id: "state_3",
         *             listen: {event: "event_1", to: "state_4"}
         *         },
         *         ...
         *     ]);
         * </pre>
         *
         * @param stateDefList Array of KrewState instances or definition objects.
         * @param funcOwner If you speciify functions with name string, pass function-owner object.
         */
        public function KrewStateMachine(stateDefList:Array=null, funcOwner:Object=null) {
            displayable = false;

            _initStates(stateDefList, funcOwner);
        }

        private function _initStates(stateDefList:Array, funcOwner:Object=null):void {
            // guard
            if (stateDefList == null) { return; }

            if (!(stateDefList is Array)) {
                throw new Error("[KrewFSM] Constructor argument must be Array.");
            }
            if (stateDefList.length == 0) { return; }

            // do init
            addInitializer(function():void {
                for each (var stateDef:* in stateDefList) {
                    addState(stateDef, funcOwner);
                }

                _setDefaultNextStates(_rootStateList);
                _setInitialState(stateDefList);
            });
        }

        private function _setInitialState(stateDefList:Array):void {
            var firstDef:* = stateDefList[0];
            var initStateId:String;

            if (firstDef is KrewState) {
                initStateId = firstDef.stateId;
            }
            else if (firstDef is Object) {
                initStateId = firstDef.id;
            }
            else {
                throw new Error("[KrewFSM] Invalid first stateDef.");
            }

            changeState(initStateId);
        }

        /**
         * next が指定されていない state について、コンストラクタで渡した定義で
         * 上から下になぞるように next を設定していく。
         * この関数は再帰で、ツリーの末端の子 state を返す
         */
        private function _setDefaultNextStates(stateList:Vector.<KrewState>):KrewState {
            if (stateList.length == 0) { return null; }

            for (var i:int = 0;  i < stateList.length;  ++i) {
                var state:KrewState = stateList[i];

                if (state.nextStateId == null) {
                    if (state.hasChildren()) {
                        state.nextStateId = state.childStates[0].stateId;
                    }
                    else if (i + 1 <= stateList.length - 1) {
                        state.nextStateId = stateList[i + 1].stateId;
                    }
                }

                if (state.hasChildren()) {
                    var edgeState:KrewState = _setDefaultNextStates(state.childStates);

                    if (edgeState.nextStateId == null) {
                        if (i + 1 <= stateList.length - 1) {
                            edgeState.nextStateId = stateList[i + 1].stateId;
                        } else {
                            return edgeState;
                        }
                    }
                }
            }

            var edgeState:KrewState = krew.last(stateList);
            if (!edgeState.hasParent()) { return null; }

            return edgeState;
        }

        protected override function onDispose():void {
            for each (var state:KrewState in _states) {
                state.eachChild(function(childState:KrewState):void {
                    childState.dispose();
                });
            }

            _states        = null;
            _rootStateList = null;
            _currentState  = null;
            _listenMap     = null;
        }

        //------------------------------------------------------------
        // public
        //------------------------------------------------------------

        /**
         * @see KrewState.addState
         */
        public function addState(stateDef:*, funcOwner:Object=null):void {
            var state:KrewState = KrewState.makeState(stateDef, funcOwner);
            _registerStateTree(state);
            _rootStateList.push(state);
        }

        public function changeState(stateId:String):void {
            if (!_states[stateId]) {
                throw new Error("[KrewFSM] stateId not registered: " + stateId);
            }

            var oldStateId:String = "null";
            var oldState:KrewState = _currentState;
            var newState:KrewState = _states[stateId];

            // Good bye old state
            if (_currentState != null) {
                oldStateId = _currentState.stateId;
                oldState.exit();
                oldState.end(newState);
            }

            _log("[Info] [KrewFSM] SWITCHED: " + newState.stateId + "  <-  " + oldStateId);

            // Hello new state
            _currentState = newState;
            newState.begin(oldState);
            newState.enter();
        }

        /**
         * If given state is current state OR parent of current state, return true.
         * For example, when current state is "A-sub", and it is child state of "A",
         * both isState("A-sub") and isState("A") returns true.
         *
         * 現在の state が指定したものか、指定したものの子 state なら true を返す。
         * 例えば現在の state "A-sub" が "A" の子 state であるとき、isState("A-sub") でも
         * isState("A") でも true が返る。
         */
        public function isState(stateName:String):Boolean {
            if (_currentState.stateId == stateName) { return true; }

            var stateIter:KrewState = _currentState;
            while (stateIter.hasParent()) {
                stateIter = stateIter.parentState;
                if (stateIter.stateId == stateName) { return true; }
            }
            return false;
        }

        public function getState(stateId:String):KrewState {
            if (!_states[stateId]) {
                throw new Error("[KrewFSM] stateId not registered: " + stateId);
            }
            return _states[stateId];
        }

        public function get currentState():KrewState {
            return _currentState;
        }

        //------------------------------------------------------------
        // called by KrewState
        //------------------------------------------------------------

        public function listenToStateEvent(event:String):void {
            if (_listenMap[event]) { return; }  // already listening by other state

            listen(event, function(args:Object):void {
                _onEvent(args, event);
            })

            _listenMap[event] = true;
        }

        public function stopListeningToStateEvent(event:String):void {
            if (!_listenMap[event]) { return; }  // already stopped by other state

            stopListening(event);

            _listenMap[event] = false;
        }

        //------------------------------------------------------------
        // called by krewFramework
        //------------------------------------------------------------

        public override function onUpdate(passedTime:Number):void {
            if (_currentState == null) { return; }

            _currentState.eachParent(function(state:KrewState):void {
                if (state.onUpdateHandler == null) { return; }
                state.onUpdateHandler(state, passedTime);
            });
        }

        //------------------------------------------------------------
        // private
        //------------------------------------------------------------

        private function _onEvent(args:Object, event:String):void {
            _currentState.onEvent(args, event);
        }

        /**
         * State を、子を含めて全て Dictionary に保持
         * （State は Composite Pattern で sub state を子に持てる）
         */
        private function _registerStateTree(state:KrewState):void {
            state.eachChild(function(aState:KrewState):void {
                _registerState(aState);
            });
        }

        // State 1 個ぶんを Dictionary に保持
        private function _registerState(state:KrewState):void {
            if (_states[state.stateId]) {
                throw new Error("[KrewFSM] stateId already registered: " + state.stateId);
            }

            _states[state.stateId] = state;
            state.stateMachine = this;
        }

        //------------------------------------------------------------
        // debug method
        //------------------------------------------------------------

        private function _log(text:String):void {
            if (!VERBOSE) { return; }
            krew.fwlog(text);
        }

        public function dumpDictionary():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state dictionary dump");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _states) {
                krew.log(" - " + state.stateId);
            }
            krew.log(krew.str.repeat("^", 50));
        }

        public function dumpDictionaryVerbose():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state dictionary dump -v");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _states) {
                state.dump();
            }
        }

        public function dumpState(stateId:String):void {
            _states[stateId].dump();
        }

        public function dumpStateTree():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state tree dump");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _rootStateList) {
                state.dumpTree();
            }
            krew.log(krew.str.repeat("^", 50));
        }

    }
}

package krewfw.builtin_actor.system {

    import flash.utils.Dictionary;

    import krewfw.core.KrewActor;

    /**
     * Hierarchical Finite State Machine for krewFramework.
     *
     * [Note] 後から動的に登録 State を変えるような使い方は想定していない。
     *        addState はあるが removeState のインタフェースは用意していない。
     *
     *        また、現状 KrewStateMachine に addState を行った後の state に
     *        sub state を足すような書き方にも対応していない。
     *        KrewStateMachine のコンストラクタで一度に定義してしまうことを推奨する。
     */
    //------------------------------------------------------------
    public class KrewStateMachine extends KrewActor {

        /** If trace log is annoying you, set it false. */
        public static var VERBOSE:Boolean = true;

        // key  : state id
        // value: KrewState instance
        private var _states:Dictionary = new Dictionary();

        // States with constructor argument order. Used for default 'next' settings.
        private var _rootStateList:Vector.<KrewState> = new Vector.<KrewState>();

        private var _currentState:KrewState;

        private var _listenMap:Dictionary = new Dictionary();

        //------------------------------------------------------------
        /**
         * Usage:
         * <pre>
         *     var fsm:KrewStateMachine = new KrewStateMachine([
         *         {
         *             id: "state_1",  // First element will be an initial state.
         *             enter: onEnterFunc,
         *             next: "state_2"  // Default next state is next element of this Array.
         *         },
         *
         *         new YourCustomState(),  // Instead of Object, KrewState instance is OK.
         *
         *         {
         *             id: "state_2",
         *             children: [  // State can contain sub States.
         *                 { id: "state_2_a" },
         *                 { id: "state_2_b" },
         *                 {
         *                     id : "state_2_c",
         *                     listen: {event: "event_1", to: "state_4"},
         *                     guard : guardFunc
         *                 },
         *                 {
         *                     id: "state_2_d",
         *                     listen: [  // Array is also OK.
         *                         {event: "event_2", to: "state_1"},
         *                         {event: "event_3", to: "state_2"}
         *                     ]
         *                 }
         *             ]
         *         },
         *         {
         *             id: "state_3",
         *             listen: {event: "event_1", to: "state_4"}
         *         },
         *         ...
         *     ]);
         * </pre>
         *
         * @param stateDefList Array of KrewState instances or definition objects.
         * @param funcOwner If you speciify functions with name string, pass function-owner object.
         */
        public function KrewStateMachine(stateDefList:Array=null, funcOwner:Object=null) {
            displayable = false;

            _initStates(stateDefList, funcOwner);
        }

        private function _initStates(stateDefList:Array, funcOwner:Object=null):void {
            // guard
            if (stateDefList == null) { return; }

            if (!(stateDefList is Array)) {
                throw new Error("[KrewFSM] Constructor argument must be Array.");
            }
            if (stateDefList.length == 0) { return; }

            // do init
            addInitializer(function():void {
                for each (var stateDef:* in stateDefList) {
                    addState(stateDef, funcOwner);
                }

                _setDefaultNextStates(_rootStateList);
                _setInitialState(stateDefList);
            });
        }

        private function _setInitialState(stateDefList:Array):void {
            var firstDef:* = stateDefList[0];
            var initStateId:String;

            if (firstDef is KrewState) {
                initStateId = firstDef.stateId;
            }
            else if (firstDef is Object) {
                initStateId = firstDef.id;
            }
            else {
                throw new Error("[KrewFSM] Invalid first stateDef.");
            }

            changeState(initStateId);
        }

        /**
         * next が指定されていない state について、コンストラクタで渡した定義で
         * 上から下になぞるように next を設定していく。
         * この関数は再帰で、ツリーの末端の子 state を返す
         */
        private function _setDefaultNextStates(stateList:Vector.<KrewState>):KrewState {
            if (stateList.length == 0) { return null; }

            for (var i:int = 0;  i < stateList.length;  ++i) {
                var state:KrewState = stateList[i];

                if (state.nextStateId == null) {
                    if (state.hasChildren()) {
                        state.nextStateId = state.childStates[0].stateId;
                    }
                    else if (i + 1 <= stateList.length - 1) {
                        state.nextStateId = stateList[i + 1].stateId;
                    }
                }

                if (state.hasChildren()) {
                    var edgeState:KrewState = _setDefaultNextStates(state.childStates);

                    if (edgeState.nextStateId == null) {
                        if (i + 1 <= stateList.length - 1) {
                            edgeState.nextStateId = stateList[i + 1].stateId;
                        } else {
                            return edgeState;
                        }
                    }
                }
            }

            var edgeState:KrewState = krew.last(stateList);
            if (!edgeState.hasParent()) { return null; }

            return edgeState;
        }

        protected override function onDispose():void {
            for each (var state:KrewState in _states) {
                state.eachChild(function(childState:KrewState):void {
                    childState.dispose();
                });
            }

            _states        = null;
            _rootStateList = null;
            _currentState  = null;
            _listenMap     = null;
        }

        //------------------------------------------------------------
        // public
        //------------------------------------------------------------

        /**
         * @see KrewState.addState
         */
        public function addState(stateDef:*, funcOwner:Object=null):void {
            var state:KrewState = KrewState.makeState(stateDef, funcOwner);
            _registerStateTree(state);
            _rootStateList.push(state);
        }

        public function changeState(stateId:String):void {
            if (!_states[stateId]) {
                throw new Error("[KrewFSM] stateId not registered: " + stateId);
            }

            var oldStateId:String = "null";
            var oldState:KrewState = _currentState;
            var newState:KrewState = _states[stateId];

            // Good bye old state
            if (_currentState != null) {
                oldStateId = _currentState.stateId;
                oldState.exit();
                oldState.end(newState);
            }

            _log("[Info] [KrewFSM] SWITCHED: " + newState.stateId + "  <-  " + oldStateId);

            // Hello new state
            _currentState = newState;
            newState.begin(oldState);
            newState.enter();
        }

        /**
         * If given state is current state OR parent of current state, return true.
         * For example, when current state is "A-sub", and it is child state of "A",
         * both isState("A-sub") and isState("A") returns true.
         *
         * 現在の state が指定したものか、指定したものの子 state なら true を返す。
         * 例えば現在の state "A-sub" が "A" の子 state であるとき、isState("A-sub") でも
         * isState("A") でも true が返る。
         */
        public function isState(stateName:String):Boolean {
            if (_currentState.stateId == stateName) { return true; }

            var stateIter:KrewState = _currentState;
            while (stateIter.hasParent()) {
                stateIter = stateIter.parentState;
                if (stateIter.stateId == stateName) { return true; }
            }
            return false;
        }

        public function getState(stateId:String):KrewState {
            if (!_states[stateId]) {
                throw new Error("[KrewFSM] stateId not registered: " + stateId);
            }
            return _states[stateId];
        }

        public function get currentState():KrewState {
            return _currentState;
        }

        //------------------------------------------------------------
        // called by KrewState
        //------------------------------------------------------------

        public function listenToStateEvent(event:String):void {
            if (_listenMap[event]) { return; }  // already listening by other state

            listen(event, function(args:Object):void {
                _onEvent(args, event);
            })

            _listenMap[event] = true;
        }

        public function stopListeningToStateEvent(event:String):void {
            if (!_listenMap[event]) { return; }  // already stopped by other state

            stopListening(event);

            _listenMap[event] = false;
        }

        //------------------------------------------------------------
        // called by krewFramework
        //------------------------------------------------------------

        public override function onUpdate(passedTime:Number):void {
            if (_currentState == null) { return; }

            _currentState.eachParent(function(state:KrewState):void {
                if (state.onUpdateHandler == null) { return; }
                state.onUpdateHandler(state, passedTime);
            });
        }

        //------------------------------------------------------------
        // private
        //------------------------------------------------------------

        private function _onEvent(args:Object, event:String):void {
            _log("[Info] [KrewFSM] EVENT: " + event);
            _currentState.onEvent(args, event);
        }

        /**
         * State を、子を含めて全て Dictionary に保持
         * （State は Composite Pattern で sub state を子に持てる）
         */
        private function _registerStateTree(state:KrewState):void {
            state.eachChild(function(aState:KrewState):void {
                _registerState(aState);
            });
        }

        // State 1 個ぶんを Dictionary に保持
        private function _registerState(state:KrewState):void {
            if (_states[state.stateId]) {
                throw new Error("[KrewFSM] stateId already registered: " + state.stateId);
            }

            _states[state.stateId] = state;
            state.stateMachine = this;
        }

        //------------------------------------------------------------
        // debug method
        //------------------------------------------------------------

        private function _log(text:String):void {
            if (!VERBOSE) { return; }
            krew.fwlog(text);
        }

        public function dumpDictionary():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state dictionary dump");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _states) {
                krew.log(" - " + state.stateId);
            }
            krew.log(krew.str.repeat("^", 50));
        }

        public function dumpDictionaryVerbose():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state dictionary dump -v");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _states) {
                state.dump();
            }
        }

        public function dumpState(stateId:String):void {
            _states[stateId].dump();
        }

        public function dumpStateTree():void {
            krew.log(krew.str.repeat("-", 50));
            krew.log(" KrewStateMachine state tree dump");
            krew.log(krew.str.repeat("-", 50));

            for each(var state:KrewState in _rootStateList) {
                state.dumpTree();
            }
            krew.log(krew.str.repeat("^", 50));
        }

    }
}

Add log to KrewStateMachine

Add log to KrewStateMachine


////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package spark.skins.ios7.supportClasses
{
import flash.display.BlendMode;
import flash.display.GradientType;
import flash.display.Graphics;
import flash.display.GraphicsPathCommand;
import flash.display.Sprite;

import mx.core.DPIClassification;
import mx.core.FlexGlobals;
import mx.core.IVisualElement;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.utils.ColorUtil;

import spark.components.Application;
import spark.components.ArrowDirection;
import spark.components.Callout;
import spark.skins.ios7.CalloutSkin;
import spark.skins.mobile.supportClasses.MobileSkin;

use namespace mx_internal;

/**
 *  The arrow skin part for CalloutSkin.
 *
 *  @see spark.skin.mobile.CalloutSkin
 *
 *  @langversion 3.0
 *  @playerversion AIR 3
 *  @productversion Flex 4.6
 */
public class CalloutArrow extends UIComponent
{
    public function CalloutArrow()
    {
        super();

        useBackgroundGradient = true;

		var applicationDPI:Number = DPIClassification.DPI_160;
		if (FlexGlobals.topLevelApplication is Application)
		{
			applicationDPI = Application(FlexGlobals.topLevelApplication).applicationDPI;
		}

        // Copy DPI-specific values from CalloutSkin
        switch (applicationDPI)
        {
			case DPIClassification.DPI_640:
			{
				// Note provisional may need changes
				gap = 32;
				backgroundGradientHeight = 440;
				highlightWeight = 4;

				break;
			}
			case DPIClassification.DPI_480:
			{
				// Note provisional may need changes
				gap = 24;
				backgroundGradientHeight = 330;
				highlightWeight = 3;

				break;
			}
            case DPIClassification.DPI_320:
            {
                gap = 16;
                backgroundGradientHeight = 220;
                highlightWeight = 2;

                break;
            }
			case DPIClassification.DPI_240:
			{
				gap = 12;
				backgroundGradientHeight = 165;
				highlightWeight = 1;

				break;
			}
			case DPIClassification.DPI_120:
			{
				// Note provisional may need changes
				gap = 6;
				backgroundGradientHeight = 83;
				highlightWeight = 1;

				break;
			}
            default:
            {
                // default DPI_160
                gap = 8;
                backgroundGradientHeight = 110;
                highlightWeight = 1;

                break;
            }
        }
    }

    /**
     *  A gap on the frame-adjacent side of the arrow graphic to avoid
     *  drawing past the CalloutSkin backgroundCornerRadius.
     *
     *
The default implementation matches the gap value with the
     *  backgroundCornerRadius value in CalloutSkin.

     *
     *  @see spark.skins.mobile.CalloutSkin#backgroundCornerRadius
     *
     *  @langversion 3.0
     *  @playerversion AIR 3
     *  @productversion Flex 4.6
     */
    protected var gap:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#backgroundGradientHeight
     */
    protected var backgroundGradientHeight:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#highlightWeight
     */
    private var highlightWeight:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#useBackgroundGradient
     */
    protected var useBackgroundGradient:Boolean;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#borderColor
     */
    protected var borderColor:Number = -1; // if not set

    /**
     *  @copy spark.skins.mobile.CalloutSkin#borderThickness
     */
    protected var borderThickness:Number = -1 ;      // marker that borderThickness was not set  directly

    /**
     *  @private
     *  A sibling of the arrow used to erase the drop shadow in CalloutSkin
     */
    private var eraseFill:Sprite;

    /* helper private accessors */

    /* returns borderThickness from style if member is -1, or borderThickness.  Returns 0 if NaN */
    private function get actualBorderThickness():Number
    {
        return calloutSkin.actualBorderThickness;
    }

    private function get actualBorderColor():uint
    {
        return calloutSkin.actualBorderColor;
    }

    protected function get calloutSkin():CalloutSkin
    {
        return parent as CalloutSkin ;
    }

    protected function get calloutHostComponent():Callout {
        return  calloutSkin.hostComponent;
    }

    /**
     * @private
     */

    override protected function createChildren():void
    {
        super.createChildren();

        // eraseFill has the same position and arrow shape in order to erase
        // the drop shadow under the arrow when backgroundAlpha < 1
        eraseFill = new Sprite();
        eraseFill.blendMode = BlendMode.ERASE;

        // layer eraseFill below the arrow
        parent.addChildAt(eraseFill, parent.getChildIndex(this));
    }

    /**
     * @private
     */
    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.updateDisplayList(unscaledWidth, unscaledHeight);

        graphics.clear();
        eraseFill.graphics.clear();
        var hostComponent: Callout = calloutHostComponent;
        var arrowDirection:String = hostComponent.arrowDirection;

        if (arrowDirection == ArrowDirection.NONE)
            return;

        // when drawing the arrow, compensate for cornerRadius via padding
        var arrowGraphics:Graphics = this.graphics;
        var eraseGraphics:Graphics = eraseFill.graphics;
        var arrowWidth:Number = unscaledWidth;
        var arrowHeight:Number = unscaledHeight;
        var arrowX:Number = 0;
        var arrowY:Number = 0;
        var arrowTipX:Number = 0;
        var arrowTipY:Number = 0;
        var arrowEndX:Number = 0;
        var arrowEndY:Number = 0;

        var borderWeight:Number = actualBorderThickness;
        var showBorder:Boolean = borderWeight > 0;

        var borderHalf:Number = borderWeight / 2;
        var isHorizontal:Boolean = false;

        if ((arrowDirection == ArrowDirection.LEFT) ||
            (arrowDirection == ArrowDirection.RIGHT))
        {
            isHorizontal = true;

            arrowX = -borderHalf;
            arrowY = gap;
            arrowHeight = arrowHeight - (gap * 2);

            arrowTipX = arrowWidth - borderHalf;
            arrowTipY = arrowY + (arrowHeight / 2);

            arrowEndX = arrowX;
            arrowEndY = arrowY + arrowHeight;

            // flip coordinates to point left
            if (arrowDirection == ArrowDirection.LEFT)
            {
                arrowX = arrowWidth - arrowX;
                arrowTipX = arrowWidth - arrowTipX;
                arrowEndX = arrowWidth - arrowEndX;
            }
        }
        else
        {
            arrowX = gap;
            arrowY = -borderHalf;
            arrowWidth = arrowWidth - (gap * 2);

            arrowTipX = arrowX + (arrowWidth / 2);
            arrowTipY = arrowHeight - borderHalf;

            arrowEndX = arrowX + arrowWidth;
            arrowEndY = arrowY;

            // flip coordinates to point up
            if (hostComponent.arrowDirection == ArrowDirection.UP)
            {
                arrowY = arrowHeight - arrowY;
                arrowTipY = arrowHeight - arrowTipY;
                arrowEndY = arrowHeight - arrowEndY;
            }
        }

        var commands:Vector.<int> = new Vector.<int>(3, true);
        commands[0] = GraphicsPathCommand.MOVE_TO;
        commands[1] = GraphicsPathCommand.LINE_TO;
        commands[2] = GraphicsPathCommand.LINE_TO;

        var coords:Vector.<Number> = new Vector.<Number>(6, true);
        coords[0] = arrowX;
        coords[1] = arrowY;
        coords[2] = arrowTipX
        coords[3] = arrowTipY;
        coords[4] = arrowEndX
        coords[5] = arrowEndY;

        var backgroundColor:Number = getStyle("backgroundColor");
        var backgroundAlpha:Number = getStyle("backgroundAlpha");

        if (useBackgroundGradient)
        {
            var backgroundColorTop:Number = ColorUtil.adjustBrightness2(backgroundColor,
                CalloutSkin.BACKGROUND_GRADIENT_BRIGHTNESS_TOP);
            var backgroundColorBottom:Number = ColorUtil.adjustBrightness2(backgroundColor,
                CalloutSkin.BACKGROUND_GRADIENT_BRIGHTNESS_BOTTOM);

            // translate the gradient based on the arrow position
            MobileSkin.colorMatrix.createGradientBox(unscaledWidth,
                backgroundGradientHeight, Math.PI / 2, 0, -getLayoutBoundsY());

            arrowGraphics.beginGradientFill(GradientType.LINEAR,
                [backgroundColorTop, backgroundColorBottom],
                [backgroundAlpha, backgroundAlpha],
                [0, 255],
                MobileSkin.colorMatrix);
        }
        else
        {
            arrowGraphics.beginFill(backgroundColor, backgroundAlpha);
        }

        // cover the adjacent border from the callout frame
        if (showBorder)
        {
            var coverX:Number = 0;
            var coverY:Number = 0;
            var coverWidth:Number = 0;
            var coverHeight:Number = 0;

            switch (arrowDirection)
            {
                case ArrowDirection.UP:
                {
                    coverX = arrowX;
                    coverY = arrowY;
                    coverWidth = arrowWidth;
                    coverHeight = borderWeight;
                    break;
                }
                case ArrowDirection.DOWN:
                {
                    coverX = arrowX;
                    coverY = -borderWeight;
                    coverWidth = arrowWidth;
                    coverHeight = borderWeight;
                    break;
                }
                case ArrowDirection.LEFT:
                {
                    coverX = arrowX;
                    coverY = arrowY;
                    coverWidth = borderWeight;
                    coverHeight = arrowHeight;
                    break;
                }
                case ArrowDirection.RIGHT:
                {
                    coverX = -borderWeight;
                    coverY = arrowY;
                    coverWidth = borderWeight;
                    coverHeight = arrowHeight;
                    break;
                }
            }

            arrowGraphics.drawRect(coverX, coverY, coverWidth, coverHeight);
        }

        // erase the drop shadow from the CalloutSkin
        if (backgroundAlpha < 1)
        {
            // move eraseFill to the same position as the arrow
            eraseFill.x = getLayoutBoundsX()
            eraseFill.y = getLayoutBoundsY();

            // draw the arrow shape
            eraseGraphics.beginFill(0, 1);
            eraseGraphics.drawPath(commands, coords);
            eraseGraphics.endFill();
        }

        // draw arrow path
        if (showBorder)
            arrowGraphics.lineStyle(borderWeight, actualBorderColor, 1, true);

        arrowGraphics.drawPath(commands, coords);
        arrowGraphics.endFill();

        // adjust the highlight position to the origin of the callout
        var isArrowUp:Boolean = (arrowDirection == ArrowDirection.UP);
        var offsetY:Number = (isArrowUp) ? unscaledHeight : -getLayoutBoundsY();

        // highlight starts after the backgroundCornerRadius
        var highlightX:Number = gap - getLayoutBoundsX();

        // highlight Y position is based on the stroke weight
        var highlightOffset:Number = (highlightWeight * 1.5);
        var highlightY:Number = highlightOffset + offsetY;

        // highlight width spans the callout width minus the corner radius
        var highlightWidth:Number = IVisualElement(calloutSkin).getLayoutBoundsWidth() - (gap * 2);

        if (isHorizontal)
        {
            highlightWidth -= arrowWidth;

            if (arrowDirection == ArrowDirection.LEFT)
                highlightX += arrowWidth;
        }

        // highlight on the top edge is drawn in the arrow only in the UP direction
        if (useBackgroundGradient)
        {
            if (isArrowUp)
            {
                // highlight follows the top edge, including the arrow
                var rightWidth:Number = highlightWidth - arrowWidth;

                // highlight style
                arrowGraphics.lineStyle(highlightWeight, 0xFFFFFF, 0.2 * backgroundAlpha);

                // in the arrow coordinate space, the highlightX must be less than 0
                if (highlightX < 0)
                {
                    arrowGraphics.moveTo(highlightX, highlightY);
                    arrowGraphics.lineTo(arrowX, highlightY);

                    // compute the remaining highlight
                    rightWidth -= (arrowX - highlightX);
                }

                // arrow highlight (adjust Y downward)
                coords[1] = arrowY + highlightOffset;
                coords[3] = arrowTipY + highlightOffset;
                coords[5] = arrowEndY + highlightOffset;
                arrowGraphics.drawPath(commands, coords);

                // right side
                if (rightWidth > 0)
                {
                    arrowGraphics.moveTo(arrowEndX, highlightY);
                    arrowGraphics.lineTo(arrowEndX + rightWidth, highlightY);
                }
            }
            else
            {
                // straight line across the top
                arrowGraphics.lineStyle(highlightWeight, 0xFFFFFF, 0.2 * backgroundAlpha);
                arrowGraphics.moveTo(highlightX, highlightY);
                arrowGraphics.lineTo(highlightX + highlightWidth, highlightY);
            }
        }
    }
}
}

////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package spark.skins.ios7.supportClasses
{
import flash.display.BlendMode;
import flash.display.GradientType;
import flash.display.Graphics;
import flash.display.GraphicsPathCommand;
import flash.display.Sprite;

import mx.core.DPIClassification;
import mx.core.FlexGlobals;
import mx.core.IVisualElement;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.utils.ColorUtil;

import spark.components.Application;
import spark.components.ArrowDirection;
import spark.components.Callout;
import spark.skins.ios7.CalloutSkin;
import spark.skins.mobile.supportClasses.MobileSkin;

use namespace mx_internal;

/**
 *  The arrow skin part for CalloutSkin.
 *
 *  @see spark.skin.mobile.CalloutSkin
 *
 *  @langversion 3.0
 *  @playerversion AIR 3
 *  @productversion Flex 4.6
 */
public class CalloutArrow extends UIComponent
{
    public function CalloutArrow()
    {
        super();

        useBackgroundGradient = false;

		var applicationDPI:Number = DPIClassification.DPI_160;
		if (FlexGlobals.topLevelApplication is Application)
		{
			applicationDPI = Application(FlexGlobals.topLevelApplication).applicationDPI;
		}

        // Copy DPI-specific values from CalloutSkin
        switch (applicationDPI)
        {
			case DPIClassification.DPI_640:
			{
				// Note provisional may need changes
				gap = 32;
				backgroundGradientHeight = 440;
				highlightWeight = 4;
				arrowTipCurveAnchorDistance = 12;

				break;
			}
			case DPIClassification.DPI_480:
			{
				// Note provisional may need changes
				gap = 24;
				backgroundGradientHeight = 330;
				highlightWeight = 3;
				arrowTipCurveAnchorDistance = 8;

				break;
			}
            case DPIClassification.DPI_320:
            {
                gap = 16;
                backgroundGradientHeight = 220;
                highlightWeight = 2;
				arrowTipCurveAnchorDistance = 6;

                break;
            }
			case DPIClassification.DPI_240:
			{
				gap = 12;
				backgroundGradientHeight = 165;
				highlightWeight = 1;
				arrowTipCurveAnchorDistance = 4;

				break;
			}
			case DPIClassification.DPI_120:
			{
				// Note provisional may need changes
				gap = 6;
				backgroundGradientHeight = 83;
				highlightWeight = 1;
				arrowTipCurveAnchorDistance = 2;

				break;
			}
            default:
            {
                // default DPI_160
                gap = 8;
                backgroundGradientHeight = 110;
                highlightWeight = 1;
				arrowTipCurveAnchorDistance = 3;

                break;
            }
        }
    }

    /**
     *  A gap on the frame-adjacent side of the arrow graphic to avoid
     *  drawing past the CalloutSkin backgroundCornerRadius.
     *
     *
The default implementation matches the gap value with the
     *  backgroundCornerRadius value in CalloutSkin.

     *
     *  @see spark.skins.mobile.CalloutSkin#backgroundCornerRadius
     *
     *  @langversion 3.0
     *  @playerversion AIR 3
     *  @productversion Flex 4.6
     */
    protected var gap:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#backgroundGradientHeight
     */
    protected var backgroundGradientHeight:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#highlightWeight
     */
    private var highlightWeight:Number;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#useBackgroundGradient
     */
    protected var useBackgroundGradient:Boolean;

    /**
     *  @copy spark.skins.mobile.CalloutSkin#borderColor
     */
    protected var borderColor:Number = -1; // if not set

    /**
     *  @copy spark.skins.mobile.CalloutSkin#borderThickness
     */
    protected var borderThickness:Number = -1 ;      // marker that borderThickness was not set  directly

    /**
     *  @private
     *  A sibling of the arrow used to erase the drop shadow in CalloutSkin
     */
    private var eraseFill:Sprite;

	/**
	 * The distance of the arrow tip's curve's anchor point from arrow tip
	 */
	private var arrowTipCurveAnchorDistance:Number;

    /* helper private accessors */

    /* returns borderThickness from style if member is -1, or borderThickness.  Returns 0 if NaN */
    private function get actualBorderThickness():Number
    {
        return calloutSkin.actualBorderThickness;
    }

    private function get actualBorderColor():uint
    {
        return calloutSkin.actualBorderColor;
    }

    protected function get calloutSkin():CalloutSkin
    {
        return parent as CalloutSkin ;
    }

    protected function get calloutHostComponent():Callout {
        return  calloutSkin.hostComponent;
    }

    /**
     * @private
     */

    override protected function createChildren():void
    {
        super.createChildren();

        // eraseFill has the same position and arrow shape in order to erase
        // the drop shadow under the arrow when backgroundAlpha < 1
        eraseFill = new Sprite();
        eraseFill.blendMode = BlendMode.ERASE;

        // layer eraseFill below the arrow
        parent.addChildAt(eraseFill, parent.getChildIndex(this));
    }

    /**
     * @private
     */
    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
    {
        super.updateDisplayList(unscaledWidth, unscaledHeight);

        graphics.clear();
        eraseFill.graphics.clear();
        var hostComponent: Callout = calloutHostComponent;
        var arrowDirection:String = hostComponent.arrowDirection;

        if (arrowDirection == ArrowDirection.NONE)
            return;

        // when drawing the arrow, compensate for cornerRadius via padding
        var arrowGraphics:Graphics = this.graphics;
        var eraseGraphics:Graphics = eraseFill.graphics;
        var arrowWidth:Number = unscaledWidth;
        var arrowHeight:Number = unscaledHeight;
        var arrowX:Number = 0;
        var arrowY:Number = 0;
        var arrowTipX:Number = 0;
        var arrowTipY:Number = 0;
        var arrowEndX:Number = 0;
        var arrowEndY:Number = 0;

        var borderWeight:Number = actualBorderThickness;
        var showBorder:Boolean = borderWeight > 0;

        var borderHalf:Number = borderWeight / 2;
        var isHorizontal:Boolean = false;
		var commands:Vector.<int> = new Vector.<int>(4, true);
		var coords:Vector.<Number> = new Vector.<Number>(10, true);
		commands[0] = GraphicsPathCommand.MOVE_TO;
		commands[1] = GraphicsPathCommand.LINE_TO;
		commands[2] = GraphicsPathCommand.CURVE_TO;
		commands[3] = GraphicsPathCommand.LINE_TO;

        if ((arrowDirection == ArrowDirection.LEFT) ||
            (arrowDirection == ArrowDirection.RIGHT))
        {
            isHorizontal = true;

            arrowX = -borderHalf;
            arrowY = gap;
            arrowHeight = arrowHeight - (gap * 2);

            arrowTipX = arrowWidth - borderHalf;
            arrowTipY = arrowY + (arrowHeight / 2);

            arrowEndX = arrowX;
            arrowEndY = arrowY + arrowHeight;

            // flip coordinates to point left
            if (arrowDirection == ArrowDirection.LEFT)
            {
                arrowX = arrowWidth - arrowX;
                arrowTipX = arrowWidth - arrowTipX;
                arrowEndX = arrowWidth - arrowEndX;
            }

			coords[0] = arrowX;
			coords[1] = arrowY;
			coords[2] = arrowTipX + arrowTipCurveAnchorDistance;
			coords[3] = arrowTipY - arrowTipCurveAnchorDistance;
            if (arrowDirection == ArrowDirection.LEFT)
            {
				coords[4] = arrowTipX - arrowTipCurveAnchorDistance * 2;
            }
			else
			{
				coords[4] = arrowTipX + arrowTipCurveAnchorDistance * 2;
			}
			coords[5] = arrowTipY;
			coords[6] = arrowTipX + arrowTipCurveAnchorDistance;
			coords[7] = arrowTipY + arrowTipCurveAnchorDistance;
			coords[8] = arrowEndX
			coords[9] = arrowEndY;
        }
        else
        {
            arrowX = gap;
            arrowY = -borderHalf;
            arrowWidth = arrowWidth - (gap * 2);

            arrowTipX = arrowX + (arrowWidth / 2);
            arrowTipY = arrowHeight - borderHalf;

            arrowEndX = arrowX + arrowWidth;
            arrowEndY = arrowY;

            // flip coordinates to point up
            if (hostComponent.arrowDirection == ArrowDirection.UP)
            {
                arrowY = arrowHeight - arrowY;
                arrowTipY = arrowHeight - arrowTipY;
                arrowEndY = arrowHeight - arrowEndY;
            }

			coords[0] = arrowX;
			coords[1] = arrowY;
			coords[2] = arrowTipX - arrowTipCurveAnchorDistance;
			coords[3] = arrowTipY - arrowTipCurveAnchorDistance;
			coords[4] = arrowTipX;
            if (hostComponent.arrowDirection == ArrowDirection.UP)
            {
				coords[5] = arrowTipY - arrowTipCurveAnchorDistance * 2;
			}
			else
			{
				coords[5] = arrowTipY + arrowTipCurveAnchorDistance * 2;
			}
			coords[6] = arrowTipX + arrowTipCurveAnchorDistance;
			coords[7] = arrowTipY - arrowTipCurveAnchorDistance;
			coords[8] = arrowEndX
			coords[9] = arrowEndY;
        }

        var backgroundColor:Number = getStyle("backgroundColor");
        var backgroundAlpha:Number = getStyle("backgroundAlpha");

        if (useBackgroundGradient)
        {
            var backgroundColorTop:Number = ColorUtil.adjustBrightness2(backgroundColor,
                CalloutSkin.BACKGROUND_GRADIENT_BRIGHTNESS_TOP);
            var backgroundColorBottom:Number = ColorUtil.adjustBrightness2(backgroundColor,
                CalloutSkin.BACKGROUND_GRADIENT_BRIGHTNESS_BOTTOM);

            // translate the gradient based on the arrow position
            MobileSkin.colorMatrix.createGradientBox(unscaledWidth,
                backgroundGradientHeight, Math.PI / 2, 0, -getLayoutBoundsY());

            arrowGraphics.beginGradientFill(GradientType.LINEAR,
                [backgroundColorTop, backgroundColorBottom],
                [backgroundAlpha, backgroundAlpha],
                [0, 255],
                MobileSkin.colorMatrix);
        }
        else
        {
            arrowGraphics.beginFill(backgroundColor, backgroundAlpha);
        }

        // cover the adjacent border from the callout frame
        if (showBorder)
        {
            var coverX:Number = 0;
            var coverY:Number = 0;
            var coverWidth:Number = 0;
            var coverHeight:Number = 0;

            switch (arrowDirection)
            {
                case ArrowDirection.UP:
                {
                    coverX = arrowX;
                    coverY = arrowY;
                    coverWidth = arrowWidth;
                    coverHeight = borderWeight;
                    break;
                }
                case ArrowDirection.DOWN:
                {
                    coverX = arrowX;
                    coverY = -borderWeight;
                    coverWidth = arrowWidth;
                    coverHeight = borderWeight;
                    break;
                }
                case ArrowDirection.LEFT:
                {
                    coverX = arrowX;
                    coverY = arrowY;
                    coverWidth = borderWeight;
                    coverHeight = arrowHeight;
                    break;
                }
                case ArrowDirection.RIGHT:
                {
                    coverX = -borderWeight;
                    coverY = arrowY;
                    coverWidth = borderWeight;
                    coverHeight = arrowHeight;
                    break;
                }
            }

            arrowGraphics.drawRect(coverX, coverY, coverWidth, coverHeight);
        }

        // erase the drop shadow from the CalloutSkin
        if (backgroundAlpha < 1)
        {
            // move eraseFill to the same position as the arrow
            eraseFill.x = getLayoutBoundsX()
            eraseFill.y = getLayoutBoundsY();

            // draw the arrow shape
            eraseGraphics.beginFill(0, 1);
            eraseGraphics.drawPath(commands, coords);
            eraseGraphics.endFill();
        }

        // draw arrow path
        if (showBorder)
            arrowGraphics.lineStyle(borderWeight, actualBorderColor, 1, true);

        arrowGraphics.drawPath(commands, coords);
        arrowGraphics.endFill();

        // adjust the highlight position to the origin of the callout
        var isArrowUp:Boolean = (arrowDirection == ArrowDirection.UP);
        var offsetY:Number = (isArrowUp) ? unscaledHeight : -getLayoutBoundsY();

        // highlight starts after the backgroundCornerRadius
        var highlightX:Number = gap - getLayoutBoundsX();

        // highlight Y position is based on the stroke weight
        var highlightOffset:Number = (highlightWeight * 1.5);
        var highlightY:Number = highlightOffset + offsetY;

        // highlight width spans the callout width minus the corner radius
        var highlightWidth:Number = IVisualElement(calloutSkin).getLayoutBoundsWidth() - (gap * 2);

        if (isHorizontal)
        {
            highlightWidth -= arrowWidth;

            if (arrowDirection == ArrowDirection.LEFT)
                highlightX += arrowWidth;
        }

        // highlight on the top edge is drawn in the arrow only in the UP direction
        if (useBackgroundGradient)
        {
            if (isArrowUp)
            {
                // highlight follows the top edge, including the arrow
                var rightWidth:Number = highlightWidth - arrowWidth;

                // highlight style
                arrowGraphics.lineStyle(highlightWeight, 0xFFFFFF, 0.2 * backgroundAlpha);

                // in the arrow coordinate space, the highlightX must be less than 0
                if (highlightX < 0)
                {
                    arrowGraphics.moveTo(highlightX, highlightY);
                    arrowGraphics.lineTo(arrowX, highlightY);

                    // compute the remaining highlight
                    rightWidth -= (arrowX - highlightX);
                }

                // arrow highlight (adjust Y downward)
                coords[1] = arrowY + highlightOffset;
                coords[3] = arrowTipY + highlightOffset;
                coords[5] = arrowEndY + highlightOffset;
                arrowGraphics.drawPath(commands, coords);

                // right side
                if (rightWidth > 0)
                {
                    arrowGraphics.moveTo(arrowEndX, highlightY);
                    arrowGraphics.lineTo(arrowEndX + rightWidth, highlightY);
                }
            }
            else
            {
                // straight line across the top
                arrowGraphics.lineStyle(highlightWeight, 0xFFFFFF, 0.2 * backgroundAlpha);
                arrowGraphics.moveTo(highlightX, highlightY);
                arrowGraphics.lineTo(highlightX + highlightWidth, highlightY);
            }
        }
    }
}
}

Tweak CalloutSkin. The arrow tip has a curve.