Group transport
Group transport is a complex beast with a lot of interconnected entities that are required to correctly handle all requirements. This tutorial helps developers to implement a complete solution. The nature of CCP is that a developer can start with a basic implementation and build it out from there. This tutorial shows every part of the group transport component but developers can cherry-pick parts that they require for their specific needs.
This tutorial assumes the role of the user performing these actions is a transporter. All other roles have limited rights to the entities shown below and cannot, by design, perform all actions described in this tutorial. Parts of the tutorial are usable for other roles, like absence creation, but these will probably be described in other tutorials more suited for the task. All examples use the application/json wire-format for readability. You are free to use one of the other available wire-formats.
CCP was initially developed as a complete solution for group transport. In later iterations the platform evolved to include other solutions like booking and dispatch. Group transport includes a lot of interrelated entities. The following list contains all relevant entities needed for group transport.
| EntitySet | Description |
|---|---|
| Contractors | The contractor is the entity that issues a public tender for transporters. |
| Transporters | The transporter is the company performing the transport of one or more travelers. |
| Travelers | The traveler is the person that is transported. |
| Trips | Group transport consists of a route which has one or more pickups and drop-offs. The combined route is stored in a generic Trip entity. |
| Movements | A movement is a combination of a pickup part and a drop-off part. Group transport routes have one or more movements, usually more than one. Every movement is linked to a traveler and a contractor. It optionally connects to a location for pickup and/or drop-off. |
| Locations | Location is a destination for the route. It’s a separate entity so that multiple movements point to the same location including linked entities. This way users can be created on the system that only have access to movements and travelers linked with that location. |
| Indications | Indications are a generic way to link special attributes to specific entities. In group transport it can be used to add indications like “wheelchair” to a traveler or indications about the execution to a movement or trip. To retrieve a list of available indications in the system perform a GET request to /SystemIndications. |
| Amounts | Amounts are used for the monetary amounts of movements and trips. Each trip or movement can have multiple amounts for different stakeholders e.g. an amount the traveler needs to pay or an amount to bill a contractor. |
| Absences | Group transport consists of a base route that has a recurring pattern. It is mostly executed in the same order but sometimes a traveler can be ill, on vacation or some other reason for absence. An absence can be created and linked to a traveler and CCP takes care of the rest. It automatically checks if a traveler is absent on the requested movements and returns this in the AbsenceStateName property of the Movement. |
| LocationExceptions | A LocationException is to a location what an absence is to a traveler. It can contain the dates that a location is closed. CCP automatically process these exceptions when requesting movements in the same way it does with absences. |
| MovementSchedules | A MovementSchedule can be used to store the base routes for a traveler. Specifically in what period, on which days, the traveler has to be picked up and to where. It’s also used to store exceptions on that base route. The information can be used to perform a dynamic planning of routes based on information stored in these entities. |
| Vehicles | Vehicles are stored as their own entity and linked to the Trip entity. This is required to perform the more advanced features of CCP like retrieval of data communication and position data and ETA calculation. |
| Drivers | Drivers are their own entity and can be linked to a Trip. By linking a user account to this driver entity, a driver can retrieve his or her own Trips and much more. |
The reference section contains detailed information about the entities and their relationships, including multiplicity, to other entities.
Because of the need for linked entities, a complete route can only be added in steps. First all related entities need to be created before adding trips and movements. Depending on the contractor/transporter construction, some related entities could already be inside CCP like travelers and locations. In this case the transporter needs to make sure that the entities inside CCP are linked to the same internal entity. This off course to prevent duplicates.
For group transport it’s recommended to create related entities, when needed, in the following order. Using this order you ensure that every relation between entities can be resolved.
- Contractors
- Travelers
- MovementSchedules
- Absences
- Locations
- Location Exceptions
- Drivers
- Vehicles
- Trips
- Movements
- Indications
For this tutorial we assume that the contractor is the leading entity. The contractor owns the travelers and locations. The contractor and each traveler is linked to a transporter. This way the transporter can request the contractor, locations and related travelers. The transporter is responsible for the routes and related entities like driver and vehicle. The contractor is responsible for the travelers, locations, movement schedules and absences.
Basic Synchronization
To simplify the examples a required for synchronization is omitted from the request. You don’t want to request every single entity every single syncing interval. You can use a sliding interval to request only the results that are newer than the previous changes. Read the section Synchronization for more information about this.
Synchronizing basic entities
In this case the contractor owns the primary group transport entities. First you must retrieve this information so you can link these entities to your routes. For tips about synchronizing entities read the chapter Synchronization.
Contractors
We start by requesting contractors. Issue a GET request to:
/Contractors
The following result is returned as a collection of Contractor entities.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Contractors",
"value" : [{
"ContractorId" : 1,
"CompanyName" : "Opdrachtgever test",
"LastUpdated" : "2014-03-12T08:52:10.64",
"Deleted" : false
}
]
}
This contractor with ContractorId 1 is our main contractor for the routes. In every Trip and Movement that the transporter sends to CCP this ContractorId must be included. This way the right Trips will be visible to the contractor. You probably want to link this ContractorId to an internal Contractor in your own data-store.
Travelers
Next we’ll request the travelers. Issue a GET request to:
/Travelers
The following results are returned as a collection of Traveler entities.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Travelers",
"value" : [{
"TravelerId" : 3320,
"FirstName" : "Lars",
"LastName" : "Jong",
"NamePrefix" : "de",
"Initials" : "LJAM",
"Salutation" : "Dhr.",
"SexName" : "Male",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : "1987-05-15T00:00:00",
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2013-06-11T14:19:27.65",
"Deleted" : false
}, {
"TravelerId" : 8240,
"FirstName" : "",
"LastName" : "Stephan",
"NamePrefix" : "",
"Initials" : null,
"Salutation" : null,
"SexName" : "Unknown",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : null,
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2014-02-27T15:24:18.347",
"Deleted" : false
}
]
}
Addresses and contacts
By default the Traveler entity does not include addresses and contactinformation because these are stored in seperate related entities. The reason for this is because entities can have multiple addresses and contacts. If you’re only interested in the primary address and contact you can issue a GET request to /TravelersSummary. This returns a collection of TravelersSummary entities which include the primary address and primary contact.
If you want to request all addresses and contacts of the travelers you can use the OData $expand query option to include these related entities. Issue a GET request to:
/Travelers?$expand=Addresses,ContactInformationData
If you want to update addresses or contacts you must use the $expand version. In this version the result includes the unique identification of the address and contact entities. This id is required when updating or deleting an address or contact. The summary entities are designed for convencience when displaying the information.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Travelers",
"value" : [{
"Addresses" : [{
"AddressId" : 1755055,
"Street" : "Lieve Vrouweplein",
"Number" : 9,
"Addition" : "10",
"PostalCode" : "5038TS",
"State" : null,
"Town" : "Tilburg",
"LastUpdated" : "2014-12-08T10:27:57.04",
"PrimaryAddress" : true,
"Deleted" : false,
"TravelerId" : 3320,
"ContractorId" : null,
"TransporterId" : null,
"DriverId" : null,
"CountryId" : 528,
"RoutePointId" : null,
"AgentId" : null,
"FavoriteId" : null,
"Coordinates" : {
"Latitude" : 51.554100000000005,
"Longitude" : 5.0796100000000006,
"FormatName" : "GeoDecimal"
}
}
],
"ContactInformationData" : [{
"ContactInformationId" : 16613,
"FirstName" : null,
"LastName" : null,
"PhoneNumberWork" : "0134609282",
"PhoneNumberWork2" : null,
"FaxNumber" : "0134609281",
"FaxNumber2" : null,
"PhoneNumberPrivate" : null,
"PhoneNumberPrivate2" : null,
"MobileNumberWork" : null,
"MobileNumberPrivate" : null,
"EmailAddress" : null,
"EmailAddress2" : null,
"PrimaryContact" : true,
"Description" : null,
"LastUpdated" : "2014-12-08T10:28:52.887",
"Deleted" : false,
"TravelerId" : 3320,
"ContractorId" : null,
"TransporterId" : null,
"DriverId" : null,
"LocationId" : null,
"AgentId" : null
}
],
"TravelerId" : 3320,
"FirstName" : "Lars",
"LastName" : "Jong",
"NamePrefix" : "de",
"Initials" : "LJAM",
"Salutation" : "Dhr.",
"SexName" : "Male",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : "1987-05-15T00:00:00",
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2013-06-11T14:19:27.65",
"Deleted" : false
}, {
"Addresses" : [],
"ContactInformationData" : [],
"TravelerId" : 8240,
"FirstName" : "",
"LastName" : "Stephan",
"NamePrefix" : "",
"Initials" : null,
"Salutation" : null,
"SexName" : "Unknown",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : null,
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2014-02-27T15:24:18.347",
"Deleted" : false
}
]
}
When looking at the address and contactinformation data you can see the TravelerId embedded in the data. For other entities their respective fields are filled with the corresponding id of the owning entity. This concept opens up the possibility to request address and contact information in separate requests for multiple entities at once. This is more efficient than expending every entity with addresses and contactinformation. Addresses and contactinformation data are updated independant of the main entity. When using a sync strategy and combine this with requesting address and contactinformation separately, you get a very efficient system that only returns results when addresses or contacts are added or updated.
Linked Contractors
A traveler can be linked to one or more contractors. Your system may want to know which travelers are linked to which contractors. CCP links these automatically when a transporter add trips and movements. In these entities a contractor is required to show the trips and movements to the right contractor. The linked traveler in the movements are automatically linked to that contractor. It’s also possible for the transporter to link these manually but this is outside the scope of this tutorial.
Requesting linked contractors is as simple as expanding the contractors entityset when requesting travelers. This will return the full contractor entity. You already have the contractors so you only need the unique ContractorId. You can use the OData query option $select to only select the fields you need. In this case ContractorId. Use the following GET request to retrieve the travelers with their respecting linked contractors.
/Travelers?$expand=Contractors&$select=*,Contractors/ContractorId
The following result is returned.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Travelers&$select=*,Contractors/ContractorId",
"value" : [{
"Contractors" : [{
"ContractorId" : 1
}
],
"TravelerId" : 3320,
"FirstName" : "Lars",
"LastName" : "Jong",
"NamePrefix" : "de",
"Initials" : "LJAM",
"Salutation" : "Dhr.",
"SexName" : "Male",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : "1987-05-15T00:00:00",
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2013-06-11T14:19:27.65",
"Deleted" : false
}, {
"Contractors" : [{
"ContractorId" : 1
}
],
"TravelerId" : 8240,
"FirstName" : "",
"LastName" : "Stephan",
"NamePrefix" : "",
"Initials" : null,
"Salutation" : null,
"SexName" : "Unknown",
"Synchronize" : false,
"Description" : null,
"DateOfBirth" : null,
"ContractorAdministrationNumber" : null,
"CitizenIdentificationNumber" : null,
"CostCenterId" : null,
"LastUpdated" : "2014-02-27T15:24:18.347",
"Deleted" : false
}
]
}
Movement schedules
After requesting and storing the travelers, you can request the MovementSchedules of those Travelers. MovementSchedules are used to inform the transporter of mutations on existing schedules or new schedules for new or existing travelers. The information in these schedules can be used to modify the planning. The MovementSchedules are fairly complex because the subject matter is complex. A basic schedule is made up of traveler, day of the week, direction, validity, pickup/dropoff information and a lot more. If a traveler is transported 5 days a week, 2 times a day that’s 10 schedules per traveler.
More information about Movement schedules can be found in the tutorial Handling movement schedules.Absences
After requesting and storing travelers you can request absences of those travelers. It is recommended to request absences and use the included TravelerId to link the absence to the right Traveler. You can also add the $expand option to the request and expand the travelers but this results in a lot of redundant data. So first request the travelers to identify the traveler and from that point you can request absences. Request absences by issuing the following GET.
/Absences
The following result is returned.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Absences",
"value" : [{
"AbsenceId" : 63732,
"From" : "2014-12-08T00:00:00",
"Until" : null,
"Remark" : "",
"AbsenceTypeName" : "Ill",
"AbsenceScopeName" : "Both",
"LastUpdated" : "2014-12-08T11:13:21.637",
"Deleted" : false,
"TravelerId" : 3320
}
]
}
When working with absences we make a difference in Ill/Available records and Holiday/other records. They are handled and interpreted differently:
- Ill/Available records:
IllandAvailableare two seperate records. Once someone is reportedIll, the person will remainIlluntil he is reportedAvailable.- Both
IllandAvailablerecords are valid throughout the entire day. This means that only the date-part of theFromfield is used, not the time-part. - The field
Untilis not used. - If, for example, someone is reported
Illat 09:00, the report will also apply to trips that were planned earlier that same day or trips that are busy at the time of the report. Trips that are already performed that day remain obviously unaffected (If the Traveler was reportedIlltoo late, the Traveler would have been reportedNoShowby the Driver.) - If, for example, someone is reported
Availableat 16:00, the Traveler will already be expected available on the remaining trips that day. It that case it might be advisable to report the TravelerAvailablethe next day by filling in aFrom-date of the next day - If multiple
IllandAvailablereports are made the same day, theFromfield will not suffice to determine their order (as it contains only the date-part). The fieldRegistrationTimewill be leading in this particular case. AbsenceScopeNameslike "OnlyTo", "OnlyReturn" or "Both" are not applicable forIll/Availablerecords, because these records are always valid for the entire day.
Holiday/Otherrecords:
- Holidays are always one record each day. So if a Traveler is on a holiday from 15-01-2015 until 20-01-2015, six
Absence-records are added. - Of the field
Untilonly time-part will be used. - A
Holiday/Other-record can be valid in the following ways:
- The entire day: The time-part of the
Fromfield is 0:00, theUntilfield is eithernullor its time-part has the value 0:00. - A part of the day: the time-part of either the field
Fromor the field fieldUntilhas a value different from 0:00. - During trips towards/from a location:
AbsenceScopeNamewill contain "OnlyTo" or "OnlyReturn".
It's up to the transporter to interpret which trips that day are considered towards a location (AbsenceScopeName = "OnlyTo" applies here) or from a location (AbsenseScopeName = "OnlyReturn"). This can be confusing, especially when a Traveler needs to be transported from Location A to Location B (in that case the trip should be considered towards location B).
- The entire day: The time-part of the
- Holidays are always one record each day. So if a Traveler is on a holiday from 15-01-2015 until 20-01-2015, six
Locations
The next important part of group transport are locations. A Location is basically an address with a name, where travelers can be transported to or transported from. Every movement can have a location linked to the pickup or drop-off. By linking a location it is possible for supervisors of the locations, when they have an account, to request and update their owned data (e.g via Cabman Online, custom web application or mobile app). It’s recommended to synchronize the locations and use these locations when sending trips and movements.
Locations also have Closing and Opening hours. This information can be used by transporters in planning related tasks.
The last part of locations are LocationExceptions. These exceptions are for locations what absences are for Travelers. When a location is closed on a specific date or range of dates, this has consequences for every traveler that’s transported to that location on those dates. You could do the same by creating absences for every single traveler on that date but that is a lot of work. CCP checks the closing dates the same way as absences. You should do the same.
Requesting Locations is very straightforward. Just issue a GET on the following EntitySet.
/Locations
The following result is returned
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Locations",
"value" : [{
"LocationId" : 653,
"Description" : "Euphoria",
"LocationTypeName" : "Other",
"AirportCode" : null,
"LastUpdated" : "2013-09-06T12:52:48.613",
"ContractorId" : 1,
"Deleted" : false,
"Opening" : {
"Monday" : "1980-01-01T08:30:00",
"Tuesday" : "1980-01-01T08:30:00",
"Wednesday" : "1980-01-01T08:30:00",
"Thursday" : "1980-01-01T08:30:00",
"Friday" : "1980-01-01T08:30:00",
"Saturday" : "1980-01-01T00:00:00",
"Sunday" : "1980-01-01T00:00:00"
},
"Closing" : {
"Monday" : "1980-01-01T17:30:00",
"Tuesday" : "1980-01-01T17:30:00",
"Wednesday" : "1980-01-01T17:30:00",
"Thursday" : "1980-01-01T17:30:00",
"Friday" : "1980-01-01T17:30:00",
"Saturday" : "1980-01-01T00:00:00",
"Sunday" : "1980-01-01T00:00:00"
},
"Address" : {
"Street" : "Lieve-Vrouweplein",
"Number" : 9,
"Addition" : null,
"PostalCode" : null,
"State" : null,
"Town" : "Tilburg",
"CountryId" : 528,
"Coordinates" : {
"Latitude" : 51.555420000000005,
"Longitude" : 5.10583,
"FormatName" : "GeoDecimal"
}
}
}
]
}
You can see that the result includes the address without expanding. This is because in this case there can only be a single address associated with that location. In this case the address is not a collection of Address entities but a AddressFragment complex type. This is used for entities that can only have a single address. This is also the case with the Opening and Closing properties. These are LocationTimes complex types containing a time for every day of the week. The date part can be ignored.
Location Exceptions
After requesting and storing the locations you can request the location exceptions. Every LocationException entity contains a LocationId property that links back to the location. It is very similar to Absence and has a lot of the same properties. Request all exceptions using a GET to:
/LocationExceptions
The following result is returned.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#LocationExceptions",
"value" : [{
"LocationExceptionId" : 1,
"From" : "2014-12-25T00:00:00",
"Until" : "2014-12-25T00:00:00",
"Remark" : "Christmas",
"LastUpdated" : "2014-12-08T16:05:53.297",
"Deleted" : false,
"LocationId" : 653
}
]
}
Every date per location is it’s own unique entity. Because locationexceptions can also be changed it’s recommended to store the LocationExceptionId to know if you already processed the exception.
Drivers
A driver is an optional property of the trip. You can add trips without a driver but this is not recommended. A contractor may require information about the vehicle and driver. There are also a range of extra feature available when linking a driver. So we’ll include the driver in this tutorial.
Reading Drivers
Requesting drivers is the same as every other entity. Just execute a GET request to the entityset
/Drivers
This results in the a collection of Driver entities. The first request results, obviously, in zero results. This is the first entity in this tutorial that the transporter is fully responsible for.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Drivers",
"value" : [{
"DriverId" : 31,
"FirstName" : "Jeroen",
"LastName" : "Volkering",
"NamePrefix" : null,
"Initials" : "JTCF",
"Salutation" : "Dhr.",
"SexName" : "Male",
"Description" : null,
"DateOfBirth" : null,
"CitizenIdentificationNumber" : null,
"DriverCardNumber" : null,
"DatacommunicationDriverCode" : null,
"TransporterId" : 1,
"LastUpdated" : "2014-11-28T15:18:15",
"Deleted" : false
}
]
}
Writing Drivers
There are two possible workflows conceivable when writing Drivers to CCP. Both methods have their own advantages and disadvantages.
- Create every driver in your own data-store in a batch to CCP. The advantage is that drivers are all available from the get go en only need to be linked to the Trip. The disadvantages are that not all drivers will be used and you still need to maintain the drivers in case of new additions and mutations.
- Create the driver when needed. The advantage is that CCP only contains the drivers that are planned on trips sent to CCP. The initial load is reduced. The main disadvantage is that is a bit more complex to implement. Before creating a Trip to CCP the client application needs to check if the driver is already stored in CCP and if not create it first before creating the Trip.
There isn’t a single method that’s the correct and recommended one. It all depends on context and use-case. For group transport for a specific contactor the on-demand approach is best suitable because the drivers are not used for anything else. When used to real shift and vehicledata as a transporter or make this information available to drivers the first method is more suitable.
For our tutorial the on-demand approach is best. We assume that a trip is queued and that the attached driver is not avaible in CCP so it needs to be added.
Writing a Driver is pretty straight forward. Perform a POST request to:
/Drivers
There are a few http headers that are required. The are coverd in detail in the appropriate reference sections. The most important ones are authorization (for Basic Authentication), Content-type to notify the server of the format of the payload, Accept for the preferred format of the result and the versioning headers.
POST /CCPService/DataServiceCCP.svc/Drivers HTTP/1.1
Host: www.cabmanonline.nl
Authorization: Basic dGVzdEB0ZXN0LmNvbTp0ZXN0MTIz
Accept: application/json
Content-Type: application/json
DataServiceVersion: 3.0
MaxDataServiceVersion: 3.0
CabmanCloudPlatformVersion: 11.0
MaxCabmanCloudPlatformVersion: 11.0
We use application/json as the wire-format for readability. You can use every one of the three supported formats. The body of the request contains the Driver properties.
{
"FirstName" : "Jeroen",
"LastName" : "Volkering",
"Initials" : "JTCF",
"SexName" : "Male",
"DateOfBirth" : "1979-04-01T00:00:00",
"DriverCardNumber" : "12345678"
}
We only include properties that are required for the entity and that we have available from our client application. After executing the POST request the result should be a http 201 (created). The body of the response contains the newly created entity.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Drivers/@Element",
"DriverId" : 7782,
"FirstName" : "Jeroen",
"LastName" : "Volkering",
"NamePrefix" : null,
"Initials" : "JTCF",
"Salutation" : null,
"SexName" : "Male",
"Description" : null,
"DateOfBirth" : "1979-04-01T00:00:00",
"CitizenIdentificationNumber" : null,
"DriverCardNumber" : "12345678",
"DatacommunicationDriverCode" : null,
"TransporterId" : 1,
"LastUpdated" : "2014-12-09T09:59:59.71325Z",
"Deleted" : false
}
Store the DriverId with the internal identifiction of the driver for future reference. The DriverId is also needed to link the Driver to the Trip.
Vehicles
The workflow for handling Vehicles is mostly the same as it is for Drivers. The same considerations can be applied to Vehicles. This means that in our tutorial we choose the on-demand approach. There is a Trip queued and has a Vehicle assigned.
Reading Vehicles
To read existing Vehicles perform a GET request to:
/Vehicles
Existing vehicles are returned. In our case there are no Vehicles returned because there are no Vehicles in CCP yet.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Vehicles",
"value" : []
}
Writing Vehicles
We need to create our vehicle. Perform a POST to:
/Vehicles
There are a few required headers. These are explained in more detail in previous parts.
POST /CCPService/DataServiceCCP.svc/Drivers HTTP/1.1
Host: www.cabmanonline.nl
Authorization: Basic dGVzdEB0ZXN0LmNvbTp0ZXN0MTIz
Accept: application/json
Content-Type: application/json
DataServiceVersion: 3.0
MaxDataServiceVersion: 3.0
CabmanCloudPlatformVersion: 11.0
MaxCabmanCloudPlatformVersion: 11.0
Then we prepare the body. The only required field is the LicensePlate. Try to provide as much vehicle information as possible. The body for our tutorial contains the following properties.
{
"LicensePlate" : "66-BCT-6",
"Make" : "Volkswagen",
"Model" : "Passat TDI",
"FuelTypePrimaryName" : "D",
"Built" : 2012,
"VehicleNr" : 5
}
Most properties are self-explanatory. See the reference documentation for more details about the available properties. When the POST is successful the http statuscode of the response is 201 (created). The newly created entity is returned.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Vehicles/@Element",
"VehicleId" : 6549,
"LicensePlate" : "66-BCT-6",
"StrippedLicensePlate" : "66BCT6",
"Make" : "Volkswagen",
"Model" : "Passat TDI",
"FuelTypePrimaryName" : "D",
"FuelTypeSecondaryName" : null,
"Built" : 2012,
"Weight" : null,
"Telephone" : null,
"LastUpdated" : "2014-12-09T10:41:29.166375Z",
"Deleted" : false,
"TransporterId" : 1,
"VehicleTypeId" : null,
"VehicleNr" : 5
}
Store the VehicleId with your internal vehicle identification for future reference. The VehicleId property is also used to link a Vehicle to a Trip.
Register Data communication
If the transporter wants to automatically link historical data about the execution to the planned trips and movements there is an additional step to perform on the Vehicle. If the transporter uses Cabman BCT or Cabman CS vehicle equipment this is possible. To link the vehicle to our external FM data-store (FM-Cabman Fleet Management) which contains all historical data execute an Action available on the Vehicle. This action creates a link between the CCP vehicle and the “anonymous” vehicle in the FM store. All data in the FM store is linked to a unique unitname. The combination of a random customer code and that unitname is allowed to retrieve information. The customer code can be configured by Euphoria to prevent unauthorized access and protect privacy.
In our example we assume that the vehicle has a Cabman BCT that is configured with the unit name “TST123”. We want to link our newly created vehicle. Perform a POST request to the following function.
/Vehicles(6549)/ConnectFixedDevice
The body of the request is a single property called unitName. This must be encoded as json. This results in the following
{
"unitName" : "TST123"
}
The http result of the POST should be 200 (OK). It contains no payload. This will only succeed if the unitname is known and is a real existing Cabman CS of Cabman BCT unit. If this is not the case then the server will return the custom exception UnitNameDoesntExistException which you can see below. Our example unit doesn’t exist.
{
"odata.error" : {
"code" : "UnitNameDoesntExistException",
"message" : {
"lang" : "json",
"value" : "{\"UnitName\":\"TST123\",\"UnitCode\":null,\"VehicleId\":6549}"
}
}
}
Synchronizing Routes
When all prior entities are available in CCP you can finally start writing routes. Most of the previous work is a one-time effort. After that only new entities, changes and deletions must be processed but the source data doesn’t change very often so most of the time only routes are written to CCP.
A route in CCP consists of a Trip entity and several Movement entities. Both entities contain a lot of properties. Not all properties are required. Of the required properties most are filled automatically. The most important part to remember is that you must treat a trip and it’s movements as individual items. You can add, update and delete a single movement to minimize bandwidth. But to do this you need to store the CCP MovementId with your internal movement identification.
Every movement must be linked to an existing Trip entity. So first we must create a Trip entity and link this to our internal Trip identification. For group transport reading of Trips and Movements is not a strict requirement. This is because these two entities are controlled by the Transporter. It is possible for CCP to add extra information (e.g Calculate coordinates for movements, calculate cost). When the contractor demands that this information must be used by the Transporter reading is of course mandatory. To be thorough we include the reading of Trips and Movements.
NOTE: Like with the other entities in the tutorial, examples are kept simple and do not include synchronization logic. Please note that this is necessary in real-world scenarios. Read the Synchronization section how to implement this.
Reading Trips and Movements
Trips and Movements are two separate entities but inexplicably linked. While it’s possible to read Trips and Movement separately, in this case it’s logical to read them in a single request. In previous examples we used the OData $expand query option. For reading complete routes (Trip+Movements) this is the recommended approach. To read the full routes execute the following GET request:
/Trips?$expand=Movements
The result is a collection of Trip entities. Each Trip entity is expanded to include the linked Movements. This is a very large payload which is why we don’t print it here. The full Trip+Movements is a convenient way to sync back updated Trips and Movements from CCP to your local data-store. If you want to use the information to retrieve other related information like historical data it’s recommended that you only request the properties you need. You can use the OData $select query option for this.
Writing Trips and Movements
The recommended way for writing Routes to CCP is to first write the Trip, retrieve the TripId from the result and immediately after that write the Movements linked to that TripId. To minimize bandwidth it’s recommended to reuse the Trip when the route changes and just modify, add or remove the movements. To prevent concurrency problems it’s recommended to write Trips and Movements in a single OData Batch request.
Writing movements inline
There is an alternative way to write routes and that is to write the Trip including Movements using it’s inline representation. When using the inline method the related entities are created and automatically linked to the referencing entity. The downside to this method is that the response only returns the main entity (Trip) and not the individual movements. Using this method you can’t directly retrieve the MovementId properties of the newly created Movements. The other downside is that this method only works for entity creation and not for updating. It is possible to retrieve the MovementId properties afterwards. When writing the movements you can include a property called “TransporterMovementId” and fill this property with your own movement identification. The POST returns the newly created TripId, in this example 123456. Using this TripId perform a GET request with a few extra OData query options.
/Movements?$filter=TripId eq 123456&$select=MovementId,TransporterMovementId
This returns a collection of Movements but only the properties MovementId and TransporterMovementId. This way you can link the two together and updating individual movements becomes possible.
There is another way to use the inline method in combination with updates and that is to delete the Trip when something changes to the route and recreate it using the inline method. This is technically possible but not recommended. The reason for this is that it creates a lot of unnecessary requests and uses a lot of bandwidth. But the main reason is when deleting an entity it’s not physically deleted but just marked as deleted. This way other clients have the ability to perform a sync operation that includes deleted entities (using the ShowDeleted header) and process them. Using this method results in a lot of deleted entities. Eventually they will be permanently removed but this takes a few months. Try to use the other method. It’s faster, cleaner and much more efficient.
Sequential creation of Trip and Movements
This is the recommended way of creating a Trip with Movements. First create the Trip with at least the following properties
- RouteNumber; Property used a lot for easy referral to a route. Visible to users on Cabman Online.
- TravelerCount; The number of distinct travelers (including absent Travelers) in the route.
- WheelchairCount; The number of travelers that are in a rigid wheelchair.
- AttendantCount; The number of attendants
- StatusName; Indicate what the status of the trip is. It’s recommended to update the status whenever it changes in your software. Start with the initial state Planned.
- PlannedDistance; The total route distance as planned by the transporter. When unknown fill it with 0. It is recommended to fill this field with the correct distance so the contractor can compare planned vs executed.
- DatacommunicationTripId; This is an important property for linking planned Trips to historical data. By filling this field with the CombinationNumber or Ordernumber used to assign the trip to a Cabman CS or Cabman BCT, we have the ability to retrieve the historical data of the execution of the Trip by the driver. If the order was sent as a combination, fill this with the combinationnumber. Use the ordernumber in a related field in the Movement. If not then use ordernumber. By not filling this field you indicate that we are not allowed to retrieve the data. There is no other way for us to identify the historical data. All data in our Fleet-Management store is anonymous. It can only be retrieved by a combination of a random customercode, a unitname that cannot be linked to a license plate and this DatacommunicationTripId.
- ContractorId; A very important field if you want to make the trip visible to contractors. When not filled it is only visible to the transporter that created it. For this tutorial the value of ContractorId should be set to 1 which is the unique identification of our example contractor in CCP.
- DriverId; Fill this with the unique identification of the driver created in CCP. For this tutorial the property should be set to 7782.
- VehicleId; Fill this with the unique identification of the vehicle created in CCP. For this tutorial the property should be set to 6549.
To write the Trip to CCP use a POST request to
/Trips
Taking the above property list into account the body of the request will be as follows.
{
"RouteNumber" : "101",
"PlannedDistance" : 0.0,
"TravelerCount" : 3,
"WheelchairCount" : 0,
"AttendantCount" : 0,
"StatusName" : "Planned",
"VehicleId" : 6549,
"DriverId" : 7782,
"ContractorId" : 1
}
The http result code is 201 (created) and the result contains the created Trip
{
"odata.metadata": "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Trips/@Element",
"TripId": 452181,
"RouteNumber": "101",
"Description": null,
"PlannedDistance": 0,
"TravelerCount": 3,
"WheelchairCount": 0,
"AttendantCount": 0,
"StatusName": "Planned",
"DayCode": null,
"Remark": null,
"CostCenterId": null,
"Reference": null,
"PaymentTypeName": null,
"VehicleKindName": null,
"DatacommunicationTripId": null,
"TransporterTripId": null,
"LastUpdated": "2014-12-10T15:46:09.322625Z",
"ShiftId": null,
"VehicleId": 6549,
"DriverId": 7782,
"ContractorId": 1,
"TransporterId": 1,
"AssignedTransporterId": null,
"BookingId": null,
"PreviousTripId": null,
"VehicleTypeId": null,
"Deleted": false
}
Using the TripId 452181 we can continue with the creation of the Movements. When not using batch processing you need to post every movement in its own request. For this tutorial we create a single Movement. We’re not using batch processing for readability.
Create the movement with at least the following properties
- TripId; Required property. Use it to link the movement to the right Trip entity. In our example the value of TripId will be 452181.
- TravelerId; This property is technically not required but one of the most important properties in the entity. Use this to link the traveler to the movement. This property is used to calculate the absence state of the traveler in the context of the movement. For this example we use TravelerId 3320
- ContractorId; Fill this property with the contractor associated with the movement. It’s possible that movements of multiple contractors are combined in a route. For the system to make movements only available to the appropriate contractor this property needs to contain the contractorid. When the contractor for the trip and movements are the same this property is optional but it’s recommended to always fill it. ContractorId for our example is 1
- StatusName; Indicate what the status of the trip is. It’s recommended to update the status whenever it changes in your software. Start with the initial state “Pending”.
- PlannedDistance; The total movement distance as planned by the transporter. When unknown fill it with 0. It is recommended to fill this field with the correct distance so the contractor can compare planned vs executed.
- DatacommunicationMovementId; When assigning a route to a Cabman CS or Cabman BCT, every movement will be a separate trip. Each of those trips has a OrderNumber. This ordernumber must be filled in this property. This way we can link this planned movement to the historical data.
- PickUpLocationId/DropOffLocationId; In a previous step of this tutorial we retrieved locations. These locations are entities which could be linked to user accounts. These accounts want to have access to movements that are related to them. This relation can be achieved by linking the locations in one of these properties. When the traveler is picked up at a location PickUpLocationId must be filled and when the Traveler is dropped off at a location DropOffLocationId must be filled. These properties are separate from the PickUp and DropOff properties. This way it’s still possible to add a different address or a more specific address and time independent of the location settings.
- PickUp/DropOff; Arguably the most important properties of a Movement. Both properties are a complex type named MovementFragment and contain a number of properties. These properties are the planned time, the sequence of pickup or dropoff relative to the route and address information.
Let’s create a movement. This is a movement for Traveler 3320 from location 653 to the address Wilhelminapark 36, TILBURG. Perform a POST request to the following EntitySet:
/Movements
The body of the request will be as follows.
{
"StatusName" : "Pending",
"PlannedDistance" : 1.5,
"TravelerId" : 3320,
"ContractorId" : 1,
"TripId" : 452181,
"PickUpLocationId" : 653,
"DropOffLocationId" : null,
"PickUp" : {
"PlannedTime" : "2014-12-11T10:00:00",
"Sequence" : 1,
"Address" : {
"Street" : "Lieve-Vrouweplein",
"Number" : 9,
"Addition" : "10",
"PostalCode" : "5038TS",
"Town" : "TILBURG"
}
},
"DropOff" : {
"PlannedTime" : "2014-12-11T10:05:13",
"Sequence" : 2,
"Address" : {
"Street" : "Wilhelminapark",
"Number" : 36,
"PostalCode" : "5041EC",
"Town" : "TILBURG"
}
}
}
The http result code of the response will be 201 (created). The body of the result contains the newly created movement.
{
"odata.metadata" : "https://www.cabmanonline.nl/CCPService/DataServiceCCP.svc/$metadata#Movements/@Element",
"MovementId" : 1151231,
"StatusName" : "Pending",
"AbsenceStateName" : null,
"CallBackService" : false,
"CallBackNumber" : null,
"Description" : null,
"PlannedDistance" : 1.5,
"TransporterMovementId" : null,
"DatacommunicationMovementId" : null,
"LastUpdated" : "2014-12-11T10:23:35.760125Z",
"Deleted" : false,
"TravelerId" : 3320,
"ContractorId" : 1,
"TripId" : 452181,
"PickUpLocationId" : 653,
"PickUpRoutePointId" : 1748887,
"DropOffLocationId" : null,
"DropOffRoutePointId" : 1748886,
"PickUp" : {
"PlannedTime" : "2014-12-11T10:00:00",
"Sequence" : 1,
"FlightNumber" : null,
"Address" : {
"Street" : "Lieve-Vrouweplein",
"Number" : 9,
"Addition" : "10",
"PostalCode" : "5038TS",
"State" : null,
"Town" : "TILBURG",
"CountryId" : null,
"Coordinates" : {
"Latitude" : null,
"Longitude" : null,
"FormatName" : null
}
}
},
"DropOff" : {
"PlannedTime" : "2014-12-11T10:05:13",
"Sequence" : 2,
"FlightNumber" : null,
"Address" : {
"Street" : "Wilhelminapark",
"Number" : 36,
"Addition" : null,
"PostalCode" : "5041EC",
"State" : null,
"Town" : "TILBURG",
"CountryId" : null,
"Coordinates" : {
"Latitude" : null,
"Longitude" : null,
"FormatName" : null
}
}
}
}
Updating Routes
After creating Trips and Movements you will have the TripId and MovementId properties linked to your internal routes. From that point it’s possible to update Trips and Movements. There are a lot of scenario’s where this is relevant. The general rule should be that every change to the route should be reflected in CCP. This includes changes to pickup and dropoff properties (e.g time or address), distance and status. When the Trip is in transit, canceled, done etc. and when a movement is active, dropped off or has a no show, this should result in an update of the Trip and/or Movement in CCP.
Updating an entity is very easy. The OData spec describes three different http verbs to perform an update. PUT is used to replace the entire entity. Every property must be in the payload or it will be reset to its default value. If you want to perform a differential update the MERGE/PATCH verb must be used. PATCH is the preferred method. Just include the properties that must be updated. Below is a http header and body to perform a differential update on our Movement. It updates the status of the movement to Active.
PATCH /CCPService/DataServiceCCP.svc/Movements(1151231) HTTP/1.1
Host: www.cabmanonline.nl
Authorization: Basic dGVzdEB0ZXN0LmNvbTp0ZXN0MTIz
Accept: application/json
Content-Type: application/json
DataServiceVersion: 3.0
MaxDataServiceVersion: 3.0
CabmanCloudPlatformVersion: 11.0
MaxCabmanCloudPlatformVersion: 11.0
{
"StatusName" : "Active"
}
The result will be a http result 204 (no content).
Move between trips
Not all properties can be updated using the PATCH method. Some properties are immutable. One of those properties is TripId. When a movement changes from one trip to another you could try to update the TripId but this will not work. The only way you can move a movement from one trip to another is to delete it from the source and create a new version in the other.
Events
The final part of our tutorial is about execution data. The routes are assigned to a vehicle and executed by the driver. When using data communication this results in status information about the execution including boarding time, position, odometer and a lot more. When using a Cabman CS or Cabman BCT in combination with the previously mentioned linking method it’s possible to retrieve most of this information automatically. But when using other competing products or the contractor demands other types of information not available from our FM data-store, this information must come from the transporter.
CCP has a few entities that can be leveraged for this. These are the Events entities. There are three distinct derived types for different use-cases:
- TripEvent; Store events related to Trips
- MovementEvent; Store events related to Movements
- ShiftEvent; Store events related to Shifts
These event entities are all derived of the BaseEvent entity and are mostly the same except an extra property to link the event to the appropriate entity.
Most properties are self-explanatory except EventTypeId. This properties links to a EntitySet named EntityTypes. This is a collection of EntityType objects and contains all possible entitytypes. To request the available EntityTypes in the system execute a GET request to:
/EventTypes
This returns the available entitytypes. You can safely use the values in EntityTypes as constants. They will not change, only expanded when required.
The event entities work the same way as every other entity in CCP. This means that you need to store the created EventId and use this EventId to update or remove the event. As these types of events do not change and are probably created once and never touched again this is not mandatory but recommended to prevent duplicates.
Derived entitysets work a bit different than normal entitysets. There is one entityset named Events which return all events. To limit the results to a specific type add the full qualified name of the derived entity to the URI.
/Events/CCPEntities.MovementEvent
For this tutorial we create a start event for our Movement. This includes the starting time, coordinates and odometer. Perform a POST request with the following body:
{
"MovementId" : 1151231,
"EventTypeId" : 26,
"EventDateTime" : "2014-12-11T09:51:43",
"OdoMeter" : 97.5,
"Coordinates" : {
"Latitude" : 51.555450,
"Longitude" : 5.10590,
"FormatName" : "GeoDecimal"
}
}