PharmaSales Application

Adobe AIR offers a data synchronization solution based on Data Management Services for those who own licenses of LiveCycle Data Service ES 2.6. This solution is described at the InsideRIA blog at http://tinyurl.com/6fa254.

But application developers who use an open source BlazeDS don’t have any generic way of setting such data synchronization process. This section offers a smart component called OfflineDataCollection that’s based on the DataCollection object described in Chapter 6. This component will take care of the data synchronization for you.

OfflineDataCollection is part of the Clear Toolkit’s component library clear.swc. You’ll see how to use it while reviewing a sample PharmaSales application that supports the sales force of a fictitious pharmaceutical company called Acme Pharm.

This application will have two types of users:

  • A salesperson visiting doctors’ offices trying to persuade doctors to use Acme Pharm’s latest drug, Xyzin

  • The Acme Pharm dispatcher who schedules daily routes for each salesperson

The corporate database schema supporting PharmaSales will look like Figure 9-11 (for simplicity, there are no relationships between tables).

The PharmaSales database model

Figure 9-11. The PharmaSales database model

Every morning a salesman starts the PharmaSales application, which connects to the corporate database (MySQL) and automatically loads his visit schedule for the day from the table visit_schedule. At this point, the data is being loaded into a local database (SQLite) that exists on the salesman’s laptop. The database will be automatically created on the first run of the application. The salesman’s laptop has to be connected to the Internet.

While visiting a particular doctor’s office, the salesman uses the PharmaSales application to take notes about the visit. In this case, the salesman’s laptop is disconnected from the Internet and all the records about visitations are saved in the local database only. As soon as Sal, the salesman, starts this application in connected mode, the local data with the latest visit information should be automatically synchronized with the corporate database.

To help Sal in finding doctors’ offices on the road, the application can be integrated with Google Maps.

Installing PharmaSales

For testing the PharmaSales application, you’ll need the following software installed:

  • Java development kit version 1.5 or higher

  • Eclipse JEE 3.3 or higher with the Flash Builder 3 plug-in

  • Apache Tomcat Servlet container

  • MySQL Server 5 DBMS

In Eclipse, import the PharmaSales application; it comes as two projects: air.offline.demo, which can be used in connected or disconnected mode by a salesperson, and air.offline.demo.web, which is used by the Acme Pharm dispatcher in connected mode only.

Note

If, after importing the project, you see an Unbound JDK error, go to the properties of the air.offline.demo.web project, select the option Java Build Path Libraries, remove the unbound JDK, select Add Library, and point at the directory where your JDK is installed—for example, C:Program FilesJavajdk1.6.0_12.

To simplify the installation, create a C:workspace soft link pointing at your Eclipse workspace directory as described in the section Preparing for Teamwork in Chapter 4. For example, if your workspace is located at D:myworkspace, the junction utility command will look like this:

junction c:workspace d:myworkspace

The PharmaSales Application for Dispatchers

The air.offline.demo.web project has a folder db that contains the file database.sql, which is a DDL script for creation of sample pharma database in MySQL Server. Download the MySQL GUI tools and create the database and the user dba with the password sql. Run these scripts and grant all the privileges to the dba user.

The easiest way to create this sample database is to open a command window and run the mysql utility, entering the right user ID and password for the user root. The following line is written for the user root with the password root, assuming that the file database.sql is located in the same directory as MySQL:

mysql -u root -p root < database.sql

The air.offline.demo.web project also has the file pharma.properties in the .settings directory with the database connectivity parameters. If you created the pharma database under a different user ID than dba, modify the user and the password there accordingly.

If you didn’t run the Café Townsend (the CDB version) example from Chapter 1, create a new server in the Eclipse JEE IDE by selecting File New Server and point it to your Tomcat installation. Add the project to the Tomcat server in Eclipse IDE and start the server.

Note

If you are not willing to install and run this application on your computer, you can instead watch a screencast that shows the process of configuring and running the PharmaSales application, which is available at http://www.myflex.org/demos/PharmaAir/PharmaAir.html.

The Acme Pharm’s dispatcher is the only user of the application VisitSchedules.mxml (the Flash Builder’s project air.offline.demo.web). Its main window allows scheduling new visits and viewing existing visits for each salesperson (Figure 9-12).

Viewing visit schedules

Figure 9-12. Viewing visit schedules

Click the Add button to open another view and schedule a new visit for any salesperson (Figure 9-13).

Scheduling a new visit

Figure 9-13. Scheduling a new visit

Scheduled visits are saved in the central MySQL Server database in the table visit_schedule, and each time the salesperson logs on to the system from her laptop, her visits are automatically downloaded to the local SQLite DBMS.

We won’t review all the code of this application; it was generated by Clear Data Builder similarly to Café Townsend, as described in Chapter 1. CDB has generated this application based on the abstract Java class VisitSchedule shown in Example 9-8.

Example 9-8. VisitSchedule.java

package com.farata.demo.pharmasales;

import java.util.List;

/**
 * @daoflex:webservice
 *   pool=jdbc/pharma
 */
public abstract class VisitSchedule {
    /**
     * @daoflex:sql
     *  pool=jdbc/pharma
     *  sql=::  SELECT
     *   visit_schedule.id as id,
     *   visit_schedule.salesman_id as salesman_id,
     *   visit_schedule.address_id as address_id,
     *   visit_schedule.scheduled_date as scheduled_date,
     *   CONCAT(salesmen.fname, " ", salesmen.lname) as fullname,
     *   CONCAT(addresses.addr_line_1, ", ", addresses.city, ", ",
                                 addresses.state) as fulladdress,
     *   visits.comments as comments
     *   FROM (visit_schedule LEFT JOIN visits ON visit_schedule.id =
                       visits.visit_schedule_id), salesmen, addresses
     *   WHERE
     *   visit_schedule.salesman_id = salesmen.id AND
     *   visit_schedule.address_id = addresses.id
     *
     *  ::
     *  transferType=VisitScheduleDTO[]
     *  keyColumns=id, salesman_id, address_id, scheduled_date
     *  updateTable=visit_schedule
     */
     public abstract List getVisitSchedules();

     /**
      * @daoflex:sql
      *  pool=jdbc/pharma
      *  sql=::  SELECT
      *   visit_schedule.id as id,
      *   visit_schedule.salesman_id as salesman_id,
      *   visit_schedule.address_id as address_id,
      *   visit_schedule.scheduled_date as scheduled_date,
      *   CONCAT(salesmen.fname, " ", salesmen.lname) as fullname,
      *   CONCAT(addresses.addr_line_1, ", ", addresses.city, ", ",
                addresses.state) as fulladdress,
      *   visits.comments as comments
      *   FROM (visit_schedule LEFT JOIN visits ON visit_schedule.id =
                        visits.visit_schedule_id), salesmen, addresses
      *   WHERE
      *   visit_schedule.salesman_id = salesmen.id AND
      *   visit_schedule.address_id = addresses.id AND
      *   CONCAT(salesmen.fname, " ", salesmen.lname)=:fullName
      *  ::
      *  transferType=VisitScheduleDTO[]
      *  keyColumns=salesman_id, address_id, scheduled_date
      *  updateTable=visit_schedule
      */
      public abstract List getVisitSchedulesBySalesman(String fullName);

}

The generated Java code that implements the methods declared in the abstract class in the example is located in the project air.offline.demo.web in the Java file ResourcesLibrariesWeb App Librariesservices-generated.jar. You need to open the Eclipse Java perspective to see this file.

The salesman and address drop-downs were populated using resources described in Chapter 6. AddressComboResource.mxml (Example 9-9) populates the address drop-down.

Example 9-9. ComboBoxResource.mxml

<?xml version="1.0" encoding="utf-8"?>
<resources:ComboBoxResource
   xmlns:resources="com.farata.resources.*"
   width="240"
   dropdownWidth="240"
   destination="com.farata.demo.pharmasales.Address"
   keyField="id"
   labelField="fulladdress"
   autoFill="true"
   method="getAddressesCombo"
   >
</resources:ComboBoxResource>

The component SalesmanComboResource.mxml (Example 9-10) takes care of the salesman drop-down.

Example 9-10. SalesmanComboBoxResource.mxml

<?xml version="1.0" encoding="utf-8"?>
<resources:ComboBoxResource
   xmlns:resources="com.farata.resources.*"
   width="240"
   dropdownWidth="240"
   destination="com.farata.demo.pharmasales.Salesman"
   keyField="id"
   labelField="fullname"
   autoFill="true"
   method="getSalesmenCombo"
   >
</resources:ComboBoxResource>

Now you’re ready to get into the nitty-gritty details of the project air.offline.demo, which is used by salespeople and contains the code for monitoring network connectivity, data synchronization, and integration with Google Maps.

The PharmaSales Application for Salespeople

The PharmaSales application starts with a logon screen (Figure 9-14) that requires the user to enter a valid full name to retrieve the schedule for that person (the password is irrelevant here).

The PharmaSales logon screen

Figure 9-14. The PharmaSales logon screen

Just to double-check that the newly inserted schedule gets downloaded to the client’s computer, log on as a salesperson who has scheduled visits.

Detecting Network Availability

Note the two round indicators at the bottom of the logon screen that show both the network and the application server statuses. There are two reasons why an AIR application might not be able to connect to its server-side components: either there is no connection to the network or the application server doesn’t respond. Take a look at how an AIR application can detect whether the network and a URL resource are available.

Any AIR application automatically has access to a global object called flash.desktop.NativeApplication. This object has a number of useful properties and methods that can give you runtime access to the application descriptor, provide information about the number of the opened windows, and also provide other application-wide information.

Note

You may want to get familiar with yet another useful class called flash.system.System. For one thing, this class has a method gc() that forces the garbage collector to kick in to avoid memory leaks in your AIR application.

To catch a change in the network connectivity, your application should check the NativeApplication’s property nativeApplication, which points to an object dispatching events when the network status changes. Your application can almost immediately detect a change in the connectivity by listening to the Event.NETWORK_CHANGE event as shown here:

flash.desktop.NativeApplication.nativeApplication.addEventListener(
                 Event.NETWORK_CHANGE, onNetworkChange);

Unfortunately, this event may be triggered with a 10- to 15-second delay after the network status changes, and it does not bear any specific information about the current status of the network. This means that after receiving this event, you still need to test the availability of a specific network resource that your application is interested in.

Note

The PharmaSales application uses Google Maps to help salespeople find the doctors’ offices they need to visit. Hence if the network is not available, the application would lose the ability to work with maps.google.com and will have to switch to Plan B, discussed later in the section Integrating with Google Maps.

If you check the library path of a Flash Builder AIR project, you’ll find there a library servicemonitor.swc, which includes SocketMonitor and URLMonitor classes. These classes can monitor the availability of a specific socket or URL resource.

You can start monitoring the status of a specific HTTP-based resource by calling URLMonitor.start() and periodically checking the property URLMonitor.available.

Example 9-11 is the complete code of the NetworkStatus.mxml component, which monitors both the status of the network (http://maps.google.com) and the PharmaSales application server and displays either a red or green light depending on the health of the corresponding resource.

Example 9-11. Monitoring network status: NetworkStatus.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:ControlBar xmlns:mx="http://www.adobe.com/2006/mxml" horizontalAlign="left"
width="100%" creationComplete="onCreationComplete()" height="55">
   <mx:Canvas width="200" height="55">
      <mx:Label text="Server status:"/>
      <mx:Image id="serverStatusIcon" x="125" source="{serverConnected ?
                  'assets/connected.gif' : 'assets/disconnected.gif'}"/>
      <mx:Label text="Google maps status: " y="26"/>
      <mx:Image id="googleMapsStatusIcon" x="125" y="26"
             source="{googleMapsConnected ? 'assets/connected.gif' :
                                           'assets/disconnected.gif'}"/>
   </mx:Canvas>
   <mx:Script>
      <![CDATA[
         import air.net.URLMonitor;
         import mx.messaging.config.ServerConfig;

         //Monitor connection status every second
         private static const TIMER_INTERVAL:int=1000;
         private static var _googleMapsURLMonitor:URLMonitor;
         private static var _serverURLMonitor:URLMonitor;

         public function get googleMapsConnected():Boolean{
            return _googleMapsURLMonitor && _googleMapsURLMonitor.available;
         }

         public function get serverConnected():Boolean{
            return _serverURLMonitor && _serverURLMonitor.available;
         }

         public function onCreationComplete():void{

            if (_googleMapsURLMonitor == null){
               initGoogleMapsURLMonitor();
            }
            _googleMapsURLMonitor.addEventListener(StatusEvent.STATUS,
                                                       showGoogleMapsStatus);

            if (_serverURLMonitor == null){
               initServerURLMonitor();
            }
            _serverURLMonitor.addEventListener(StatusEvent.STATUS,
                                                        showServerStatus);
         }

         private function initGoogleMapsURLMonitor():void{
            var request:URLRequest=new
                                   URLRequest("http://maps.google.com/");
            request.method="HEAD";
            _googleMapsURLMonitor=new URLMonitor(request);
            _googleMapsURLMonitor.pollInterval=TIMER_INTERVAL;
            _googleMapsURLMonitor.start();
         }

         private function initServerURLMonitor():void{
            var xml:XML=ServerConfig.serverConfigData;
            var channels:XMLList=xml.channels.channel.(@id == "my-amf");
            var channelConfig:XML=channels[0];
            var uri:String=
         channelConfig.endpoint[0].attribute(ServerConfig.URI_ATTR).toString();
            _serverURLMonitor=new URLMonitor(new URLRequest(uri));
            _serverURLMonitor.pollInterval=TIMER_INTERVAL;
            _serverURLMonitor.start();
         }

         private function showServerStatus(evt:StatusEvent):void{
            serverStatusIcon.source=_serverURLMonitor.available ?
                        "assets/connected.gif" : "assets/disconnected.gif"
         }

         private function showGoogleMapsStatus(evt:StatusEvent):void {
            googleMapsStatusIcon.source=_googleMapsURLMonitor.available ?
                         "assets/connected.gif" : "assets/disconnected.gif"
         }
      ]]>
   </mx:Script>
</mx:ControlBar>

In Example 9-11, the network status is being checked as often as specified in the polling interval:

_googleMapsURLMonitor.pollInterval=TIMER_INTERVAL;

The NetworkStatus component checks the health of an HTTP resource using the URLMonitor object that listens to StatusEvent in the function initNetworkURLMonitor(). Based on our experience, the pollInterval does not guarantee that notifications of connectivity changes will arrive at the intervals specified in the TIMER_INTERVAL constant.

As an alternative, you can create a Timer object and check the value of URLMonitor.available in the timer’s event handler function. If you decide to go this route, keep in mind that it has additional overhead, which comes with any timer object.

Example 9-11 demonstrates yet another useful technique to specify the URI of the network resource without the need to hardcode it in the program as is done in the method initNetworkURIMonitor():

new URLRequest('http://maps.google.com/')

The chances that the URL of Google Maps will change are rather slim. But the URL of the PharmaSales server will definitely be different, say, in development, QA, and production environments. The function initServerURIMonitor() extracts the URI of the server based on the information about the location of the AMF channel in the server-config.xml of the JEE server that was specified during the creation of the Flex project.

This information is available inside the SWF file, and if your PharmaSales server runs locally, the value of the uri variable from the method initServerURIMonitor() may look as follows:

http://localhost:8080/air.offline.demo.web/messagebroker/amf

To test this component, you can emulate the network outage by physically unplugging the network wire. To test whether the monitoring of the PharmaSales server works properly, just stop the server where the Java portion of the air.offline.demo.web application has been deployed (in our case, we were stopping the Apache Tomcat server configured in Eclipse IDE).

After the Salesperson Logs On

The PharmaSales application is used by salespeople. After a successful logon, the following code is invoked:

private function initCollections():void{

 visitCollection=new OfflineDataCollection("com.farata.demo.pharmasales.Visit",
                                     "getVisitsBySalesman", VisitDTO);
 visitCollection.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, showStatus);

 visitDataCollection=new OfflineDataCollection(
   "com.farata.demo.pharmasales.VisitData", "getVisitDataBySalesman",VisitDataDTO);

 fill_onClick();
}

...
private function fill_onClick():void {
   visitDataCollection.fill(username.text);
   visitCollection.fill(username.text);
}

This code populates two collections (visitCollection and visitDataCollection) by bringing the salesperson’s (username.text) data from the server. For example, after logon, Liz Anthony will see only her schedule of visits.

The visitCollection object will participate in data synchronization with a remote database server, as it has to keep the table visits up-to-date.

The visitDataCollection object brings the data from visit_schedule plus the comments field from the table visits. This collection doesn’t need to be synchronized, as the visit_schedule table is being taken care of by a dispatcher of the corporation Acme Pharm.

You’ll get familiar with the code of the class OfflineDataCollection later in this chapter, but for now suffice it to say that its function fill() will retrieve all the data from a Java class that is configured in the remoting-config.xml file of BlazeDS (or LCDS).

For example, the following code creates an instance of OfflineDataCollection that’s ready to work with the server-side destination com.faratasystems.demo.pharmasales:

visitCollection = new
           OfflineDataCollection("com.farata.demo.pharmasales.Visit",
                                   "getVisitsBySalesman", VisitDTO)

In general, an application developer needs to decide which DTOs are to be saved in the local storage and should specify them while instantiating one or more OfflineDataCollection objects.

The function initCollection() assigns an event listener to the visitCollection just to display the current status of the data on the UI (e.g., the data is saved in the local database).

The call of the method fill() on OfflineDataCollection gets converted by BlazeDS to a server-side call to Java’s method getVisitBySalesman(), which returns instances of the VisitDTO objects with the information about the visits of the salesperson. The first argument of the OfflineDataCollection constructor is the name of the remote destination, the second one is the name of the method to call, and the third one is the type of the ActionScript DTOs arriving to the client.

When the user logs on to the PharmaSales application, his computer doesn’t have any local databases. The local database is being created during the first call to the method fill(), described in the section on OfflineDataCollection.

Open your application storage directory after running the application for the very first time, and you’ll find there a file called local.db (in Windows, it’s C:Documents and SettingsAdministratorApplication DataPharmaSalesLocal Store). This database is not a copy of all the tables of the remote database—it stores only the data arrived in the form of DTOs from the server.

As you continue using the application, you’ll find yet another file in the same directory. The file local.db.bak is a backup copy of the local.db file created when you modified the data in a disconnected mode.

You’ll better understand when, how, and why these databases are created after reading the next section of this chapter, which describes the class OfflineDataCollection. At this point, just remember that after the method fill() is complete, you have two databases that store application-specific DTOs on your local computer.

When the user starts working with the application, he needs to be able to save and sync the data with the remote server, which is done in the PharmaSales application in the function onSave():

visitCollection.sync();
visitDataCollection.updateLocalDB(); // update visit comments
visitDataCollection.backUp();
visitDataCollection.resetState();

You sync only the data from the visitCollection here, as it represents the data from the remote table visits.

The visitDataCollection object represents the remote table visit_schedule, which is not being changed by the salesperson and hence doesn’t need to be synchronized. You call the function backup() here just to make the database tables supporting visitDataCollection identical in the main and backup databases.

Example 9-12 contains the complete code of the file PharmaSales.mxml. This application was initially generated by Clear Data Builder, as explained in Chapter 1. In addition to generating all the code for Flex and Java, it includes such functionality as master/detail relationships.

When the user clicks on the visit row in the DataGrid, the detail screen where the salesperson enters visit details opens up. This application uses the DataForm and DataFormItem components described in Chapter .

The UI portion of the PharmaSales application contains a ViewStack component that wraps the following views:

  • Logon

  • Grid with visits

  • Visit details

  • Google Maps

Example 9-12. PharmaSales.mxml

<?xml version="1.0" encoding="UTF-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:fx="http://www.faratasystems.com/2008/components" width="800" height="600"
xmlns:controls="com.farata.controls.*" backgroundColor="white" xmlns:ns1="*">
   <mx:ViewStack id="vs" height="100%" width="100%">

      <!--   Logon view   -->
         <mx:Canvas height="100%" width="100%">
       <mx:Panel title="Pharma Sales - Salesman" width="100%"
                  verticalAlign="middle" horizontalAlign="center" height="100%"
                   backgroundImage="assets/PillHand.png">
         <mx:Form>
           <mx:FormItem label="Username:" required="true">
            <mx:TextInput id="username" text="Liz Anthony" maxChars="16"/>
           </mx:FormItem>
           <mx:FormItem label="Password:" required="true">
            <mx:TextInput id="password" text="p455w0rd" maxChars="16"
                                           displayAsPassword="true"/>
           </mx:FormItem>
         </mx:Form>
         <mx:ControlBar horizontalAlign="right">
        <ns1:NetworkStatus/>
        <mx:Button id="logon" label="Logon" click="onLogon()"/>
            <mx:Button id="reset" label="Reset" click="onReset()"/>
         </mx:ControlBar>
        </mx:Panel>
      </mx:Canvas>

         <!--   Data grid view with visits  -->
      <mx:Canvas height="100%" width="100%">
        <mx:Panel title="Pharma Sales - Salesman" width="100%" height="100%">
         <fx:DataGrid toolTip="Double click for details"
                doubleClick="onDoubleClick()" doubleClickEnabled="true"
                horizontalScrollPolicy="auto" width="100%" id="dg"
                dataProvider="{visitDataCollection}" editable="true" height="100%">
          <fx:columns>
            <fx:DataGridColumn dataField="fullname" editable="false"
                       headerText="Salesman"/>
            <fx:DataGridColumn dataField="fulladdress"
                      editable="false" headerText="Address" width="150"/>
            <fx:DataGridColumn dataField="scheduled_date" editable="false"
                     headerText="Scheduled Date" itemEditor="mx.controls.DateField"
                     editorDataField="selectedDate" formatString="shortDate"/>
            <fx:DataGridColumn dataField="comments" editable="false"
                                                  headerText="Comments"/>
            </fx:columns>
            </fx:DataGrid>
           <mx:ControlBar horizontalAlign="right">
            <ns1:NetworkStatus id="network"/>
            <mx:Button enabled="{dg.selectedIndex != -1 &&
                         (network.googleMapsConnected || hasMapImage())}"
                         click="googleMap_onClick()" label="Google Map"/>
            <mx:Button enabled="{!visitCollection.commitRequired &&
                                   !visitCollection.syncRequired}"
                                  click="fill_onClick()" label="Retrieve"/>
            <mx:Button enabled="{ visitCollection.commitRequired ||
                             visitCollection.syncRequired}" click="onSave()"
                       label="{visitCollection.commitRequired?'Save':'Sync'}"/>
            <mx:Button click="vs.selectedIndex=0;" label="Log out"/>
           </mx:ControlBar>
         </mx:Panel>
      </mx:Canvas>

         <!--   Visit detail view -- >
      <mx:Canvas>
         <mx:Panel width="100%" height="100%" title="Visit Details">
            <fx:DataForm dataProvider="{dg.selectedItem}">
            <fx:DataFormItem dataField="fullname" label="Salesman:"
                                                           enabled="false"/>
            <fx:DataFormItem dataField="fulladdress" label="Address:"
                                                           enabled="false"/>
            <fx:DataFormItem dataField="scheduled_date" label="Scheduled
                              Date:" formatString="shortDate" enabled="false"/>
            </fx:DataForm>
            <fx:DataForm dataProvider="{visit}" width="100%">
            <fx:DataFormItem dataField="visit_date" label="Visit Date:"
                                                     formatString="shortDate"/>
            <fx:DataFormItem dataField="contact_name"
                                         label="Contact   Name:" width="100%"/>
            <fx:DataFormItem dataField="comments" label="Comments:"
                                                                  width="100%">
               <mx:TextArea width="100%" height="100"/>
            </fx:DataFormItem>
            </fx:DataForm>
           <mx:ControlBar horizontalAlign="right">
            <ns1:NetworkStatus/>
            <mx:Button label="Back" click=
                       "vs.selectedIndex=1;updateVisitSchedule(dg.selectedItem)"/>
           </mx:ControlBar>
          </mx:Panel>
      </mx:Canvas>

          <!--   Google Maps integration view-->
      <mx:Canvas>
         <mx:Panel width="100%" height="100%" title="Google Map">
         <maps:Map xmlns:maps="com.google.maps.*" id="map"
                mapevent_mapready="onMapReady(event)" width="100%" height="100%"
                key="ABQIAAAAthGneZS6I6ekX8SgzwL2HxSVN_sXTad_Y..."
                url="http://code.google.com/apis/maps/"/>
           <mx:ControlBar horizontalAlign="right">
           <ns1:NetworkStatus/>
           <mx:Button click="saveMap()" label="Save"/>
           <mx:Button click="vs.selectedIndex=1;" label="Back"/>
           </mx:ControlBar>
         </mx:Panel>
      </mx:Canvas>

         <!--   Saved Google map view-->
      <mx:Canvas>
         <mx:Panel id="map_image" width="100%" height="100%"
                                               title="Google Map">
         <mx:Image id="saved_map" width="100%" height="100%"
                                   creationComplete="openMapImage()"/>
         <mx:ControlBar horizontalAlign="right">
            <ns1:NetworkStatus/>
            <mx:Button click="vs.selectedIndex=1;" label="Back"/>
         </mx:ControlBar>
         </mx:Panel>
      </mx:Canvas>
   </mx:ViewStack>

   <mx:Script>
      <![CDATA[
         import com.google.maps.overlays.Marker;
         import com.google.maps.InfoWindowOptions;
         import com.google.maps.LatLng;
         import com.google.maps.services.ClientGeocoder;
         import mx.graphics.codec.PNGEncoder;
         import com.google.maps.controls.ZoomControl;
         import com.farata.demo.pharmasales.dto.VisitDataDTO;
         import com.farata.demo.pharmasales.dto.VisitDTO;
         import com.farata.collections.OfflineDataCollection;
         import com.google.maps.services.GeocodingEvent;
         import mx.events.PropertyChangeEvent;

         [Bindable]
         public var visitDataCollection:OfflineDataCollection;
         [Bindable]
         public var visitCollection:OfflineDataCollection;
         [Bindable]
         public var visit:VisitDTO;

         private function onSave():void {
            visitCollection.sync();
            visitDataCollection.updateLocalDB();
            visitDataCollection.backUp();
            visitDataCollection.resetState();
         }

         private function onDoubleClick():void {
            if (dg.selectedItem){
               vs.selectedIndex=2;
               calculateVisit(dg.selectedItem);
            }
         }

         private function updateVisitSchedule(obj:Object):void {
            var dto:VisitDataDTO=obj as VisitDataDTO;
            dto.comments=visit.comments;
         }

         private function calculateVisit(obj:Object):void {
            var dto:VisitDataDTO=obj as VisitDataDTO;
            for(var i:int=0; i < visitCollection.length; i++){
               var visitDto:VisitDTO=visitCollection[i]as VisitDTO;
               if (dto.id == visitDto.visit_schedule_id) {
                  visit=visitDto;
                  return ;
               }
            }
            visit=new VisitDTO();
            visit.visit_schedule_id=dto.id;
            visitCollection.addItem(visit);
         }

         private function initCollections():void {
             visitCollection=new OfflineDataCollection(
                      "com.farata.demo.pharmasales.Visit",
                                "getVisitsBySalesman", VisitDTO);
            visitCollection.addEventListener(
                      PropertyChangeEvent.PROPERTY_CHANGE, showStatus);
            visitDataCollection=new OfflineDataCollection(
                      "com.farata.demo.pharmasales.VisitData",
                      "getVisitDataBySalesman", VisitDataDTO);
            fill_onClick();
         }

         private function showStatus(evt:PropertyChangeEvent):void {
            if (evt.property == "statusMessage"){
               status=evt.newValue as String;
            }
         }

         private function fill_onClick():void {
            visitDataCollection.fill(username.text);
            visitCollection.fill(username.text);
         }

         private function googleMap_onClick():void {
            if (network.googleMapsConnected) {
               cursorManager.setBusyCursor();
               vs.selectedIndex=3;
               showAddress();
            }
            else {
               vs.selectedIndex=4;
               openMapImage();
            }
         }

         private function onLogon():void {
            initCollections();
            vs.selectedIndex=1;
         }

         private function onReset():void {
            username.text="Liz Anthony";
         }

         private function onMapReady(event:Event):void {
            map.setZoom(20);
            showAddress();
         }

         private function deleteMap():void {
            var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
            var file:File= File.applicationStorageDirectory.resolvePath(
                                                   dto.fulladdress + ".png");
            if (file.exists){
               file.deleteFile();
            }
         }

         private function saveMap():void {
            deleteMap();
            var bd:BitmapData=new BitmapData(map.width, map.height);
            bd.draw(map);
            var pngEncoder:PNGEncoder=new PNGEncoder();
            var ba:ByteArray=pngEncoder.encode(bd);
            var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
            var file:File=
                       File.applicationStorageDirectory.resolvePath(
                                                   dto.fulladdress + ".png");
            var fileStream:FileStream=new FileStream();
            fileStream.open(file, FileMode.WRITE);
            fileStream.writeBytes(ba);
            fileStream.close();
            status="Google map image is saved to '" + file.nativePath + "'";
         }

         private function openMapImage():void {
            if (saved_map && saved_map.initialized){
               var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
               var file:File=
                           File.applicationStorageDirectory.resolvePath(
                                                   dto.fulladdress + ".png");
               saved_map.source=file.nativePath;
               map_image.title="Displaying '" + file.name + "'";
            }
         }

         private function hasMapImage():Boolean
         {
            var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
            var file:File= File.applicationStorageDirectory.resolvePath(
                                                dto.fulladdress + ".png");
            return file.exists;
         }

         private function showAddress():void {
            if (map && map.initialized){
               var cg:ClientGeocoder=new ClientGeocoder();
               cg.addEventListener(
                         GeocodingEvent.GEOCODING_SUCCESS, onGeocodeSuccess);
               var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
               cg.geocode(dto.fulladdress);
            }
         }

         private function onGeocodeSuccess(event:GeocodingEvent):void{
            cursorManager.removeBusyCursor();
            var point:LatLng=event.response.placemarks[0].point as LatLng;
            var marker:Marker=new Marker(point);
            map.addOverlay(marker);
            map.setCenter(point);
            var dto:VisitDataDTO=dg.selectedItem as VisitDataDTO;
            var opt:InfoWindowOptions=new InfoWindowOptions();
            opt.drawDefaultFrame=true;
            opt.contentHTML=dto.fulladdress;
            marker.openInfoWindow(opt);
         }
      ]]>
   </mx:Script>
</mx:WindowedApplication>
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.140.153