21、构建交互式谷歌地图实时行程应用

构建交互式谷歌地图实时行程应用

在这个项目中,我们将构建一个交互式谷歌地图应用,该应用会根据朋友在南美洲的旅行信息进行动画展示。以下是详细的实现步骤和原理分析。

1. 准备工作

虽然本项目的很多元素基于之前的工作,但我们将从头开始,不过不会过多关注已经学过的内容。当用户在世界地图上“旅行”时,如果数据源中有消息,地图会淡出并显示消息,之后用户可继续旅行。

2. 创建文件

我们需要创建两个文件:一个 HTML 文件和一个 JavaScript 文件。

2.1 创建 HTML 文件

<!DOCTYPE html>
<html>
  <head>
    <title>Google Maps Markers and Events</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <link href='http://fonts.googleapis.com/css?family=Yellowtail' rel='stylesheet' type='text/css'>
    <style>
      html { height: 100% }
      body { height: 100%; margin: 0; padding: 0 }
      #map { height: 100%; width:100%; position:absolute; top:0px; left:0px }

      .overlay {
        background: #000000 scroll;
        height: 100%;
        left: 0;
        opacity: 0;
        position: absolute;
        top: 0;
        width: 100%;
        z-index: 50;
    }
    .overlayBox {
        left: -9999em;
        opacity: 0;
        position: absolute;
        z-index: 51;
        text-align:center;
        font-size:32px;
        color:#ffffff;
        font-family: 'Yellowtail', cursive;
    }
    </style>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script src="http://maps.googleapis.com/maps/api/js?key=AIzaSyBp8gVrtxUC2Ynjwqox7I0dxrqjtCYim-8&sensor=false"></script>
    <script src="https://www.google.com/jsapi"></script>

    <script src="./10.05.travel.js"></script>
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>

2.2 创建 JavaScript 文件(10.05.travel.js)

以下是 JavaScript 文件的详细步骤:
1. 初始化可视化库并存储全局地图变量:

google.load('visualization', '1.0');
google.setOnLoadCallback(init);
var map;
  1. init 函数触发时,加载地图并加载存储朋友旅行信息的 Google 电子表格:
function init() {
  var BASE_CENTER = new google.maps.LatLng(48.516817734860105,13.005318750000015 );
  map = new google.maps.Map(document.getElementById("map"),{
    center: BASE_CENTER,
    mapTypeId: google.maps.MapTypeId.SATELLITE,
    disableDefaultUI: true,
  });

  var query = new google.visualization.Query(
      'https://spreadsheets.google.com/tq?key=0Aldzs55s0XbDdERJVlYyWFJISFN3cjlqU1JnTGpOdHc');
    query.send(onTripDataReady);
}
  1. 文档加载时触发 onTripDataReady 监听器,创建 GoogleMapTraveler 对象:
function onTripDataReady(response){
  var gmt = new GoogleMapTraveler(response.g.D,map);    
}
  1. GoogleMapTraveler 对象的构造函数准备变量,创建 Animator Traveler google.maps.Polyline 对象,并调用 nextPathPoint 方法创建第一个旅行点:
function GoogleMapTraveler(aData,map){
  this.latLong; //will be used to store current location
  this.zoomLevel; //to store current zoom level
  this.currentIndex=0;
  this.data = aData; //locations
  this.map = map;
  //this.setPosition(0,2);
  this.animator = new Animator(30);
  this.pathPoints = [this.getPosition(0,1)]; //start with two points at same place.
  var lineSymbol = {
        path: 'M 0,-1 0,1',
        strokeOpacity: .6,
        scale: 2
      };
    this.lines = new google.maps.Polyline({
        path: this.pathPoints,
        strokeOpacity: 0,
        strokeColor: "#FF0000",
        icons: [{
          icon: lineSymbol,
          offset: '0',
          repeat: '20px'
        }],
        map: map
      });
  this.traveler = new Traveler(this.map,this.getPosition(0,1));
  this.nextPathPoint(1);
}
  1. getPosition 方法用于创建新的 google.maps.LatLng 对象:
GoogleMapTraveler.prototype.getPosition = function (index,amount){
  var lat=0;
  var lng=0;
  for(var i=0; i<amount; i++){
    lat+= parseFloat(this.data[index+i].c[0].v);
    lng+= parseFloat(this.data[index+i].c[1].v);
  }
  var ll=new google.maps.LatLng(
            lat/amount,
            lng/amount);
  return ll;
}
  1. setPosition 方法用于设置旅行者的位置:
GoogleMapTraveler.prototype.setPosition = function(index,amount){
  this.currentFocus = index;
  var lat=0;
  var lng=0;
  for(var i=0; i<amount; i++){
    lat+= parseFloat(this.data[index+i].c[0].v);
    lng+= parseFloat(this.data[index+i].c[1].v);
  }
  var ll=new google.maps.LatLng(
            lat/amount,
            lng/amount);
  if(this.data[index].c[2])this.map.setZoom(this.data[index].c[2].v);
  this.map.setCenter(ll);
}
  1. nextPathPoint 方法结合 Animator 对象实现自动从一个步骤移动到下一个步骤的逻辑:
GoogleMapTraveler.prototype.nextPathPoint = function(index){
  this.setPosition(index-1,2);
  this.pathPoints.push(this.getPosition(index-1,1)); //add last point again
  var currentPoint = this.pathPoints[this.pathPoints.length-1];
  var point = this.getPosition(index,1);
  this.animator.add(currentPoint,"Za",currentPoint.Za,point.Za,1);
  this.animator.add(currentPoint,"Ya",currentPoint.Ya,point.Ya,1);
  this.animator.add(this.traveler.ll,"Za",this.traveler.ll.Za,point.Za,2,0.75);
  this.animator.add(this.traveler.ll,"Ya",this.traveler.ll.Ya,point.Ya,2,0.75);
  this.animator.onUpdate = this.bind(this,this.renderLine);
  this.animator.onComplete = this.bind(this,this.showOverlayCopy);//show copy after getting to destination
}
  1. renderLine 方法更新地图上的信息:
GoogleMapTraveler.prototype.renderLine = function(){
  this.lines.setPath(this.pathPoints);
  if(this.traveler.isReady)this.traveler.refreshPosition();
}
  1. bind 方法用于绑定作用域:
GoogleMapTraveler.prototype.bind = function(scope, fun){
   return function () {
        fun.apply(scope, arguments);
    };
}
  1. 创建 Traveler 类:
function Traveler(map,ll) {
  this.ll = ll;
    this.radius = 15;
    this.innerRadius = 10;
    this.glowDirection = -1;
    this.setMap(map);
    this.isReady = false;

  }
  Traveler.prototype = new google.maps.OverlayView();
  Traveler.prototype.onAdd = function() {
  this.div = document.createElement("DIV");
  this.canvasBG = document.createElement("CANVAS");
    this.canvasBG.width = this.radius*2;
  this.canvasBG.height = this.radius*2;
  this.canvasFG = document.createElement("CANVAS");
    this.canvasFG.width = this.radius*2;
  this.canvasFG.height = this.radius*2;
  this.div.style.border = "none";
  this.div.style.borderWidth = "0px";
  this.div.style.position = "absolute";
  this.canvasBG.style.position = "absolute";
  this.canvasFG.style.position = "absolute";
  this.div.appendChild(this.canvasBG);
  this.div.appendChild(this.canvasFG);
    this.contextBG = this.canvasBG.getContext("2d");
    this.contextFG = this.canvasFG.getContext("2d");
  var panes = this.getPanes();
    panes.overlayLayer.appendChild(this.div);
  }
  Traveler.prototype.draw = function() {
    var radius = this.radius;
    var context = this.contextBG;
  context.fillStyle = "rgba(73,154,219,.4)";
  context.beginPath();
    context.arc(radius,radius, radius, 0, Math.PI*2, true);
  context.closePath();
  context.fill();
  context = this.contextFG;
  context.fillStyle = "rgb(73,154,219)";
  context.beginPath();
    context.arc(radius,radius, this.innerRadius, 0, Math.PI*2, true);
  context.closePath();
  context.fill();
    var projection = this.getProjection();
    this.updatePosition(this.ll);
    this.canvasBG.style.opacity = 1;
    this.glowUpdate(this);
    setInterval(this.glowUpdate,100,this);
    this.isReady = true;

  };

  Traveler.prototype.refreshPosition=function(){
    this.updatePosition(this.ll);   
  }

  Traveler.prototype.updatePosition=function(latlng){
    var radius = this.radius;
    var projection = this.getProjection();
  var point = projection.fromLatLngToDivPixel(latlng);
    this.div.style.left = (point.x - radius) + 'px';
    this.div.style.top = (point.y - radius) + 'px'; 
  }

  Traveler.prototype.glowUpdate=function(scope){ //endless loop
    scope.canvasBG.style.opacity = parseFloat(scope.canvasBG.style.opacity) + scope.glowDirection*.04;
    if(scope.glowDirection==1 && scope.canvasBG.style.opacity>=1) scope.glowDirection=-1;
    if(scope.glowDirection==-1 && scope.canvasBG.style.opacity<=0.1) scope.glowDirection=1;
  }
  1. 创建 Animator 类:
function Animator(refreshRate){
  this.onUpdate = function(){};
  this.onComplete = function(){};
  this.animQue = [];
  this.refreshRate = refreshRate || 35; //if nothing set 35 FPS
  this.interval = 0;
}
Animator.prototype.add = function(obj,property, 
from,to,time,delay){
  obj[property] = from;
  this.animQue.push({obj:obj,
            p:property,
            crt:from,
            to:to,
            stepSize: (to-from)/(time*1000/this.refreshRate),
            delay:delay*1000 || 0});
  if(!this.interval){ //only start interval if not running already
    this.interval = setInterval(this._animate,this.refreshRate,this);   
  }
}
Animator.prototype._animate = function(scope){
  var obj;
  var data;
  for(var i=0; i<scope.animQue.length; i++){
      data = scope.animQue[i];
      if(data.delay>0){
        data.delay-=scope.refreshRate;
      }else{
        obj = data.obj;
        if((data.stepSize>0 && data.crt<data.to) ||
           (data.stepSize<0 && data.crt>data.to)){
          data.crt = data.crt + data.stepSize;
          obj[data.p] = data.crt;
        }else{
          obj[data.p] = data.to;    
          scope.animQue.splice(i,1);
          --i;
        }
      }
  }
  scope.onUpdate();
  if( scope.animQue.length==0){
    clearInterval(scope.interval);
    scope.interval = 0; //reset interval variable
    scope.onComplete();
  }
}

3. 工作原理

3.1 HTML 和 CSS 部分

从 Google 字体库中选择字体并集成到文本覆盖层:

<link href='http://fonts.googleapis.com/css?family=Yellowtail' rel='stylesheet' type='text/css'>
.overlayBox {
  font-family: 'Yellowtail', cursive;
}

3.2 JavaScript 部分

  • 加载 Google 可视化库,触发 init 函数,加载地图和电子表格。
  • 使用 google.visualization.Query 对象直接处理电子表格数据。
  • GoogleMapTraveler 对象作为管理中心,管理 Traveler 标记和 google.maps.Polyline 对象。
  • 通过更新 polyline 实现新线路的显示效果。

3.3 nextPathPoint 方法逻辑

  1. 设置地图视图:
this.setPosition(index-1,2);
  1. 添加新点到 pathPoints 数组:
this.pathPoints.push(this.getPosition(index-1,1));
  1. 创建辅助变量:
var currentPoint = this.pathPoints[this.pathPoints.length-1];
var point = this.getPosition(index,1);
  1. 动画当前点和旅行者的经纬度信息:
this.animator.add(currentPoint,"Za",currentPoint.Za,point.Za,1);
this.animator.add(currentPoint,"Ya",currentPoint.Ya,point.Ya,1);
this.animator.add(this.traveler.ll,"Za",this.traveler.ll.Za,point.Za,2,0.75);
this.animator.add(this.traveler.ll,"Ya",this.traveler.ll.Ya,point.Ya,2,0.75);
  1. 设置动画回调函数:
this.animator.onUpdate = this.bind(this,this.renderLine);
this.animator.onComplete = this.bind(this,this.showOverlayCopy);

3.4 Traveler 标记

Traveler 类主要基于之前的工作,增加了内部动画和 updatePosition 方法,用于移动标记位置。

3.5 Animator 对象更新

  • 集成回调方法 onUpdate onComplete
  • 改进核心动画逻辑,支持正负区域的动画。

以下是整个流程的 mermaid 流程图:

graph LR
    A[初始化] --> B[加载地图和电子表格]
    B --> C[创建 GoogleMapTraveler 对象]
    C --> D[设置地图视图]
    D --> E[添加新点到 pathPoints 数组]
    E --> F[创建辅助变量]
    F --> G[动画当前点和旅行者信息]
    G --> H[设置动画回调函数]
    H --> I[更新地图信息]
    I --> J{动画是否完成}
    J -- 是 --> K[显示覆盖层信息]
    K --> L[更新索引,继续下一个旅行点]
    J -- 否 --> I

通过以上步骤,我们构建了一个交互式谷歌地图应用,展示朋友的旅行路径。希望你能通过这个项目获得乐趣和知识!如果你有任何问题或想法,欢迎分享。

以上就是构建这个交互式谷歌地图实时行程应用的详细内容,包含了具体的操作步骤、代码实现和原理分析。通过这些步骤,你可以创建一个能够根据电子表格数据动态展示旅行路径的地图应用。

4. 代码详细分析

4.1 GoogleMapTraveler

GoogleMapTraveler 类是整个应用的核心管理类,负责协调各个组件的工作。以下是对其关键方法的详细分析:

setPosition 方法
GoogleMapTraveler.prototype.setPosition = function(index,amount){
  this.currentFocus = index;
  var lat=0;
  var lng=0;
  for(var i=0; i<amount; i++){
    lat+= parseFloat(this.data[index+i].c[0].v);
    lng+= parseFloat(this.data[index+i].c[1].v);
  }
  var ll=new google.maps.LatLng(
            lat/amount,
            lng/amount);
  if(this.data[index].c[2])this.map.setZoom(this.data[index].c[2].v);
  this.map.setCenter(ll);
}

此方法根据传入的索引和数量,计算平均经纬度,并设置地图的中心和缩放级别。通过循环遍历数据,累加经纬度值,然后取平均值得到最终的经纬度。如果电子表格中存在缩放级别信息,则设置地图的缩放级别。

nextPathPoint 方法
GoogleMapTraveler.prototype.nextPathPoint = function(index){
  this.setPosition(index-1,2);
  this.pathPoints.push(this.getPosition(index-1,1)); 
  var currentPoint = this.pathPoints[this.pathPoints.length-1];
  var point = this.getPosition(index,1);
  this.animator.add(currentPoint,"Za",currentPoint.Za,point.Za,1);
  this.animator.add(currentPoint,"Ya",currentPoint.Ya,point.Ya,1);
  this.animator.add(this.traveler.ll,"Za",this.traveler.ll.Za,point.Za,2,0.75);
  this.animator.add(this.traveler.ll,"Ya",this.traveler.ll.Ya,point.Ya,2,0.75);
  this.animator.onUpdate = this.bind(this,this.renderLine);
  this.animator.onComplete = this.bind(this,this.showOverlayCopy);
}

该方法是整个应用逻辑的核心,它会递归地处理电子表格中的所有点。首先调用 setPosition 方法设置地图视图,然后将当前点添加到 pathPoints 数组中。接着创建辅助变量 currentPoint point ,分别表示当前点和目标点。使用 Animator 对象对当前点和旅行者的经纬度信息进行动画处理,并设置动画的更新和完成回调函数。

4.2 Traveler

Traveler 类用于创建动画标记,基于之前的工作进行了改进,增加了内部动画和位置更新功能。

updatePosition 方法
Traveler.prototype.updatePosition=function(latlng){
  var radius = this.radius;
  var projection = this.getProjection();
  var point = projection.fromLatLngToDivPixel(latlng);
  this.div.style.left = (point.x - radius) + 'px';
  this.div.style.top = (point.y - radius) + 'px';   
}

此方法根据传入的经纬度,将其转换为像素坐标,并更新标记的位置。通过 getProjection 方法获取投影对象,然后使用 fromLatLngToDivPixel 方法将经纬度转换为像素坐标,最后设置标记的 left top 样式属性。

glowUpdate 方法
Traveler.prototype.glowUpdate=function(scope){ 
  scope.canvasBG.style.opacity = parseFloat(scope.canvasBG.style.opacity) + scope.glowDirection*.04;
  if(scope.glowDirection==1 && scope.canvasBG.style.opacity>=1) scope.glowDirection=-1;
  if(scope.glowDirection==-1 && scope.canvasBG.style.opacity<=0.1) scope.glowDirection=1;
}

该方法实现了标记的发光动画效果。通过不断更新 canvasBG 的透明度,实现发光和暗淡的循环效果。当透明度达到最大值或最小值时,改变发光方向。

4.3 Animator

Animator 类用于处理动画逻辑,通过集成回调方法和改进核心动画逻辑,支持正负区域的动画。

add 方法
Animator.prototype.add = function(obj,property, 
from,to,time,delay){
  obj[property] = from;
  this.animQue.push({obj:obj,
            p:property,
            crt:from,
            to:to,
            stepSize: (to-from)/(time*1000/this.refreshRate),
            delay:delay*1000 || 0});
  if(!this.interval){ 
    this.interval = setInterval(this._animate,this.refreshRate,this);   
  }
}

此方法用于添加动画任务到动画队列中。设置对象的初始属性值,计算步长和延迟时间,并将动画任务添加到 animQue 数组中。如果动画定时器未启动,则启动定时器。

_animate 方法
Animator.prototype._animate = function(scope){
  var obj;
  var data;
  for(var i=0; i<scope.animQue.length; i++){
      data = scope.animQue[i];
      if(data.delay>0){
        data.delay-=scope.refreshRate;
      }else{
        obj = data.obj;
        if((data.stepSize>0 && data.crt<data.to) ||
           (data.stepSize<0 && data.crt>data.to)){
          data.crt = data.crt + data.stepSize;
          obj[data.p] = data.crt;
        }else{
          obj[data.p] = data.to;    
          scope.animQue.splice(i,1);
          --i;
        }
      }
  }
  scope.onUpdate();
  if( scope.animQue.length==0){
    clearInterval(scope.interval);
    scope.interval = 0; 
    scope.onComplete();
  }
}

该方法是动画的核心逻辑,通过定时器不断更新动画任务的属性值。如果动画任务有延迟,则减少延迟时间;否则,根据步长更新属性值。当动画任务完成时,从动画队列中移除该任务。当所有动画任务完成时,清除定时器并触发完成回调函数。

5. 总结

5.1 主要功能总结

  • 构建了一个交互式谷歌地图应用,根据朋友的旅行信息进行动画展示。
  • 集成了 Google 电子表格数据,实现了动态更新旅行路径的功能。
  • 通过 GoogleMapTraveler 类管理地图视图、标记和线路的动画。
  • 使用 Traveler 类创建动画标记,增加了标记的视觉效果。
  • 改进了 Animator 类,支持正负区域的动画和回调函数。

5.2 技术亮点

  • 利用 Google 可视化库和 API 接口处理电子表格数据。
  • 通过更新 polyline 实现新线路的显示效果,增强了用户体验。
  • 集成回调方法,实现了动画的更新和完成逻辑。

5.3 未来展望

  • 可以进一步优化动画效果,增加更多的交互功能,如点击标记显示详细信息。
  • 支持更多的数据来源,如数据库或其他在线服务。
  • 改进性能,减少动画的卡顿现象。

以下是关键类和方法的总结表格:
| 类名 | 主要方法 | 功能描述 |
| ---- | ---- | ---- |
| GoogleMapTraveler | setPosition nextPathPoint renderLine showOverlayCopy | 管理地图视图、标记和线路的动画,处理电子表格数据 |
| Traveler | updatePosition glowUpdate | 创建动画标记,更新标记位置和实现发光效果 |
| Animator | add _animate | 处理动画逻辑,支持回调函数和正负区域的动画 |

通过以上的分析和总结,我们可以看到这个应用的设计思路和实现方法,希望能为你开发类似的地图应用提供参考。如果你在开发过程中遇到问题或有新的想法,欢迎随时交流。

以下是整个应用的关键流程 mermaid 流程图:

graph LR
    A[初始化] --> B[加载地图和数据]
    B --> C[创建 GoogleMapTraveler 对象]
    C --> D[设置初始地图视图]
    D --> E[添加第一个点到路径]
    E --> F[启动动画]
    F --> G{动画是否完成}
    G -- 是 --> H[显示覆盖层信息]
    H --> I{是否还有数据}
    I -- 是 --> D
    I -- 否 --> J[结束]
    G -- 否 --> F

这个流程图展示了应用的主要流程,从初始化开始,加载地图和数据,然后创建管理对象,设置地图视图,添加路径点,启动动画。动画完成后,显示覆盖层信息,判断是否还有数据,如果有则继续下一个点的处理,否则结束应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值