TomTom Showcase: 卡车司机的路线规划 





0/5 (0投票)
为了展示 TomTom API 的强大功能,本文将介绍如何使用 TomTom Android SDK 构建一个面向卡车司机的应用程序。

当美国空军开发全球定位系统 (GPS) 时,其目标是使 GPS 接收器用户能够随时随地在地球上或其附近接收地理定位和时间信息。GPS 在军事背景下的作用显而易见。一旦 GPS 技术对公众开放,TomTom 等公司便开始利用它来做许多其他有用的事情,包括开发复杂的消费者应用程序。而且,由于 TomTom 最近决定向开发者发布 API,将 GPS 技术集成到复杂的消费者应用程序中比以往任何时候都更加容易。
TomTom 的 API 为开发者提供了对强大地图和地理定位解决方案的简单访问。这些解决方案是 TomTom 多年工程创新的成果,以及数十年来关于交通模式和趋势的信息。
为了展示 TomTom API 的强大功能,本文将介绍如何使用 TomTom Android SDK 构建一个面向卡车司机的应用程序。具体来说,我们将使用 SDK 来确定最佳路线,并告知司机何时需要出发才能按时到达。
(请注意,本文重点介绍 Android SDK,但也有适用于 Web 和 iOS 的 SDK。另外请注意,如果您愿意,也可以直接从应用程序连接到 API,而不是使用 SDK。)
入门:TomTom SDK
让我们开始研究如何创建 TomTom 开发者帐户并获取 SDK 访问权限。
您可以在 TomTom 开发者门户找到开始使用 TomTom API 所需的一切。您首先需要做的是在门户网站上创建一个帐户。从主页开始,输入您的电子邮件地址,然后单击“获取免费 API 密钥”按钮。

过程中的下一步是选择用户名并仔细阅读条款和条件。您的免费帐户每天支持最多 2500 次免费交易。需要注意的是,如果每天 2500 次 API 交易不够用,可以通过访问开发者仪表板中的“我的积分”屏幕来购买更多交易。
一封包含密码设置链接的电子邮件将发送到您的帐户,然后您就可以开始使用了。在请求 API 密钥之前,您需要配置一个应用程序。在您的仪表板中,单击“+ 添加新应用”按钮。

应用程序需要一个名称,并且您需要启用该应用程序需要访问的 API。在此示例中,我们将使用的产品是搜索 API、路线 API 和地图 API。如果您正在跟随操作,请选择搜索 API、路线 API 和地图 API 产品,然后单击创建应用。

应用程序会很快获得批准并在您的仪表板上显示。条目显示了消费者 API 密钥以及有关您的应用程序的信息,包括应用程序的批准状态以及它使用的每个产品。

我们在配置应用程序中的 TomTom 依赖项时将需要消费者 API 密钥。
设置我们的项目
我使用 Android Studio 构建了此应用程序,并从一个空白活动屏幕开始。我们的目标是构建一个简单的应用程序来演示以下概念:
- 具有“模糊”搜索的地址自动补全(即,搜索结果不限于搜索查询中使用的特定关键字或短语,还包括相关术语)
- 确定设备当前位置
- 查找最佳路线
我将在末尾提供链接,其中包含有关每个组件的更多信息以及一个更复杂的示例,您可以下载该示例并更全面地探索此功能。
首先,我们将创建输入字段供司机输入目的地和到达时间。一旦我们有了这些信息,我们将查找目的地的坐标,并确定最佳路线。我们将显示司机需要出发的时间,并显示路线。

屏幕底部的 <fragment> 的 android:name 为 com.tomtom.online.sdk.map.MapFragment。
权限、访问和构建配置
在我们开始编写解决方案代码之前,需要完成一些管理任务。这包括将 TomTom SDK 添加为依赖项以及设置设备权限。我们将首先将 TomTom 存储库添加到项目的 build.gradle 文件中。找到文件中的 allprojects 对象,并将 TomTom 存储库添加到该对象。
allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url "https://maven.tomtom.com:8443/nexus/content/repositories/releases/"
        }
    }
}
现在我们可以访问存储库了,打开应用程序的 build.gradle 文件,并将以下依赖项添加到 dependencies 对象。同时,您还需要确保 compileOptions 将源和目标兼容性设置为至少 1.8。
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.tomtom.online:sdk-maps:2.+'
    implementation 'com.tomtom.online:sdk-search:2.+'
    implementation 'com.tomtom.online:sdk-routing:2.+'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
最后,您需要打开 AndroidManifest.xml 文件并进行以下添加。首先,添加用户权限,允许应用访问设备位置服务。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
其次,将以下 meta-data 对象添加到应用程序对象。请务必将 ReplaceWithAPIKey 替换为您的 API 密钥。
<meta-data
    android:name="OnlineSearch.Key"
    android:value="ReplaceWithAPIKey" />
<meta-data
    android:name="OnlineMaps.Key"
    android:value="ReplaceWithAPIKey" />
<meta-data
    android:name="OnlineRouting.Key"
    android:value="ReplaceWithAPIKey" />
现在我们已经处理了 housekeeping,是时候构建解决方案了。
使用模糊搜索自动补全地址
Delivery Address 的控件是一个 AutoCompleteTextView 对象。我们可以向此控件添加一个监听器,该监听器可以响应文本的变化,并使用 SearchAPI 建议可能的地址。
让我们开始初始化一些我们将需要使用的辅助对象和几个常量。我们还将初始化本示例中将使用的 TomTom API,并创建一个用于它们的初始化函数。
private AutoCompleteTextView txtAddress;
private List<String> addressAutocompleteList;
private Map<String, LatLng> searchResultsMap;
private ArrayAdapter<String> searchAdapter;
private Handler searchTimerHandler = new Handler();
private Runnable searchRunnable;
private SearchApi searchApi;
private RoutingApi routingApi;
private TomtomMap tomtomMap;
private static final int MIN_LEVEL_OF_FUZZINESS = 2;
private static final int MIN_AUTOCOMPLETE_CHARACTERS = 3;
private static final int AUTOCOMPLETE_SEARCH_DELAY_MILLIS = 600;
private void initTomTomAPIs() {
    searchApi = OnlineSearchApi.create(getApplicationContext());
    routingApi = OnlineRoutingApi.create(getApplicationContext());
    MapFragment mapFragment = (MapFragment) getSupportFragmentManager().findFragmentById(R.id.mapFragment);
    mapFragment.getAsyncMap(this);
}
让我们更详细地研究一下常量。这些值中的每一个都可以进行调整,以微调自动补全功能的工作方式。
- MIN_LEVEL_OF_FUZZINESS- 模糊搜索会查找与我们输入的文本完全匹配或相似的匹配项。不同程度的“模糊性”会影响您获得的结果类型。
- MIN_COMPLETE_CHARACTERS- 在输入至少 3 个字符之前,我们不会开始搜索匹配项。
- AUTOCOMPLETE_SEARCH_DELAY_MILLIS- 用户停止输入后,我们将等待 600 毫秒再返回结果。此延迟限制了对 API 的调用次数,并提供了更好的用户体验。
我们将分两步实现自动补全功能。第一步是在字段上配置自动补全功能,第二步是创建搜索函数。自动补全功能是样板自动补全代码,它调用 addressAutoComplete,我们将在下一步进行探讨。
public void configureAutocomplete(final AutoCompleteTextView autoCompleteTextView) {
    addressAutocompleteList = new ArrayList<>();
    searchResultsMap = new HashMap<>();
    searchAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, addressAutocompleteList);
    autoCompleteTextView.setAdapter(searchAdapter);
    autoCompleteTextView.addTextChangedListener(new BaseTextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (searchTimerHandler != null) {
                searchTimerHandler.removeCallbacks(searchRunnable);
            }
        }
        @Override
        public void afterTextChanged(final Editable s) {
            if (s.length() > 0) {
                if (s.length() >= MIN_AUTOCOMPLETE_CHARACTERS) {
                    searchRunnable = () -> addressAutoComplete(s.toString());
                    searchAdapter.clear();
                    searchTimerHandler.postDelayed(searchRunnable, AUTOCOMPLETE_SEARCH_DELAY_MILLIS);
                }
            }
        }
    });
    autoCompleteTextView.setOnItemClickListener((parent, view, position, id) -> {
        String item = (String) parent.getItemAtPosition(position);
        if (autoCompleteTextView == txtAddress) {
            destination = searchResultsMap.get(item);
        } else if (autoCompleteTextView == txtAddress) {
            destination = searchResultsMap.get(item);
        }
        hideKeyboard(view);
    });
}
private void hideKeyboard(View view) {
    InputMethodManager in = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
    if (in != null) {
        in.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
    }
}
private abstract class BaseTextWatcher implements TextWatcher {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
}
您会看到,我们为自动补全功能添加了一个 ItemClickListener。当用户单击他们想要的地址时,API 的响应还包括 GPS 坐标。我们将它们保存在 destination 参数中以备后用。
addressAutoComplete 函数接受地址文本和一个 AutoCompleteTextView 控件,并使用 TomTom SearchAPI 对象执行模糊搜索。我们将首先使用 ApplicationContext 创建对象,该对象将我们的 API 密钥从 manifest 文件传递。然后,我们将构建一个 SearchQuery,订阅响应,当响应返回时,我们将更新文本控件上的自动补全列表。
public void addressAutoComplete(final String address, final AutoCompleteTextView autoCompleteTextView) {
    searchApi.search(new FuzzySearchQueryBuilder(address)
        .withLanguage(Locale.getDefault().toLanguageTag())
        .withTypeAhead(true)
        .withMinFuzzyLevel(MIN_LEVEL_OF_FUZZINESS).build()
    )
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new DisposableSingleObserver<FuzzySearchResponse>() {
        @Override
        public void onSuccess(FuzzySearchResponse fuzzySearchResponse) {
            if (!fuzzySearchResponse.getResults().isEmpty()) {
                addressAutocompleteList.clear();
                searchResultsMap.clear();
                for (FuzzySearchResult result : fuzzySearchResponse.getResults()) {
                    String addressString = result.getAddress().getFreeformAddress();
                    addressAutocompleteList.add(addressString);
                    searchResultsMap.put(addressString, result.getPosition());
                }
                searchAdapter.clear();
                searchAdapter.addAll(addressAutocompleteList);
                searchAdapter.getFilter().filter("");
            }
        }
        @Override
        public void onError(Throwable e) {
            Toast.makeText(MainActivity.this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
        }
    });
}
最后一步是将自动补全功能添加到文本字段。在 onCreate 函数中,添加以下行。
initTomTomAPIs(); initCurrentLocation(); txtAddress = findViewById(R.id.txtAddress); configureAutocomplete(txtAddress); txtAddress.setAdapter(searchAdapter)

我们将在下一步创建 initCurrentLocation 函数。
确定当前位置
既然我们知道要去哪里,我们就需要知道我们的出发位置。我们将使用设备的当前位置,借此机会了解 TomTom LocationSource 对象。第一步是在 Java 类中实现 LocationUpdateListener 接口。然后,我们将创建一个 LocationSource 对象、一个 currentLocation 对象来存储当前位置,以及一个常量来保存位置服务权限的位置。
public class MainActivity extends AppCompatActivity implements LocationUpdateListener {
    private LocationSource locationSource;
    private LatLng currentLocation;
    private static final int PERMISSION_REQUEST_LOCATION = 0;
LocationUpdateListener 接口要求我们实现 onLocationChanged 函数。我们还将创建一个函数来初始化 LocationSource 并处理权限检查。
private void initCurrentLocation() {
    PermissionChecker permissionChecker = AndroidPermissionChecker.createLocationChecker(this);
    if(permissionChecker.ifNotAllPermissionGranted()) {
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_LOCATION);
    }
    LocationSourceFactory locationSourceFactory = new LocationSourceFactory();
    locationSource = locationSourceFactory.createDefaultLocationSource(this, this,  LocationRequest.create()
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
            .setFastestInterval(2000)
            .setInterval(5000));
    locationSource.activate();
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case PERMISSION_REQUEST_LOCATION:
            if(grantResults.length >= 2 &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED &&
                    grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                locationSource.activate();
            }
            else {
                Toast.makeText(this, "Location permissions not granted.", Toast.LENGTH_SHORT).show();
            }
            break;
    }
}
@Override
public void onLocationChanged(final Location location) {
    currentLocation = new LatLng(location);
}
LocationSource 对象使我们能够访问 Android 设备上的位置服务。此服务返回 GPS 坐标,并可以配置为在位置发生变化时更新应用。如果我们的应用过于频繁地更新,我们可以增加更新之间的间隔,甚至停用 LocationSource 对象。LocationSource 对象的初始化取决于用户是否授予应用使用设备上位置服务的权限。
激活 LocationSource 对象后,currentLocation 将更新为当前位置。我们可以使用此位置以及目的地地址的位置来确定路线。
查找路线
现在我们有了当前位置、目的地位置和送达时间,我们拥有了向 TomTom 请求创建路线所需的一切。我们将从 onCreate 函数开始,然后从那里构建所需的函数。
txtDepartureTime = findViewById(R.id.txtTime);
btnScheduleDelivery = findViewById(R.id.btnScheduleDelivery);
btnScheduleDelivery.setOnClickListener(v -> {
    String time[] =  txtDepartureTime.getText().toString().split(":");
    LocalDateTime deliveryTime = LocalDate.now().atTime(new Integer(time[0]), new Integer(time[1]));
    requestRoute(currentLocation, destination, TravelMode.TRUCK, Date.from(deliveryTime.atZone(ZoneId.systemDefault()).toInstant()));
    hideKeyboard(v);
    tomtomMap.zoomToAllMarkers();
});
我们首先获取送达时间文本字段和“安排送达”按钮的引用。我们在按钮上添加一个点击监听器,该监听器获取送达时间字段中的文本并进行拆分。理想情况下,我们会验证这些值,但为了简洁起见,我们假设输入格式为小时:分钟。
我们将 current location、destination、travel mode(我们将其硬编码为卡车)和 delivery time 作为参数调用 requestRoute 函数。送达时间允许 TomTom 根据一天中的时间来补偿典型的交通状况。
private void requestRoute(final LatLng departure, final LatLng destination, TravelMode byWhat, Date arriveAt) {
    RouteQuery routeQuery = new RouteQueryBuilder(departure, destination)
            .withRouteType(RouteType.FASTEST)
            .withConsiderTraffic(true)
            .withTravelMode(byWhat)
            .withArriveAt(arriveAt)
            .build();
    routingApi.planRoute(routeQuery)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new DisposableSingleObserver<RouteResponse>() {
                @Override
                public void onSuccess(RouteResponse routeResponse) {
                    if (routeResponse.hasResults()) {
                        FullRoute fullRoute = routeResponse.getRoutes().get(0);
                        int currentTravelTime = fullRoute.getSummary().getTravelTimeInSeconds();
                        LocalDateTime departureTime = arriveAt.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().minusSeconds(currentTravelTime);
                        Toast.makeText(getApplicationContext(), "Depart at " + departureTime.format(timeFormatter), Toast.LENGTH_LONG).show();
                        displayRouteOnMap(fullRoute.getCoordinates());
                    }
                }
                @Override
                public void onError(Throwable e) {
                    Toast.makeText(getApplicationContext(), "Error finding the route.", Toast.LENGTH_LONG).show();
                }
                private void displayRouteOnMap(List<LatLng> coordinates) {
                    RouteBuilder routeBuilder = new RouteBuilder(coordinates)
                            .isActive(true);
                    tomtomMap.clear();
                    tomtomMap.addRoute(routeBuilder);
                    tomtomMap.displayRoutesOverview();
                }
            });
}
RouteAPI 允许您设置不同的参数。对于我们的查询,我们正在寻找最快的路线,并且我们希望考虑一天中的交通状况。其他可用选项包括选择最经济的路线、最短路线,您甚至可以提交燃油消耗信息并获得所需的燃油量估算。
路线生成后,我们将其显示在地图上,然后计算所需的出发时间,并将其作为应用内的弹出窗口显示给用户。

学习更多和超越此示例
此应用程序提供了对 TomTom Search API、Routing API 和 Maps API 以及 Android SDK 的基本介绍。如果您想查看完整的源代码,可以从 GitHub 下载。
TomTom 提供了一个更广泛的示例,您可以 在此处了解更多信息。
有关此示例中使用的 API 的更多信息,包括其他选项和功能,您可以通过以下链接查阅文档:

