当前位置: 首页 > news >正文

使用高德API和MapboxGL实现路径规划并语音播报

概述

本文使用高德API实现位置查询和路径规划,使用MapboxGL完成地图交互与界面展示,并使用Web Speech API实现行驶中路线的实时语音播报。

效果

image.png

image.png

Web Speech API简介

Web Speech API 使你能够将语音数据合并到 Web 应用程序中。Web Speech API 有两个部分:SpeechSynthesis 语音合成(文本到语音 TTS)和 SpeechRecognition 语音识别(异步语音识别)。

  • 语音识别通过 SpeechRecognition接口进行访问,它提供了识别从音频输入(通常是设备默认的语音识别服务)中识别语音情景的能力。一般来说,你将使用该接口的构造函数来构造一个新的 SpeechRecognition对象,该对象包含了一系列有效的对象处理函数来检测识别设备麦克风中的语音输入。SpeechGrammar 接口则表示了你应用中想要识别的特定文法。文法则通过 JSpeech Grammar Format (JSGF.) 来定义。

  • 语音合成通过SpeechSynthesis接口进行访问,它提供了文字到语音(TTS)的能力,这使得程序能够读出它们的文字内容(通常使用设备默认的语音合成器)。不同的声音类类型通过SpeechSynthesisVoice对象进行表示,不同部分的文字则由 SpeechSynthesisUtterance 对象来表示。你可以将它们传递给 SpeechSynthesis.speak()方法来产生语音。

SpeechSynthesisUtterance是HTML5中新增的API,用于将指定文字合成为对应的语音。它包含一些配置项,可以指定如何去阅读(如语言、音量、音调等)。

简单使用示例如下代码:

// 创建 SpeechSynthesisUtterance 对象
var utterance = new SpeechSynthesisUtterance();
// 可选:设置语言(例如,中文)
utterance.lang = "zh-CN";
// 可选:设置语音
utterance.voice = window.speechSynthesis.getVoices()[0];
// 可选:设置音量(0.0到1.0之间)
utterance.volume = 1.0;
// 可选:设置语速(正常速度为1.0)
utterance.rate = 1.0;
// 可选:设置语调(正常语调为1.0)
utterance.pitch = 1.0;
// 设置要朗读的文本
utterance.text = '设置要朗读的文本';
window.speechSynthesis.speak(utterance);

实现

实现思路

  1. 地图初始化的时候通过H5的geolocation接口获取当前位置;
  2. 调用rgeo接口,根据获取到的位置获取位置所在市;
  3. 调用inputtips接口完成关键词联想查询;
  4. 调用/direction/driving接口完成路径的规划;
  5. 用MapboxGL实现地图交互与路径展示;
  6. 根据当前位置判断是否进入对应的步骤,提示对应的语音。

实现代码

示例使用Vue作为演示,界面与地图初始化代码如下:

<template><div class="container"><div class="query"><el-form :model="form" label-width="auto" class="query-form"><el-form-item label="起点"><el-autocompletev-model="form.startPosition":fetch-suggestions="querySearchStart"clearableplaceholder="请输入起点位置"@select="handleSelectStart"><template #default="{ item }"><div class="autocomplete-value">{{ item.value }}</div><div class="autocomplete-address">{{ item.address }}</div></template></el-autocomplete></el-form-item><el-form-item label="终点" style="margin-bottom: 0"><el-autocompletev-model="form.endPosition":fetch-suggestions="querySearchEnd"clearableplaceholder="请输入终点位置"@select="handleSelectEnd"><template #default="{ item }"><div class="autocomplete-value">{{ item.value }}</div><div class="autocomplete-address">{{ item.address }}</div></template></el-autocomplete></el-form-item></el-form><div class="query-button"><el-button:disabled="!form.startPosition || !form.endPosition"class="query-button-inner"@click="queryRoute">查询</el-button></div></div><div class="map" id="map"><div class="map-button"><el-buttonv-show="path"class="map-button-inner"type="primary"@click="toggleAnimate">{{ isPlay ? "结束导航" : "开始导航" }}</el-button></div></div></div>
</template><script>
const AK = "你申请的key"; // 高德地图key
const BASE_URL = "https://restapi.amap.com/v3";
let map = null,animation = null;import { ElMessage, ElMessageBox } from "element-plus";
import AnimationRoute from "./utils/route";//封装请求
function request(url, params) {let fullUrl = `${BASE_URL}${url}?key=${AK}`;for (const key in params) {fullUrl += `&${key}=${params[key]}`;}return new Promise((resolve, reject) => {fetch(fullUrl).then((res) => res.json()).then((res) => {if (res.status === "1") resolve(res);else {ElMessage.error("接口请求失败");reject(res);}});});
}export default {data() {return {center: [113.94150905808976, 22.523881824251347],form: {startPosition: "",endPosition: "",startCoord: [],endCoord: [],},cityInfo: {},path: null,isPlay: false,};},mounted() {const that = this;function successFunc(position) {const { longitude, latitude } = position.coords;that.center = [longitude, latitude];that.initMap(true);}function errFunc() {that.initMap(false);}if (navigator.geolocation) {try {errFunc();navigator.geolocation.getCurrentPosition(successFunc, errFunc);} catch (e) {errFunc();}} else {errFunc();}},methods: {toggleAnimate() {if (!animation) return;if (this.isPlay) {animation.pause();ElMessageBox.confirm("确认取消导航吗?", "提示", {confirmButtonText: "确认",cancelButtonText: "取消",type: "warning",}).then(() => {animation.destory();animation = null;this.path = null;this.form = {startPosition: "",endPosition: "",startCoord: [],endCoord: [],};this.isPlay = false;}).catch(() => {animation.play();this.isPlay = true;});} else {animation.play();this.isPlay = true;}},initMap(isLocate) {map = new SFMap.Map({container: "map",center: this.center,zoom: 17.1,});map.on("load", (e) => {new SFMap.SkyLayer({map: map,type: "atmosphere",// 设置天空光源的强度atmosphereIntensity: 12,// 设置太阳散射到大气中的颜色atmosphereColor: "rgba(87, 141, 219, 0.8)",// 设置太阳光晕颜色atmosphereHaloColor: "rgba(202, 233, 250, 0.1)",});request("/geocode/regeo", {location: this.center.join(","),}).then((res) => {this.cityInfo = res.regeocode.addressComponent;});});},querySearchStart(str, cb) {if (str) {request("/assistant/inputtips", {keywords: str,city: this.cityInfo?.city,}).then((res) => {cb(res.tips.map((t) => {t.value = t.name;return t;}));});} else {cb([]);}},querySearchEnd(str, cb) {if (str) {request("/assistant/inputtips", {keywords: str,city: this.cityInfo?.city,}).then((res) => {cb(res.tips.map((t) => {t.value = t.name;return t;}));});} else {cb([]);}},handleSelectStart(item) {this.form.startCoord = item.location.split(",").map(Number);},handleSelectEnd(item) {this.form.endCoord = item.location.split(",").map(Number);},queryRoute() {const { startCoord, endCoord } = this.form;request("/direction/driving", {origin: startCoord.join(","),destination: endCoord.join(","),extensions: "all",}).then((res) => {const path = res.route.paths[0];let coordinates = [];this.path = path;path.steps.forEach((step) => {const polyline = step.polyline.split(";").map((c) => c.split(",").map(Number));coordinates = [...coordinates, ...polyline];});const route = {type: "Feature",properties: { path },geometry: {type: "LineString",coordinates: coordinates,},};animation = new AnimationRoute(map, route, false);});},},
};
</script><style scoped lang="scss">
.container {width: 100%;height: 100vh;overflow: hidden;
}
.query {padding: 0.8rem;overflow: hidden;background: #ccc;.query-form {width: calc(100% - 4rem);float: left;}.query-button {width: 2rem;height: 4.7rem;float: right;display: flex;justify-content: flex-end;align-items: center;.query-button-inner {width: 3.8rem;height: 100%;}}
}
.map {height: calc(100% - 6.4rem);
}.autocomplete-value,
.autocomplete-address {white-space: nowrap;width: 100%;overflow: hidden;text-overflow: ellipsis;
}
.autocomplete-address {font-size: 0.8rem;color: #999;margin-top: -0.8rem;border-bottom: 1px solid #efefef;
}
.map-button {position: absolute;bottom: 0;left: 0;width: 100%;z-index: 99;padding: 0.8rem;box-sizing: border-box;&-inner {width: 100%;}
}
:deep .el-form-item {margin-bottom: 0.5rem;
}
</style>

示例中使用轨迹播放的方式演示了位置的变化,前文mapboxGL轨迹展示与播放已经有过分享,示例在前文的基础上做了一点改动,改动完代码如下:

const icon = "/imgs/car.png";
const arrow = "/imgs/arrow.png";import * as turf from "@turf/turf";class AnimationRoute {constructor(map, route, play = true, fit = true, speed = 60) {this._map = map;this._json = route;this._play = play;this._speed = speed;this._path = route.properties.path;this.init();if (fit) this._map.fitBounds(turf.bbox(route), { padding: 50 });}init() {const that = this;// 创建 SpeechSynthesisUtterance 对象var utterance = new SpeechSynthesisUtterance();// 可选:设置语言(例如,中文)utterance.lang = "zh-CN";// 可选:设置语音utterance.voice = window.speechSynthesis.getVoices()[0];// 可选:设置音量(0.0到1.0之间)utterance.volume = 1.0;// 可选:设置语速(正常速度为1.0)utterance.rate = 1.0;// 可选:设置语调(正常语调为1.0)utterance.pitch = 1.0;that.utterance = utterance;that._index = 0;const length = turf.length(that._json);const scale = 60;that._count = Math.round((length / that._speed) * 60 * 60) * scale;that._step = length / that._count;that._stepPlay = -1;that._flag = 0;that._playId = "play-" + Date.now();// 添加路径图层that._map.addSource(that._playId, {type: "geojson",data: that._json,});that._map.addLayer({id: that._playId,type: "line",source: that._playId,layout: {"line-cap": "round","line-join": "round",},paint: {"line-color": "#aaaaaa","line-width": 10,},});// 添加已播放路径that._map.addSource(that._playId + "-played", {type: "geojson",data: that._json,});that._map.addLayer({id: that._playId + "-played",type: "line",source: that._playId + "-played",layout: {"line-cap": "round","line-join": "round",},paint: {"line-color": "#09801a","line-width": 10,},});// 添加路径上的箭头that._map.loadImage(arrow, function (error, image) {if (error) throw error;that._map.addImage(that._playId + "-arrow", image);that._map.addLayer({id: that._playId + "-arrow",source: that._playId,type: "symbol",layout: {"symbol-placement": "line","symbol-spacing": 50,"icon-image": that._playId + "-arrow","icon-size": 0.6,"icon-allow-overlap": true,},});});// 添加动态图标that._map.loadImage(icon, function (error, image) {if (error) throw error;that._map.addImage(that._playId + "-icon", image);that._map.addSource(that._playId + "-point", {type: "geojson",data: that._getDataByCoords(),});that._map.addLayer({id: that._playId + "-point",source: that._playId + "-point",type: "symbol",layout: {"icon-image": that._playId + "-icon","icon-size": 0.75,"icon-allow-overlap": true,"icon-rotation-alignment": "map","icon-pitch-alignment": "map","icon-rotate": 50,},});that._animatePath();});}pause() {this._play = false;window.cancelAnimationFrame(this._flag);}start() {this._index = 0;this.play();}play() {this._play = true;this._animatePath();}_animatePath() {if (this._index > this._count) {window.cancelAnimationFrame(this._flag);} else {const coords = turf.along(this._json, this._step * this._index).geometry.coordinates;// 已播放的线const start = turf.along(this._json, 0).geometry.coordinates;this._map.getSource(this._playId + "-played").setData(turf.lineSlice(start, coords, this._json));// 车的图标位置this._map.getSource(this._playId + "-point").setData(this._getDataByCoords(coords));// 计算旋转角度const nextIndex =this._index === this._count ? this._count - 1 : this._index + 1;const coordsNext = turf.along(this._json, this._step * nextIndex).geometry.coordinates;let angle = turf.bearing(turf.point(coords), turf.point(coordsNext)) - 90;if (this._index === this._count) angle += 180;this._map.setLayoutProperty(this._playId + "-point","icon-rotate",angle);const camera = this._map.getFreeCameraOptions();camera.position = mapboxgl.MercatorCoordinate.fromLngLat(coords, 100);camera.lookAtPoint(coordsNext);this._map.setFreeCameraOptions(camera);this._map.setPitch(80);this._index++;if (this._play) {this._playInstruction(coords);this._flag = requestAnimationFrame(() => {this._animatePath();});}}}_playInstruction(coords) {const { steps } = this._path;const stepPlay = this._stepPlay;const start = this._stepPlay !== -1 ? this._stepPlay : 0for (let i = start; i < steps.length; i++) {const step = steps[i];const polyline = step.polyline.split(";").map((v) => v.split(",").map(Number));const pt = turf.point(coords);const line = turf.lineString(polyline);const dis = turf.pointToLineDistance(pt, line) * 1000;if (i > this._stepPlay && dis < 10) {this._stepPlay = i;break;}}if (stepPlay !== this._stepPlay) {this.utterance.text = steps[this._stepPlay].instruction;window.speechSynthesis.speak(this.utterance);}}_getDataByCoords(coords) {if (!coords || coords.length !== 2) return null;return turf.point(coords, {label: this._formatDistance(this._step * this._index),});}_formatDistance(dis) {if (dis < 1) {dis = dis * 1000;return dis.toFixed(0) + "米";} else {return dis.toFixed(2) + "千米";}}destory() {window.cancelAnimationFrame(this._flag);if (this._map.getSource(this._playId + "-point")) {this._map.removeLayer(this._playId + "-point");this._map.removeSource(this._playId + "-point");}if (this._map.getSource(this._playId + "-played")) {this._map.removeLayer(this._playId + "-played");this._map.removeSource(this._playId + "-played");}if (this._map.getSource(this._playId)) {this._map.removeLayer(this._playId);this._map.removeLayer(this._playId + "-arrow");this._map.removeSource(this._playId);}}
}export default AnimationRoute;

http://www.mrgr.cn/news/58472.html

相关文章:

  • 数据结构的删除操作
  • Page Cache(页缓存
  • 【话题】创智时代:人工智能重塑生活与工作
  • UML外卖系统报告(包含具体需求分析)
  • C++学习路线(二十二)
  • 对话系统介绍
  • Robot Framework实战
  • 使用 Kafka 和 MinIO 实现人工智能数据工作流
  • 反悔贪心学习笔记[浅谈]
  • Java多Module项目打包
  • 第一单元历年真题整理
  • Linux中查询Redis中的key和value(没有可视化工具)
  • C++常用函数定义解释
  • HBuilder X 中Vue.js基础使用->计算属性的应用(三)
  • 大数据环境下的数据清洗技术研究
  • 广告变现:2024年全球四大热门聚合广告平台
  • 什么是高存储服务器,有哪些优势,如何选择?
  • 数据挖掘:基于电力知识图谱的客户画像构建实施方案
  • 助力FP商家躲过审核机制,规避封号风险
  • 光影交织,文旅融合:开启城市新风尚
  • csdn要打开或者无法刷新内容管理,文章无法发布或者未保存成功(服务器超时)-->先保存在自己的电脑里
  • Android Navigation传递复杂参数(自定义)
  • 台达A2伺服
  • 提升海外直播画质的关键因素与解决方案
  • 国产标准数字隔离器的未来---克里雅半导体
  • vue 表单页面validate验证重置