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).
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.
For testing the PharmaSales application, you’ll need the following software installed:
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.
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 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.
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).
Click the Add button to open another view and schedule a new visit for any salesperson (Figure 9-13).
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 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).
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.
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.
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.
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
initNetworkURL
Monitor()
. 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).
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 OfflineData
Collection
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>
3.133.140.153