四、地图制作器(MapMaker):结合谷歌地图和画布
在本章中,您将学习以下内容:
-
使用 Google Maps API 显示特定位置的地图
-
使用透明度(也称为 alpha 或不透明度)和自定光标图标在画布上绘制图形
-
通过管理事件和 z 索引级别,结合使用 Google Maps 和 HTML5 功能,为您的用户提供图形用户界面(GUI)
-
计算两个地理位置之间的距离
介绍
本章的项目是一个涉及地理地图的应用程序。如今,许多应用程序都需要使用由他人或组织提供的应用程序编程接口(API)。本章将介绍 Google Maps API 的使用,并且是使用 Google Maps JavaScript 版本 3 API 的两章中的第一章。图 4-1 为开启画面。
图 4-1
地图聚光灯项目的打开屏幕
请注意位于地图中间和单词 College 顶部的红色小(手绘)x。在决定地图标记时,你面临一个权衡。较小的标记更难看到。更大和/或更复杂的标记更容易被看到,但是会阻挡地图的更多部分或者分散地图的注意力。这张地图以采购学院校园为中心。对于此程序,这是初始基准位置。基准位置用于计算距离。注意单选按钮显示三个选择,在三个大洲。中间的选择是开始的选择。
在地图上移动鼠标,如图 4-2 所示。
图 4-2
地图上的阴影/聚光灯
注意现在地图上的阴影和聚光灯的组合。地图的大部分被半透明的阴影覆盖。你需要相信我,这张截图是我在地图上移动鼠标时拍摄的。鼠标位置周围有一个圆圈,原始地图显示通过。在地图上移动时,光标不是标准的默认光标,而是我用一个代表紧凑型荧光灯的小图像创建的。
屏幕上的文字显示,从基地到地图上我点击的最后一个点的距离为 4.96 公里。所有这些位置的标记都是手绘的 x 。这个位置的纬度和经度用括号表示。
更改位置的界面是一组单选按钮(一次只能选择一个按钮)和一个标记为“更改”的按钮,当用户/观众/访问者决定进行更改时,可以单击该按钮。
这个项目的用户可以使用 Google Maps 提供的通用 GUI 功能。这包括用于放大和缩小的+和–按钮。图 4-3 展示了使用我的单选按钮切换到位于伦敦的 Springer Nature/a press Publishers 和 Google Maps +进行放大的结果。有可能放大得更远。注意哈利波特商店。
图 4-3
放大到伦敦
也可以将地图类型更改为卫星地图,并通过单击鼠标然后再次按下来平移地图。图 4-4 显示了切换到卫星和平移的效果。
图 4-4
缩小并向西移动,卫星图像
我用界面换到第三种可能:日本京都。如图 4-5 所示。
图 4-5
日本京都
接下来,我使用谷歌地图功能来改变京都的地图/地形。结果如图 4-6 所示。
图 4-6
日本京都基地,地形图
同样,请注意红色的小 x 表示基准位置,屏幕顶部的文本显示新基准位置的名称。
每个基地的位置由我确定的三个值中的每一个的纬度和经度值决定。我的代码不是“要求”Google Maps 按名称查找这些位置。如果你在谷歌或谷歌地图中输入“购买大学,纽约”和其他地点,你可能会得到稍微不同的结果。要使这个应用程序成为您自己的应用程序,您需要决定一组基本位置,并查找纬度和经度值。我将在下一节中提出实现这一点的方法。
以防你好奇,缩小到缩放比例上最远的位置会产生如图 4-7 所示的结果。这个投影展示了所谓的格陵兰问题。格陵兰并不比非洲大,但实际上大约是它的 1/14。
图 4-7
地图的最远视图
图 4-8 显示了接近最接近极限的地图。使用左上角的按钮,地图也被更改为卫星视图。
图 4-8
放大到可以探测到城市街区的地方,购买大学基地
最后,图 4-9 显示了放大到极限的地图。这本质上是在建筑层面。这栋建筑是自然和社会科学学院的所在地,也是我的办公室和电脑教室所在地。
图 4-9
一直放大
通过使用界面缩小,平移和再次放大,我可以确定从任何基本位置到世界上任何其他位置的距离!我还可以使用这个应用程序来确定任何位置的纬度和经度值。您需要知道纬度和经度,以便在第五章中更改或添加基本位置列表,以及确定项目的位置。我将在下一节回顾纬度和经度。
谷歌地图本身是一个非常有用的应用。本章和下一章将演示如何将该功能引入到您自己的应用程序中。也就是说,我们将谷歌地图的一般功能与我们可以使用 HTML5 和 JavaScript 开发的任何东西或几乎任何东西结合起来。
纬度和经度以及其他关键要求
这个项目最基本的要求是理解地理坐标系统。正如在画布上指定点或在屏幕上指定位置需要一个坐标系统一样,在地球上使用一个位置系统也是必要的。在过去的几百年里,经纬度系统已经得到了发展和标准化。这些值是角度,纬度表示与赤道的角度,经度表示与英国格林威治本初子午线的角度。后者是一个随意的选择,在 19 世纪晚期成为标准。
这里有一个北半球的偏差:纬度值从赤道的 0 度到北极的 90 度和南极的-90 度。类似地,经度值从格林威治本初子午线向东为正值,向西为负值。纬度与赤道平行,经度垂直。纬度通常被称为纬线,通常表现为水平线,经度被称为经线,通常表现为垂直线。这种定位是任意的,但是相当牢固地建立起来了。
我将使用十进制值,这是谷歌地图中默认显示的值,但你会看到度数、分钟(1/60 度)和秒(1/60 分钟)的组合。您没有必要记住经纬度值,但这有助于培养对系统的一些直观感觉。你可以通过我所说的“双向选择”来做到这一点首先,识别和比较你知道的地方的经纬度值,其次,选择值,看看它们是什么。例如,我的项目版本的基本值如下:
var locations = [
[51.534467,-0.121631, "Springer Nature (Apress Publishers) London, UK"],
[41.04796,-73.70539,"Purchase College/SUNY, NY, USA"],
[35.085136,135.776585,"Kyoto, Japan"]
];
首先要注意的是纬度值相当接近,而经度值是负的,不太接近。因为我决定选择三大洲的地方作为基地,所以你需要进行实验,看看纬度和经度的微小变化会产生什么影响。你可以看到这三个地方都在赤道以北。在经度上,伦敦的值接近于零,接近格林威治本初子午线。你也可以注意到京都的经度是正的,其他的是负的。这些都有道理,但是你需要自己做实验来适应这些单元。
有许多方法可以找到特定位置的经纬度。您可以按如下方式使用谷歌地图:
图 4-10
在谷歌地图中获取经纬度值
-
从 Gmail 中的方形点阵调用谷歌地图,或者进入
http://.maps.google.com
。 -
在位置字段中输入一个位置。我在自由女神像打字。
-
点按以获得菜单:
图 4-10 显示了一个出现在地图底部的小窗口。
点击这里得到一个显示经度和纬度的小窗口。
图 4-11
显示纬度和经度的框
另一种选择是使用 Wolfram Alpha ( www.wolframalpha.com
),如图 4-12 ,它提供了一种确定经纬度值以及许多其他东西的方法。
图 4-12
Wolfram Alpha 上的查询结果
请注意结果的格式。这是度/分/秒的格式,N 代表北方,W 代表西方。当我点击显示十进制按钮时,程序显示如图 4-13 所示的内容。
图 4-13
Wolfram Alpha 查询的十进制结果
请注意,经度仍然显示为 W 代表西方,而不是谷歌地图给出的负值。
按照我所说的“反方向走”,你可以把纬度和经度值输入谷歌地图。图 4-14 显示了放入 0.0 和 0.0 的结果。它是加纳南部海洋中的一个点。这是赤道上的一点和格林威治本初子午线上的。
图 4-14
格林威治本初子午线处的赤道
我试图在格林威治本初子午线上找到英格兰的一个地方,并在 52.0 度的纬度上猜测时产生了图 4-15 所示的结果。
图 4-15
结果在格林威治本初子午线附近的一个地方
A 标记表示 Google 数据库中离请求位置最近的地方。我使用了 Drop LatLng 标记选项来显示准确的纬度和经度值。
该项目的关键需求始于使用指定的纬度和经度值将 Google Maps 引入 HTML5 应用程序的任务。一个额外的需求是在地图上产生阴影/聚光灯的组合来跟踪鼠标的移动。我还要求将鼠标的默认光标改为我自己选择的光标。
接下来,我添加了一个在地图上放置标记的要求,但还是用我选择的图形图标,而不是谷歌地图中标准的上下颠倒的泪珠。泪珠标记很好,但我的设计目标是与众不同,向您展示如何将您自己的创造力融入到应用程序中。
除了图形,我希望用户能够使用谷歌地图设备和我用 HTML5 构建的任何 GUI 功能。这都需要管理由 Google Maps API 设置的事件和使用 HTML5 JavaScript 设置的事件。对我想要制作的用户界面的事件的响应包括以下内容:
-
用阴影/聚光灯图形跟踪鼠标移动
-
通过在地图上放置一个 x 来响应点击
-
保留对谷歌地图界面的相同响应(滑块、平移按钮、通过抓取地图进行平移)
-
以适当的方式处理单选按钮和更改按钮
谷歌地图提供了一种确定位置之间距离的方法。因为我想设置这个项目来根据基本位置工作,所以我需要一种直接计算距离的方法。
这些是地图聚光灯项目的关键要求。现在我将解释我用来构建项目的 HTML5 特性。目标是使用 Google Maps 特性和 JavaScript 特性,包括事件,并且不让它们互相干扰。你可以把你学到的东西用于这个项目和其他项目。
HTML5、CSS 和 JavaScript 特性
map-maker 项目面临的挑战是引入谷歌地图,然后在外观和 GUI 操作方面一起使用地图、画布和按钮。我将描述基本的 Google Maps API,然后解释 HTML5 特性如何提供部分屏蔽和事件处理。
谷歌地图应用编程接口
谷歌地图 JavaScript API 第 3 版基础在 http://code.google.com/apis/maps/documentation/javascript/basics.html
有很好的文档。您现在不需要参考它,但是如果您决定构建自己的项目,它会对您有所帮助。这对于为移动设备开发应用程序特别有帮助。
大多数 API 都表现为相关对象的集合,每个对象都有属性(也称为属性)和方法。API 还可以包括事件和设置事件的方法。这就是谷歌地图 API 的情况。重要的对象是Map
、LatLng
和Marker
。设置事件的方法是addListener
,这可以用来设置对点击地图的响应。
使用谷歌地图 API 的第一步是去这个网站获取一个密钥: https://developers.google.com/maps/documentation/javascript/get-api-key
。
访问 API 的代码是修改以下内容,然后将其添加到 HTML 文档中:
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
type="text/javascript"></script>
注意
本文的第一版使用了被称为“无密钥”的 API。虽然我的原始代码仍然可以在我的领域中使用,但是 Google now 更加严格了。这些特性的使用是有限额的,尽管限额看起来很大,但是如果您计划在生产中使用,您需要研究文档。
下一步——如果你只想引入一个谷歌地图,这可能就是你所需要的——是建立一个对Map
构造函数方法的调用。这方面的伪代码是
map = new google.maps.Map(place you are going to put the map, associative array with options);
请注意,将变量命名为map
并没有什么坏处。
让我们一次一个地讨论这两个参数。放置地图的地方可以是 HTML 文档主体中定义的一个div
。然而,我选择动态创建div
。我是通过在body
语句中设置onLoad
属性,在一个以通常方式调用的init
函数中使用代码来实现的。我还编写了代码来在div
中创建一个canvas
元素。代码是
candiv = document.createElement("div");
candiv.innerHTML = ("<canvas id="canvas" width="600" height="400">No canvas
</canvas>");
document.body.appendChild(candiv);
can = document.getElementById("canvas");
pl = document.getElementById("place");
ctx = can.getContext("2d");
can
、pl
和ctx
是全局变量,每个变量都可供其他函数使用。
注意
尽管我试图使用“在 HTML 文档中引入对谷歌地图的访问”这样的语言,但我对描述一个“制作”地图的功能感到内疚。Google Maps 连接是一个动态连接,其中 Google Maps 创建了所谓的“要显示的图块”。
Map
方法的第二个参数是一个关联数组。关联数组有命名元素,没有索引元素。用于Map
方法的数组可以指示缩放级别、地图中心和地图类型等。缩放级别可以从 0 到 18。0 级如图 4-7 所示。第 18 层可以显示建筑物。地图的类型有路线图、卫星图、混合图和地形图。这些都是用谷歌地图 API 中的常量来表示的。中心由一个类型为LatLng
的值给出,如您所料,该值是使用代表纬度和经度值的十进制数构造的。使用关联数组意味着我们不必遵循参数的固定顺序,默认设置将应用于我们忽略的任何参数。
下面是我的makemap
函数的开始。调用该函数时使用了两个数字来表示地图的中心纬度和经度。我的代码构造了一个名为blatlng
的LatLng
对象,设置了保存地图规范的数组,然后构造了地图——也就是说,构造了 Google Maps 的门户。
function makemap(mylat,mylong) {
var marker;
blatlng = new google.maps.LatLng(mylat,mylong);
myOptions = {
zoom: 12,
center: blatlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("place"), myOptions);
Map
方法构造对 Google Maps 的访问,从 ID 为place
的 div 中带有指定选项的地图开始。makemap
功能继续,在地图中心放置一个标记。这是通过设置一个关联数组作为Marker
方法的参数来实现的。图标标记将是我创建的图像,命名为rxmarker
,使用我自己设计的图像,绘制成红色 x 。
marker = new google.maps.Marker({
position: blatlng,
title: "center",
icon: rxmarker,
map: map });
在makemap
函数中还有一个语句,但我将在后面解释其余的。
画布图形
我们希望用鼠标在地图上移动的图形类似于第三章中使用的蒙版,将矩形视频剪辑变成圆形视频剪辑。这两个面具都可以描述为类似于一个长方形的甜甜圈:一个带圆孔的长方形。我们使用两条路径绘制阴影/聚光灯的图形,就像上一章中视频的遮罩一样。然而,这两种情况有两个明显的不同:
-
这个面具的确切形状各不相同。外部边界是整个画布,孔的位置与鼠标的当前位置对齐。这个洞会四处移动。
-
面具的颜色不是纯色颜料,而是透明的灰色。
画布从谷歌地图的顶部开始。我通过编写设置 z 索引值的样式指令来实现这一点:
canvas {position:absolute; top: 165px; left: 0px; z-index:100;}
#place {position:absolute; top: 165px; left: 0px; z-index:1;}
第一个指令引用所有画布元素。这个 HTML 文档里只有一个。回想一下,z 轴从屏幕出来朝向观察者,所以较高的值在较低的值之上。还要注意,我们在 JavaScript 代码中使用了zIndex
,在 CSS 中使用了z-index
。JavaScript 解析器会将–符号视为减号操作符,因此对zIndex
的更改是必要的。我需要编写代码来改变zIndex
以获得我想要的这个项目的事件处理。
图 4-16 显示了在画布上绘制的阴影遮罩的一个例子。我已经使用单选按钮将基本位置设置为京都。然后,我使用谷歌地图控件缩小,平移到东京,然后放大。就 z 索引而言,画布位于地图上方,遮罩是用透明的灰色绘制的,因此下方的地图是可见的。
图 4-16
地图上一个地方的阴影/聚光灯
图 4-17 显示了在同一地图上绘制的阴影掩膜的另一个例子。这是因为 Google Maps 处理用户的鼠标移动,然后 JavaScript 代码处理鼠标移动来恢复阴影。
图 4-17
地图上另一个位置的阴影遮罩
这里有几个主题是相互关联的。让我们假设变量mx
和my
保存鼠标光标在画布上的位置。我将在本章后面解释如何做到这一点。函数drawshadowmask
将绘制阴影遮罩。透明的灰色是遮罩的颜色,在名为grayshadow
的变量 I 中定义,并使用内置函数rgba
构建。rgba
代表红绿蓝阿尔法。alpha 指的是透明度/不透明度。alpha 值为 1 表示颜色完全不透明:纯色。值为 0 表示完全透明,颜色不可见。还记得红色、绿色和蓝色值从 0 到 255,255、255 和 255 的组合是白色。这是一个实验的时代。我决定为灰色/浅灰色/幽灵般的阴影设置如下:
var grayshadow = "rgba(250,250,250,.8)";
函数drawshadowmask
使用了几个常量变量——它们从不改变。指示这些值的示意图如图 4-18 所示。
图 4-18
为掩模指示变量值的示意图
遮罩分为两部分,就像对弹跳视频的遮罩所做的那样。你可以回头看图 3-8 和图 3-9 。编码是相似的:
function drawshadowmask(mx,my) {
ctx.clearRect(0,0,600,400);
ctx.fillStyle = grayshadow;
ctx.beginPath();
ctx.moveTo(canvasAx,canvasAy);
ctx.lineTo(canvasBx,canvasBy);
ctx.lineTo(canvasBx,my);
ctx.lineTo(mx+holerad,my);
ctx.arc(mx,my,holerad,0,Math.PI,true);
ctx.lineTo(canvasAx,my);
ctx.lineTo(canvasAx,canvasAy);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(canvasAx,my);
ctx.lineTo(canvasDx,canvasDy);
ctx.lineTo(canvasCx,canvasCy);
ctx.lineTo(canvasBx,my);
ctx.lineTo(mx+holerad,my);
ctx.arc(mx,my,holerad,0,Math.PI,false);
ctx.lineTo(canvasAx,my);
ctx.closePath();
ctx.fill();
}
现在我们继续看红色灯泡。
光标
光标——移动鼠标时在屏幕上移动的小图形——可以在 style 元素或 JavaScript 中设置。图形有几个内置的选择(例如,十字准线和指针),我们也可以参考我们自己的设计来定制光标,这就是我在这个项目中演示的。我加入了声明
can.onmousedown = function () { return false; } ;
在init
功能中,防止按下鼠标时改变默认光标。这可能是不必要的,因为默认可能不会被触发。
为了将移动鼠标的光标更改为传递聚光灯的东西,我创建了一个红色紧凑型荧光灯泡的图片,并将其保存在文件light.gif
中。然后,我在函数showshadow
中使用了下面的语句。showshadow
函数已经被设置为mousemove
的事件处理程序
can.style.cursor = "url('light.gif'), pointer";
指示 JavaScript 应该在光标位于can
元素顶部时使用图像的地址。此外,如果light.gif
文件不可用,该语句会指示 JavaScript 使用内置指针图标。这类似于用优先选择列表来指定字体的方式。变量can
已经被设置为引用画布元素。当画布被推到谷歌地图下时,光标将不被使用,这将在下一节讨论。
JavaScript 事件
当我开始从事这个项目时,对事件的处理——即鼠标事件,但也包括改变 Google 地图缩放比例或单击单选按钮的事件——似乎是最令人生畏的。关键的考虑是事件是由 Google Maps 处理还是由我的 JavaScript 代码处理。然而,实际的实现结果很简单。在init
函数和makemap
函数中,我编写代码来设置鼠标移动、按下鼠标按钮和抬起鼠标按钮的事件处理,所有这些都与canvas
元素有关。例如,在init
函数中,有
can.addEventListener('mousemove',showshadow);
can.addEventListener('mousedown',pushcanvasunder);
can.addEventListener("mouseout",clearshadow);
如前所述,showshadow
函数调用drawshadowmask
函数。我可以将这两个功能结合起来,但是将任务分成更小的任务通常是一个好的实践。showshadow
功能确定鼠标位置,进行调整,使灯泡底座位于聚光灯的中心,然后调用drawshadowmask
:
function showshadow(ev) {
var mx;
var my;
if ( ev.layerX || ev.layerX == 0) {
mx= ev.layerX;
my = ev.layerY;
}
else if (ev.offsetX || ev.offsetX == 0) {
mx = ev.offsetX;
my = ev.offsetY;
}
can.style.cursor = "url('light.gif'), pointer";
mx = mx+10;
my = my + 12;
drawshadowmask(mx,my);
}
提到ev.layerX
和ev.layerY
的if
声明是针对老款火狐浏览器的。它可能会被移除。
现在我需要确定当用户按下鼠标时我想做什么。我决定让阴影消失,让地图以最大亮度显示。除了事情的表象,我还想让谷歌地图 API 恢复控制。希望 Google Maps API 接管的一个关键原因是,我想在地图上放置一个标记,而不是在画布上,来标记一个位置。这是因为我想让标记随着地图移动,而这很难通过在画布上绘制来实现。我需要将画布上的标记与地图的平移和缩放同步。相反,API 为我完成了所有这些工作。此外,我需要 Google Maps API 来生成该位置的纬度和经度值。
可以说,重新控制谷歌地图的方法是“把画布压下去”。该功能是
function pushcanvasunder(ev) {
can.style.zIndex = 1;
pl.style.zIndex = 100;
}
将画布压到下面或放回上面的操作不是瞬间完成的。我愿意接受关于(1)如何定义接口和(2)如何实现你所定义的建议。这里有改进的余地。
另一个需要注意的情况是,当用户将鼠标从画布上移开时,我希望发生什么?mouseout
事件是可以监听的,所以我编写了设置事件的代码(参见前面显示的can.addEventListener
语句)由clearshadow
函数处理。clearshadow
函数正好完成了这一点——它清除了整个画布,包括阴影:
function clearshadow(ev) {
ctx.clearRect(0,0,600,400);
}
在引入 Google 地图的函数中,我为地图的mouseup
设置了一个事件处理程序。
listener = google.maps.event.addListener(map, 'mouseup', function(event) {
checkit(event.latLng);
});
对addListener
的调用是 Google Maps API 的一部分,而不是 JavaScript 本身,它设置了对checkit
函数的调用。用一种更非正式的方式重复一下已经说过的话:这个对google.maps.event.addListener
的调用设置了 Google API 来监听地图上的mouseup
事件。以下语句使 JavaScript 监听can
(画布)上的mouseout
事件。
can.addEventListener("mouseout",clearshadow);
使用event
对象的属性作为参数来调用checkit
函数。正如您所猜测的,event.latLng
是在map
对象上释放鼠标按钮时鼠标所在位置的经纬度值。checkit
功能将使用这些值来计算离基准位置的距离,并将这些值和距离一起打印在屏幕上。这段代码调用了我编写的对值进行舍入的函数。我这样做是为了避免显示一个有很多有效数字的值,超过了适合这个项目的数字。Google Maps API marker
方法提供了一种使用我选择的图像作为标记的方法,这次是黑色手绘的 x ,并在标记中包含一个标题。推荐的标题是让使用屏幕阅读器的人可以访问应用程序,尽管我不能说这个项目在可访问性方面会让任何人满意。可以产生如图 4-19 所示的屏幕。注意我住的基斯科山附近的 x。顶部的信息显示了我通勤的英里数。可以更改代码来计算英里或公里。
图 4-19
指示地图上显示的距离的标题
用保存纬度和经度值的参数调用的checkit
函数如下:
function checkit(clatlng) {
var distance = dist(clatlng,blatlng);
distance = round(distance,2);
var distanceString = String(distance)+" km";
marker = new google.maps.Marker({
position: clatlng,
title: distanceString,
icon: bxmarker,
map: map });
var clat = clatlng.lat();
var clng = clatlng.lng();
clat = round(clat,4);
clng = round(clng,4);
document.getElementById("answer").innerHTML =
"The distance from base to most recent marker ("+clat+", "+clng+") is "+String(distance) +" miles.";
//change miles to km depending on value used for R in the dist function
can.style.zIndex = 100;
pl.style.zIndex = 1;
}
尽管我省略了文本中的大部分注释,但我觉得有必要保留关于英里和公里的注释。我建议你在工作中也这样做。
请注意,该函数做的最后一件事是将画布放回地图的顶部。
CHANGE 按钮和单选按钮是使用标准 HTML 和 JavaScript 实现的。该表单是使用以下 HTML 代码生成的:
<form name="f" onSubmit=" return changebase();">
<input type="radio" name="loc" /> Springer Nature (Apress Publishers) London, UK<br/>
<input id="first" type="radio" name="loc" /> Purchase College/SUNY, NY, USA<br/>
<input type="radio" name="loc" /> Kyoto, Japan<br/>
<input type="submit" value="CHANGE">
</form>
当点击标记为 CHANGE 的提交按钮时,调用函数changebase
。changebase
函数确定选中了哪个单选按钮,并使用 Locations 表获取纬度和经度值。然后它使用这些参数值调用makemap
。这种组织数据的方式叫做并行结构:数组元素locations
对应单选按钮。最后一条语句将 header 元素的innerHTML
设置为显示文本,包括所选基本位置的名称。
function changebase() {
var mylat;
var mylong;
for(var i=0;i<locations.length;i++) {
if (document.f.loc[i].checked) {
mylat = locations[i][0];
mylong = locations[i][1];
makemap(mylat,mylong);
document.getElementById("header").innerHTML =
"Base location (small red x) is "+locations[i][2];
}
}
return false;
}
计算显示的距离和舍入值
正如我们许多人所知,谷歌地图提供距离信息,甚至区分步行和驾驶。对于这个应用程序,我需要更多的控制来指定我想要计算距离的两个位置,所以我决定用 JavaScript 开发一个函数。确定两个点之间的距离,每个点代表纬度和经度值,是使用余弦球面定律来完成的。我的消息来源是 http://www.movable-type.co.uk/scripts/latlong.html
。这是代码。请注意,为了生成以公里为单位的值,您使用一个 R 值和一个被注释的 miles 值。如果当你切换到英里,你需要确保显示的信息说英里。
function dist(point1, point2) {
var R = 6371; // km Need to make sure this syncs with the message displayed re: distance.
// var R = 3959; // miles
var lat1 = point1.lat()*Math.PI/180;
var lat2 = point2.lat()*Math.PI/180 ;
var lon1 = point1.lng()*Math.PI/180;
var lon2 = point2.lng()*Math.PI/180;
var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
Math.cos(lat1)*Math.cos(lat2) *
Math.cos(lon2-lon1)) * R;
return d;
}
警告
我没有在代码中包含很多注释,因为我在本章的表格中注释了每一行。但是,注释很重要。我强烈建议在dist
函数中留下对km
和miles
的注释,这样你就可以适当地调整你的程序。或者,您可以显示这两个值,或者给用户一个选择。
最后一个函数用于舍入值。当一个量依赖于一个人移动鼠标时,你不应该显示一个有很多小数位的值。但是,请记住,纬度和经度代表大单位。我决定用两位小数显示距离,用四位小数显示纬度和经度。
我写的函数挺一般的。它有两个参数,一个是数字num
,另一个是places
,表示取值的小数位数。你可以在其他情况下使用它。它通过添加我称为增量的值,然后计算不大于该值的最大整数,适当地向上或向下舍入。因此
-
round(9.147,2)
将产生 9.15 -
round(9.143, 2)
将产生 9.14
代码的工作方式是首先确定我称之为的因子,10 的预期位数。对于 2,这将是 100。然后我计算增量。对于两个地方,这将是 5 / 100 * 10,也就是 5/1000,也就是. 005。我的代码执行以下操作:
-
将增量加到原始数字上。
-
将结果乘以系数。
-
计算不大于结果的最大整数(这称为下限)—产生一个整数。
-
将结果除以因子。
代码如下:
function round (num,places) {
var factor = Math.pow(10,places);
var increment = 5/(factor*10);
return Math.floor((num+increment)*factor)/factor;
}
我使用round
函数将距离四舍五入到两位小数,将纬度和经度四舍五入到四位小数。
小费
JavaScript 有一个名为toFixed
的方法,本质上执行我这一轮的任务。如果num
持有一个数字,比如说 51.5621,那么num.toFixed()
将产生 51,而num.toFixed(2)
将产生 51.56。我了解到这种方法可能会有误差,所以我选择创建自己的函数。不过,你可能很乐意在自己的应用程序中使用toFixed()
。
随着相关 HTML5 和谷歌地图 API 特性的解释,我们现在可以把它们放在一起。
构建应用程序并使之成为您自己的应用程序
map spotlight 应用程序将 Google Maps 功能与 HTML5 编码结合起来。该应用程序的简要概述如下:
-
init
:初始化,包括带入地图(makemap
)和用处理程序设置鼠标事件:showshadow
、pushcanvasunder
、clearshadow
-
makemap
:引入一个地图并设置事件处理,包括对checkit
的调用 -
showshadow
:调用drawshadowmask
-
pushcanvasunder
:启用地图上的事件 -
checkit
:计算距离,添加自定义标记,显示距离和四舍五入后的经纬度
描述被调用/被调用和调用关系的函数表(表 4-1 )对于所有的应用程序都是相同的。
表 4-1
功能 在地图制作项目
|功能
|
由调用/调用
|
来电
|
| — | — | — |
| init
| 由<body>
标签中的onLoad
属性的动作调用 | makemap
|
| pushcanvasunder
| 由在init
中调用的addEventListener
的动作调用 | |
| clearshadow
| 由在init
中调用的addEventListener
的动作调用 | |
| showshadow
| 由在init
中调用的addEventListener
的动作调用 | drawshadowmask
|
| drawshadowmask
| 由showshadow
调用 | |
| makemap
| 由init
调用 | |
| checkit
| 由在makemap
中调用的addEventListener
的动作调用 | round
,dist
|
| round
| 由checkit
调用(三次) | |
| dist
| 由checkit
调用 | |
| changebase
| 由<form>
中的onSubmit
动作调用 | makemap
|
表 4-2 显示了名为mapspotlight.html
的地图制作应用程序的代码。
表 4-2
mapspotlight.html 应用程序的完整代码
|代码行
|
描述
|
| — | — |
| <!DOCTYPE html>
| 页眉 |
| <html>
| 开始html
标签 |
| <head>
| 开始head
标签 |
| <title>Spotlight </title>
| 完整标题 |
| <meta charset="UTF-8">
| 什么时候 |
| <style>
| 打开style
元件 |
| header
{font-family:Georgia,"Times New Roman",serif;
| 设置标题的字体 |
| font-size:16px;
| 字体大小 |
| display:block;}
| 前后换行 |
| canvas {position:absolute; top: 165px; left:0px;
| 单个画布元素的样式指令;在页面上稍微向下放置 |
| z-index:100;}
| 画布的初始设置在地图的顶部 |
| #place {position:absolute; top: 165px; left: 0px;
| 用于持有谷歌地图的div
的样式指令;位置与画布完全相同 |
| z-index:1;}
| 初始设置在帆布下 |
| </style>
| 关闭style
元素 |
| <script async defer src="``https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap``type="text/javascript"></script>
| 引入包含 Google Maps API 的外部脚本元素;注意:您需要获得自己的密钥来运行该程序 |
| <script type="text/javascript" charset="UTF-8">
| 开始script
标签 |
| var
locations = [
| 定义一组基本位置;注意:您需要与正文中的单选按钮相协调 |
| [51.534467,-0.121631, "Springer Nature (Apress Publishers) London, UK"],
| 伦敦的纬度、经度名称出版商办公室 |
| [41.04796,-73.70539,"Purchase College/SUNY, NY, USA"],
| 。。。采购学院 |
| [35.085136,135.776585,"Kyoto, Japan"]
| 。。。京都 |
| ];
| 封闭位置阵列 |
| var candiv;
| 用于握持div
握持画布 |
| var can;
| 参考画布元素 |
| var ctx;
| 画布的参考上下文;用于所有绘图 |
| var pl;
| 参考拿着谷歌地图的div
|
| function
init() {
| init
的功能头 |
| var mylat;
| 将保存纬度值 |
| var mylong;
| 将保存经度值 |
| candiv = document.createElement("div");
| 创建一个div
|
| candiv.innerHTML = ("<canvas id="canvas" width="600" height="400">No canvas </canvas>");
| 将其内容设置为一个canvas
元素 |
| document.body.appendChild(candiv);
| 添加到正文 |
| can = document.getElementById("canvas");
| 设置对画布的引用 |
| pl = document.getElementById("place");
| 设置对持有谷歌地图的div
的引用 |
| ctx = can.getContext("2d");
| 设置上下文 |
| can.onmousedown = function () { return false; } ;
| 防止将光标更改为默认值 |
| can.addEventListener('mousemove',showshadow);
| 为鼠标移动设置事件处理 |
| can.addEventListener('mousedown',pushcanvasunder);
| 设置按下鼠标按钮的事件处理 |
| can.addEventListener("mouseout",clearshadow);
| 设置将鼠标移出画布的事件处理 |
| mylat = locations[1][0];
| 将纬度设置为第一个(中间)位置的纬度 |
| mylong
= locations[1][1];
| 将经度设置为第一个(中间)位置的经度 |
| document.getElementById("first").checked="checked";
| 将中间单选按钮设置为显示为选中状态 |
| makemap(mylat,mylong);
| 调用函数来制作地图(在指定位置引入谷歌地图) |
| }
| 关闭init
功能 |
| function
pushcanvasunder(ev) {
| pushcanvas
函数的头,用引用事件的参数调用 |
| can.style.zIndex = 1;
| 向下推帆布 |
| pl.style.zIndex = 100;
| 设置地图div
向上 |
| }
| 关闭pushcanvasunder
功能 |
| function clearshadow(ev) {
| clearshadow
函数的头,用引用事件的参数调用 |
| ctx.clearRect(0,0,600,400);
| 清除画布(擦除阴影遮罩) |
| }
| 关闭clearshadow
功能 |
| function showshadow(ev) {
| showshadow 函数的标头,用引用事件的参数调用 |
| var mx;
| 将用于保持鼠标的水平位置 |
| var
my;
| 将用于保持鼠标的垂直位置 |
| if ( ev.layerX || ev.layerX == 0) {
| 这个浏览器用layerX
吗?注意:这是针对旧浏览器的 |
| mx = ev.layerX;
| 如果是,用它来设置mx
。。。 |
| my = ev.layerY;
| 。。。和my
|
| } else if (ev.offsetX || ev.offsetX == 0) {
| 试试offset
。注意:这适用于当前的浏览器 |
| mx = ev.offsetX;
| 如果是,用它来设置mx
。。。 |
| my = ev.offsetY;
| 。。。和my
|
| }
| 关闭条款 |
| can.style.cursor = "url('light.gif'),pointer";
| 如果可用,将光标设置为light.gif
;否则使用pointer
|
| mx = mx+10;
| 进行粗略的校正,使光的中心水平地位于灯泡的底部。。。 |
| my = my + 12;
| 。。。垂直地 |
| drawshadowmask(mx,my);
| 在修改后的(mx,my
)调用drawshadowmask
功能 |
| }
| 关闭showshadow
功能 |
| var canvasAx = 0;
| 遮罩常数:左上 x |
| var canvasAy = 0;
| 左上 y |
| var canvasBx = 600;
| 右上 x |
| var canvasBy = 0;
| 右上 y |
| var canvasCx = 600;
| 右下 x |
| var canvasCy = 400;
| 右下 y |
| var canvasDx = 0;
| 左下 x |
| var canvasDy = 400;
| 左下 y |
| var holerad = 50;
| 阴影中孔洞的恒定半径(聚光灯的半径) |
| var grayshadow = "rgba(250,250,250,.8)";
| 暗淡阴影的颜色;注意 0.8 的α值 |
| function drawshadowmask(mx,my) {
| drawshadowmask
功能的表头;参数保持环形孔的中心 |
| ctx.clearRect(0,0,600,400);
| 擦除整个画布 |
| ctx.fillStyle = grayshadow;
| 设置颜色 |
| ctx.beginPath();
| 开始第一个(顶部)路径 |
| ctx.moveTo(canvasAx,canvasAy);
| 移动到左上角 |
| ctx.lineTo(canvasBx,canvasBy);
| 画到右上角 |
| ctx.lineTo(canvasBx,my);
| 绘制到由我的参数指定的垂直点 |
| ctx.lineTo(mx+holerad,my);
| 向左画到洞的边缘 |
| ctx.arc(mx,my,holerad,0,Math.PI,true);
| 画半圆弧 |
| ctx.lineTo(canvasAx,my);
| 向左侧绘制 |
| ctx.lineTo(canvasAx,canvasAy);
| 退回起点 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填写 |
| ctx.beginPath();
| 第二(较低)路径的起点 |
| ctx.moveTo(canvasAx,my);
| 从我的参数指示的左侧点开始 |
| ctx.lineTo(canvasDx,canvasDy);
| 绘制到左下角 |
| ctx.lineTo(canvasCx,canvasCy);
| 绘制到右下角 |
| ctx.lineTo(canvasBx,my);
| 绘制到右边缘的点 |
| ctx.lineTo(mx+holerad,my);
| 向左画到洞的边缘 |
| ctx.arc(mx,my,holerad,0,Math.PI,false);
| 画半圆弧 |
| ctx.lineTo(canvasAx,my);
| 向左边缘绘制 |
| ctx.closePath();
| 关闭路径 |
| ctx.fill();
| 填写 |
| }
| 关闭drawshadowmask
功能 |
| var listener;
| 通过addListener
调用设置的变量;没有再次使用 |
| var map;
| 持有地图 |
| var blatlng;
| 保存基本经纬度对象 |
| var myOptions;
| 保存用于映射的关联数组 |
| var rxmarker = "rx1.png";
| 保存红色 x 图像的文件名 |
| var bxmarker = "bx1.png";
| 保存黑色 x 图像的文件名 |
| function makemap(mylat,mylong) {
| makemap
功能的表头;参数保存地图中心的位置 |
| var marker;
| 将保留为中心创建的标记 |
| blatlng = new google.maps.LatLng(mylat,mylong);
| 构建一个LatLng
对象(API 的特殊数据类型) |
| myOptions = {
| 集合关联数组 |
| zoom: 12,
| 缩放设置(可以是 0 到 18) |
| center: blatlng,
| 中心 |
| mapTypeId: google.maps.MapTypeId.ROADMAP
| 地图类型 |
| };
| 关闭myOptions
阵列 |
| map = new google.maps.Map(document.getElementById("place"), myOptions);
| 调用 API 在指定位置引入地图 |
| marker = new google.maps.Marker(
| 在地图中心放置标记;marker
方法采用一个关联数组作为其参数;注意:也可以使用标记对象的setMap
方法 |
| {
| 关联数组的开始 |
| position: blatlng,
| 设置位置 |
| title: "center",
| 设置标题 |
| icon: rxmarker,
| 设置图标 |
| map: map
| 将 map named 参数设置为名为 map 的变量 |
| }
| 关闭关联数组,它是调用Marker
的参数 |
| );
| 关闭对Marker
方法的调用 |
| listener = google.maps.event.addListener(
| 设置事件处理(以下三个参数);这是一个谷歌地图活动 |
| map,
| 对象,即地图 |
| 'mouseup',
| 特定事件 |
| function(event) {
| 自主功能(直接定义为addListener
中的参数) |
| checkit(event.latLng);
| 用指定的经纬度对象调用checkit
|
| }
| 关闭函数定义 |
| );
| 关闭对addListener
的呼叫 |
| }
| 关闭makemap
功能 |
| function checkit(clatlng) {
| checkit
的功能头;用纬度-经度对象调用 |
| var distance = dist(clatlng,blatlng);
| 调用dist
函数,计算点击位置与底部之间的距离 |
| var marker;
| 将保存新创建的标记 |
| distance = round(distance,2);
| 四舍五入数值 |
| var distanceString = String(distance)+" km";
| 设置distanceString
为显示 |
| marker = new google.maps.Marker(
| 调用Marker
方法,该方法将一个关联数组作为其参数 |
| {
| 关联数组的开始 |
| position: clatlng,
| 预备姿势 |
| title: distanceString,
| 设置标题 |
| icon: bxmarker,
| 将图标设置为黑色 x |
| map: map
| 将关联数组的 map 元素设置为名为 map 的变量的值 |
| }
| 紧密关联数组 |
| );
| 关闭对Marker
方法的调用 |
| var clat = clatlng.lat();
| 提取纬度值 |
| var clng = clatlng.lng();
| 提取经度值 |
| clat = round(clat,4);
| 将值四舍五入到小数点后四位 |
| clng = round(clng,4);
| 将值四舍五入到小数点后四位 |
| document.getElementById("answer").innerHTML =
| 在屏幕上设置文本。。。 |
| "The distance from base to most recent marker ("``+ clat+", "+clng+") is "+String(distance) +" km.";
| 。。。要计算和格式化的信息 |
| can.style.zIndex = 100;
| 将画布设置在顶部 |
| pl.style.zIndex = 1;
| 将pl
(手持地图)设置在下方 |
| }
| 关闭checkit
功能 |
| function round (num,places) {
| 舍入值的函数的标题 |
| var factor = Math.pow(10,places);
| 根据位置数量确定系数 |
| var increment = 5/(factor*10);
| 确定向上或向下舍入的增量 |
| return Math.floor((num+increment)*factor)/factor;
| 进行计算 |
| }
| 关闭round
功能 |
| function dist(point1, point2) {
| dist
(距离)功能的功能头 |
| // spherical law of cosines,``// from``//
http://www.movable-type.co.uk/scripts/latlong.html
| 我的来源的归属;这是标准数学 |
| var R = 6371; // km
| 用于生成答案的因子,以公里为单位 |
| // var R = 3959; // miles
| 注释掉,但是保留以防万一你想用英里给出答案,我在图 4-19 中就是这么做的 |
| var lat1 = point1.lat()*Math.PI/180;
| 将值转换为弧度 |
| var lat2 = point2.lat()*Math.PI/180 ;
| 将值转换为弧度 |
| var lon1 = point1.lng()*Math.PI/180;
| 将值转换为弧度 |
| var lon2 = point2.lng()*Math.PI/180;
| 将值转换为弧度 |
| var d =
| 计算。。。 |
| Math.acos(Math.sin(lat1)*Math.sin(lat2) + Math.cos(lat1)*Math.cos(lat2) * Math.cos(lon2-lon1)) * R;
| 用三角学确定距离 |
| return d;
| 回送结果 |
| }
| 关闭dist
功能 |
| function changebase() {
| changebase
功能的标题 |
| var mylat;
| 将持有新的基准位置纬度 |
| var mylong;
| 将保存新的基准位置经度 |
| for(var i=0;i<locations.length;i++) {
| for
循环确定哪个单选按钮被选中 |
| if (document.f.loc[i].checked) {
| 这个检查过了吗? |
| mylat = locations[i][0];
| 如果是,设置mylat
|
| mylong = locations[i][1];
| 设置mylong
|
| makemap(mylat,mylong);
| 调用makemap
|
| document.getElementById("header").``innerHTML = "Base location (small red x) is "+locations[i][2];
| 更改标题中的文本以显示名称 |
| }
| 关闭if true
子句 |
| }
| 关闭for
回路 |
| return false;
| 返回false
进行当前刷新 |
| }
| 关闭功能 |
| </script>
| 结束script
标签 |
| </head>
| 结束head
标签 |
| <body onLoad="init();">
| 开始body
标签;包括onLoad
来调用init
|
| <header id="header">Base location (small red x) </header>
| 语义头元素 |
| <div id="place" style="width:600px; height:400px"></div>
| div
持有谷歌地图 |
| <div id="answer"></div>
| div
保存点击位置的信息 |
| Change base location: <br/>
| 文本 |
| <form name="f" onSubmit=" return changebase();">
| 改变基底的形式的开始;注意:您需要与script
元素中的 locations 数组协调 |
| <input type="radio" name="loc" /> Springer Nature (Apress Publishers) London, UK<br/>
| 单选按钮选择 |
| <input id=”first” type="radio" name="loc" /> Purchase College<br/>
| 单选按钮选择;给定 ID 设置为打开时检查 |
| <input type="radio" name="loc" /> Kyoto, Japan<br/>
| 单选按钮选择 |
| <input type="submit" value="CHANGE">
| 进行更改的按钮 |
| </form>
| 结束form
标签 |
| </body>
| 结束body
标签 |
| </html>
| 结束html
标签 |
你需要决定你的基地位置。还是那句话,三没什么特别的。你的选择可能会更接近。如果你的基本列表太大,你可以考虑使用<optgroup>
产生一个下拉列表。无论如何,您都需要定义一组位置。每个地点都有两个数字——纬度和经度——以及一串包含名称的文本。一些文本在 HTML 中以 body 元素的形式重复出现。
测试和上传应用程序
这个项目由 HTML 文件和三个图像文件组成。对于我的项目版本,图像文件是灯泡(light.gif
)、红色的 x ( rx1.png
)和黑色的 x ( bx1.png
)。这些图像文件类型没有什么特别的。你喜欢什么就用什么。有人可能会说,我的 x 标记太小了,所以在决定怎么做时,要考虑你的客户。
这个应用程序需要你在线测试,因为这是联系谷歌地图的唯一方式。
摘要
在本章中,您学习了如何执行以下操作:
-
使用谷歌地图应用编程接口。
-
使用画布图形将 Google Maps API 的使用与您自己的 JavaScript 编码结合起来。也就是说,生成一个包含 Google Maps 事件和 HTML5 事件的 GUI。
-
使用控制透明度/不透明度的 alpha 设置进行绘制。
-
改为自定义光标。
-
计算地理点之间的距离。
-
四舍五入十进制数值,以便适当显示。
下一章描述了另一个使用谷歌地图的项目。您将学习如何构建一个应用程序,在该应用程序中,您可以将图片、视频剪辑或图片和音频剪辑的组合与特定的地理位置相关联,然后您将看到当用户在地图上的位置处或附近单击时,如何显示和播放指定的媒体。
五、地图门户(MapPortal):使用谷歌地图访问您的媒体
在本章中,您将学习以下内容:
-
使用 Google Maps API 播放和显示视频、音频和图像
-
动态创建 HTML5 标记
-
从内容描述中分离出程序
-
构建地理游戏
介绍
本章中的项目使用 Google Maps API 作为播放视频、显示图像或播放音频的方式和显示图像,所有这些都基于地理位置。您可以使用此项目作为模型来构建地理区域的研究或商务或度假旅行的报告,或者您可以将其发展为更复杂的地理测验。正如第四章的情况一样,主要的课程是关于将谷歌地图 API 与你自己的 JavaScript 结合使用,特别是呈现图像、音频和视频。本章的示例是一个测验应用程序。我已经获得了媒体,例如视频文件、音频文件和图像文件,并且我已经在代码中定义了媒体和特定地理位置之间的关联。为了让你了解我的意思,在我的项目中,目标位置(在代码中以经纬度坐标给出)和媒体之间的关联如表 5-1 所示。
表 5-1
内容概要
|位置描述
|
媒体
|
| — | — |
| 美国纽约采购学院(学生服务大楼) | 开始位置:无媒体 |
| 基斯科山, NY, 美国 | 埃丝特的照片和她弹钢琴的音频文件 |
| 美国纽约采购学院(自然科学大楼) | 乐高机器人视频 |
| 美国纽约州自由女神像市 | 烟花视频 |
| 日本宫浜 | 大鸟居的照片 |
应用程序可以顺利处理不同类型的媒体。这要归功于 HTML5 的特性,我谦虚地说,还要归功于我的编程。(事实上,谦虚是需要的:我需要对程序做一个小的修改,因为当我从较小的图片更改为较大的图片时,图像尺寸有很大的差异。)媒体信息以及问题和位置存储在单独的文件中。
仍然建议您提供多种视频和音频格式,以确保您的应用程序可以在不同的浏览器中工作。浏览器识别的媒体类型可能会发生变化,因此需要的类型会减少,但目前情况并非如此。
该应用程序是一个简单的测验。它由两个文件组成:mapmediaquiz.html
和mediaquizcontent.js
。mediaquizcontent.js
文件包含连接媒体和位置的信息,也包含问题的文本。
图 5-1 显示了测验的开始屏幕。
图 5-1
测验的开始屏幕
玩家现在试图通过确定位置并点击地图来回答这个问题。图 5-2 显示了当我点击购买校园时会发生什么。这不是一个好的答案,这是程序检测到的。
图 5-2
点击购买学院的结果
注意,在我点击的地方出现了一个小 x,但是它离正确的位置还不够近。我移动地图并再次尝试,图 5-3 显示了点击屏幕的结果,但没有足够接近目标位置。请注意,我已经平移了地图,将它移动到了北方。玩家可以选择得到提示。当我点击提示按钮时,图 5-3 出现了。这是一个非常强烈的暗示,鼓励读者想出一种方法来帮助玩家而不给出答案。
图 5-3
点击提示按钮的结果
当我按照指示点击红色 x 时,图 5-4 显示了结果。还要注意音频控制,它提供了暂停和恢复播放以及改变扬声器音量的方法。在不同的浏览器中,对音频(和视频)的控制会有所不同,但功能是相同的。音频会立即开始播放。还要注意,下一个问题出现了。
图 5-4
图像和音频组合
因为我知道位置在哪里,所以我知道缩小到下一个位置。图 5-5 显示了使用谷歌地图界面实现这一点的结果。音轨继续播放,我仍然可以看到图片。
图 5-5
缩小以准备向南平移
图 5-6 显示了将地图移动到南方,然后放大到采购园区的结果,在采购园区,学生制作的视频显示了一个乐高 Mindstorms 机器人正在穿越一个迷宫。
图 5-6
正确定位(足够接近)乐高机器人,播放视频
下一个地点是自由女神像。请注意,当我单击该位置附近时,会出现一个由 Google 设置的弹出标签。
图 5-7
缩小、向南平移,然后放大以单击自由女神像
最后一个问题需要穿越整个国家,穿越太平洋才能找到 Miyajama(照片的供应商 Takashi 告诉过我)。图 5-8 显示了第一步的结果。
图 5-8
缩小然后放大日本
前一个问题的结果仍然出现。我按下提示按钮,看到图 5-9 所示的内容。
图 5-9
点击提示按钮的结果
当我点击屏幕上提示的位置时,图 5-10 出现。这是日本主要的本地和全球旅游景点。
图 5-10
伟大的鸟居
在这一点上,我需要承认我的原始代码不能处理 Takashi 提供的非常好和非常大的图像。它对于我使用的编码来说太大了,而我使用的编码对于小图像来说已经足够好了。图 5-11 显示了当我将大鸟居问题和宫间的照片包含在我的原始代码中时会发生什么。这确实令人失望。这只是图像的左上角。
图 5-11
显示图像的原始编码结果
我最初的声明:
ctx.drawImage(img1,0,0);
只在画布上画了画的上角。相反,我需要编写 JavaScript 来确定如何将图片缩放到 400x400 的画布上,在执行缩放的同时保持纵横比。下面的方法可以解决这个问题:
var iw = img1.width;
var ih = img1.height;
var aspect = iw/ih;
if (iw>=ih) {
if (iw>400){
tw = 400;
th = 400/aspect;
}
else {
tw = iw;
th = ih;
}
}
else {
if (ih>400){
th = 400;
tw = 400*aspect;
}
else {
th = ih;
tw = iw;
}
}
ctx.drawImage(img1,0,0,iw,ih,0,0,tw,th);
图 5-12 表示代码的动作。原始宽度等于iw
且高度等于ih
的图像被缩小以适合 400 乘 400 的画布。最终尺寸由tw
和th
表示。这段代码产生了如图 5-10 所示的内容。
图 5-12
显示源和目标宽度和高度值关系的图表
有了这个介绍,我将继续讨论项目历史和关键需求。
项目历史和关键要求
Purchase 学院的一名大四学生收集并制作了关于纽约皇后区少数民族社区的视频剪辑和照片,并想找到一种展示这项工作的方式。谷歌地图 API 和 HTML5 中的新工具似乎非常适合这项任务。请记住,该学生只需要在她在高级项目展示中设置的计算机上演示作品,因此不兼容浏览器的问题不是问题。关键需求包括 Google Maps API 所提供的内容。正如您在上一章中所了解的,我们可以编写代码来访问以指定地理位置为中心的地图,设置初始缩放级别,并显示道路或卫星或地形或混合的视图。此外,API 还提供了一种方法来响应查看者单击地图的事件。我们需要一种方法来定义特定的位置,以便与查看者点击对应的位置进行比较。
我为学生设计的第一个系统只使用了视频和图像。我后来决定添加图像和音频组合。应用程序的关键要求是在正确的时间显示和播放指定的媒体,并在适当的时候停止和移除媒体,例如到了下一次演示的时间。
在帮助学生项目后,我想到了改变。第一个是添加了图像和音频组合。我决定不要音频本身。下一个变化是将特定内容从一般编码中分离出来。这反过来需要一种为视频和音频元素动态创建标记的方法。
我一直喜欢游戏和课程,为观众——现在最好描述为玩家或学生——构建一个带有问题或提示的应用程序似乎是一个自然的步骤。玩家通过在地图上找到正确的位置给出答案。像这样的任何应用程序都需要定义一个关于答案的容差。不能期望观众/玩家/学生准确地点击正确的点。
在测试小测验时,我意识到我需要一些方法来帮助玩家通过一个特别难的问题。因为我是老师,所以我决定给玩家看答案,而不是直接跳过问题。然而,正如我前面指出的,您也许能够设计出一种更好的方法来产生提示。
虽然在玩游戏时这一点并不明显,但是问题、地点(答案)和媒体的分离使得我们可以很容易地组织一个完全不同的测验。然而,正如我所指出的,当我决定加入不同大小和形状的图片时,我确实需要做一些调整。
描述了关键需求之后,下一节包含了对可用于构建项目的特定 HTML5 特性的解释。
HTML5、CSS 和 JavaScript 特性
像第四章中的 map maker 项目一样,这些项目是通过结合使用 Google Maps API 和 HTML5 的特性来实现的。这个项目的组合并不复杂。地图停留在窗口的左侧,媒体显示在右侧。我将快速回顾如何访问地图以及如何设置事件处理,然后继续讨论 HTML5、CSS 和 JavaScript 特性,以满足其余的关键需求。
用于地图访问和事件处理的谷歌地图 API
访问 Google Maps API 需要一个引用外部文件的脚本元素。正如在第四章中提到的,使用谷歌地图 API 的第一步是去这个网站获取一个密钥: https://developers.google.com/maps/documentation/javascript/get-api-key
。
访问 API 的代码是修改以下内容,然后将其添加到 HTML 文档中:
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
type="text/javascript"></script>
这个外部脚本元素引入了对象的定义,比如地图和标记,您现在可以使用这些定义将 Google Maps 的功能包含到您的 HTML 和 JavaScript 项目中。
我使用一个名为makemap
的函数建立了到映射的连接。它有两个参数:代表纬度和经度值的两个十进制数字:
function makemap(mylat, mylong)
保存从 0 到 18 的数字的全局变量zoomlevel
和保存图像文件地址的bxmarker and rxmarker
在函数makemap
被调用之前被设置。
引入地图的代码是对google.maps.Map
构造函数方法的调用。它需要两个参数。第一个是 HTML 文档中地图出现的位置。我在文档体中设置了一个 ID 为place
的div
:
<div id="place" style="float: left; width:50%; height:400px"></div>
第二个参数是一个关联数组。以下三个语句将地图的中心位置设置为 Google Maps 经纬度对象,创建关联数组myOptions
,并调用Map
构造函数:
blatlng = new google.maps.LatLng(mylat,mylong);
myOptions = {
zoom: zoomlevel,
center: blatlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("place"), myOptions);
为了完整起见,这里是地图类型的其他设置的截图。这些是地形、混合和卫星。mapTypeId
可以用简单的字符串设置,例如'roadmap'
。图 5-13 显示了请求显示地形的设置的结果——即指示海拔、水、公园和人工建筑区域的颜色:
图 5-13
地形图类型
mapTypeId: google.maps.MapTypeId.TERRAIN
图 5-14 显示了结合卫星和道路地图图像请求混合视图的结果。
图 5-14
混合贴图类型
mapTypeId: google.maps.MapTypeId.HYBRID
对了,混合地图是点击界面上的卫星选项产生的。
图 5-15 显示请求卫星图像的结果。我们可以认为这是纯卫星图像。请注意,主要的高速公路是可见的。
图 5-15
卫星地图类型
mapTypeId: google.maps.MapTypeId.SATELLITE
最后,在您的应用程序中,您可能不希望查看者直接更改地图。您可以通过使用myOptions
数组中的附加选项禁用默认界面来防止用户更改地图。我已经包含了我放在disableDefaultUI
之前的语句,以表明关联数组属性由逗号分隔,最后一个逗号后没有逗号。
mapTypeId: google.maps.MapTypeId.ROADMAP,
disableDefaultUI: true
图 5-16 显示了结果。用户仍然可以平移地图,即移动地图,但是+和–缩放控件以及地图和卫星按钮已被移除。
图 5-16
地图接口已移除
还有两个操作需要makemap
执行。在地图上指定的中心位置放置一个自定义标记,并为单击地图设置事件处理:
marker = new google.maps.Marker({
position: blatlng,
title: "center",
icon: rxmarker,
map: map });
listener = google.maps.event.addListener(map, 'click', function(event) {
checkit(event.latLng);
});
rxmarker
值引用了一个图像对象,它的src
被设置为一个名为rx1.png
的外部文件。这就是在地图中心产生红色小 x 的原因。提醒一下:addListener
是为 Google Maps API 设置事件的方法。addEventListener
是一个为 JavaScript 设置事件的方法。
外部文件中的项目内容
测验使用了三种媒体:video
、picture
和我称之为pictureaudio
。注意:这些是我选择包含在项目中的三种类型的术语。测验的内容是用两个数组指定的,我命名为precontent
和questions
。precontent
数组的每个元素本身是一个五或六个元素的数组。前四个元素对于所有类型都是相同的:纬度、经度、标题和类型。第五或第五和第六指向特定的媒体元素。当前测验的数据,即外部文件的内容是:
var base=
[41.04796,-73.70539,"Purchase College/SUNY"];
var zoomlevel = 13;
var precontent = [
[41.19991,-73.72353,"Esther at home","pictureaudio","estherT","esther.jpg"],
[41.05079,-73.70448,"Lego robot","video","maze"],
[40.68992,-74.04460,"Fire works","video","sfire3"],
[34.298846,132.318359,"Miyajima","picture","miyajima0.JPG"]
];
var questions = [
"Where did Grandma Esther live?",
"Show the Lego robot navigating a maze.",
"Where are great fireworks?",
"Where is the Great Torii?"
];
var maxdistance = 10;
base
、zoomlevel
和maxdistance
变量都是它们看起来的样子。base
是地图的初始中心点。zoomlevel
指定初始缩放。我说初始是因为用户可以使用谷歌地图控件来平移或放大或缩小。maxdistance
是我用来检查用户点击是否足够接近其中一个位置的数字。您需要为您的应用确定合适的距离。
precontent
数组指定了四个位置,以一个图片/音频组合开始,接着是两个视频,再接着是一个图片。如您所料,图片/音频组合数组中的元素包括两条附加信息。仅仅从这段代码来看并不明显,但是esther.jpg
指的是一个图像元素,而estherT
指的是一个音频元素。同样,maze
和sfire3
指的是视频元素,miyajima0.JPG
指的是另一个图像元素。使用两个或更多阵列的布置,如我使用的precontent
和questions
被称为并行结构。我的代码产生了一个名为content
的数组,它被checkit
函数引用(将在下面描述),适当的媒体被呈现。
使用一个script
元素将外部脚本引入主文档。对于mapmediaquiz
,这是
<script type="text/javascript" src="mediaquizcontent.js"> </script>
距离和公差
两个经纬度点之间距离的计算在前一章中已有描述。这里要解释的问题是关于如何进行距离的比较。对于测验应用程序,我需要编写代码来确定 Google 事件处理程序返回的位置是否足够接近指定问题的正确位置。变量maxdistance
保存值,有时称为容差。这里是我的checkit
函数的大部分代码。我已经忽略了switch
语句,一旦确定玩家的猜测足够接近,它会对每种问题类型做不同的处理。
function checkit(clatlng) {
var marker;
var latlnga =new google.maps.LatLng(content[nextquestion][0],content[nextquestion][1]);
var distance = dist(clatlng,latlnga);
eraseold();
marker = new google.maps.Marker({
position: clatlng,
title: "Your answer",
icon: bxmarker,
map: map });
if (distance<maxdistance) {
switch (content[nextquestion][3]) { ...
} // end switch
asknewquestion();
} // end if (distance<maxdistance)
else {
answer.innerHTML= "Not close enough to the location.";
}
}
用于创建 HTML 的正则表达式
正则表达式是描述用于检查和操作的字符串(文本)模式的强大工具。它是一种用于指定模式的完整语言。例如,为了让您对这个大主题有所了解,模式
/⁵[1-5]\d{2}-?\d{4}-?\d{4}-?\d{4}$/
可用于检测万事达卡号码。这些数字从 51 到 55 开始,后面是两个以上的数字,然后是三组四位数。该模式接受破折号,但不要求破折号。^
符号意味着模式必须出现在字符串的开头,而$
意味着它必须到达字符串的结尾。正斜杠(/
)是模式的分隔符,反斜杠是转义符。从头开始解释这种模式如下:
-
^
:从字符串的开头开始。 -
5
:图案必须包含一个 5。 -
[1-5]
:图案必须包含数字 1、2、3、4 或 5 中的一个。 -
\d{2}
:模式必须正好包含两位数字。 -
-?
:模式必须包含 0 或 1 -。 -
\d{4}
:模式必须正好包含四位数字。 -
-?
:模式必须包含 0 或 1 -。 -
\d{4}
:模式必须正好包含四位数字。 -
-?
:模式必须包含 0 或 1 -。 -
\d{4}
:模式必须正好包含四位数字。 -
$
:字符串结束。
万事达卡号码也必须遵守其他规则,你可以研究一下如何进一步验证它们。不要担心,我们将使用比这简单得多的正则表达式(也称为 regex )。
正则表达式的使用早于 HTML。可以在表单中使用正则表达式来指定输入的格式。对于这个应用程序,我们将对字符串使用replace
方法,在一个长字符串中找到一小段特定文本的所有实例,并用其他内容替换它。我使用的一种说法是
videomarkup = videomarkup.replace(/XXXX/g,name);
这样做的是找到字符串XXXX
的所有出现(这就是g
所做的),并用变量name
的值替换它们。
我可以并且可能应该更多地使用正则表达式来验证定义应用程序内容的数据。也许您想在自己的应用程序中尝试一下。
注意
在某种程度上,正确的决定可能是停止使用直接的 JavaScript 数组,包括使用并行结构,而使用 XML 或数据库。我不认为这是在这个应用程序中要求的,但我可能是错的。注意,使用 PHP 之类的语言进行服务器端编程,不管有没有数据库,都提供了一种隐藏数据的方法。
HTML5 标记和定位的动态创建
外部脚本语句引入测验应用程序的信息。现在是解释如何使用这些信息的时候了。init
函数将调用一个名为loadcontent
的函数。该函数调用makemap
在指定的基准位置制作地图。
makemap(base[0],base[1]);
content
数组从一个空数组开始。
var content = [];
顺便说一下,这不同于
var content;
您的代码需要使content
成为一个数组。
然后,它使用一个for
循环来迭代precontent
的所有元素。for
循环的开始将precontent
的第 i 个元素添加到content
数组中。
for (var i=0;i<precontent.length;i++) {
content.push(precontent[i]);
name = precontent[i][4];
下一行是一个switch
语句的头,它使用内部数组中指示类型的元素作为条件。
switch (precontent[i][3]) {
对于video
和pictureaudio
,代码创建一个div
元素并定位它,使其向右浮动。然后,它在div
元素中放置视频或音频的正确标记。那是什么标记?我有一些我称之为虚拟字符串的东西,它们有XXXX
,视频或音频文件的实际名称将放在那里。把这些当做模板。我本来可以只用一个字符串来播放视频,但是它太复杂了,所以我决定用三个和两个来播放音频。这些字符串是
var videotext1 = "<video id=\"XXXX\" loop=\"loop\" preload=\"auto\" controls=\"controls\" width=\"400\"><source src=\"XXXX.webmpv8.webm\" type=\'video/webm\'>";
var videotext2="<source src=\"XXXX.theora.ogv\" type=\'video/ogg\'> <source src=\"XXXX.mp4\" type=\'video/mp4\'>";
var videotext3="Your browser does not accept the video tag.</video>";
var audiotext1="<audio id=\"XXXX\" controls=\"controls\" preload=\"preload\"><source
src=\"XXXX.ogg\" type=\"audio/ogg\" />";
var audiotext2="<source src=\"XXXX.mp3\" type=\"audio/mpeg\" /><source src=\"XXXX.wav\"
type=\"audio/wav\" /></audio>";
注意反斜杠(\
)的使用。它告诉 JavaScript 按原样使用下一个符号,不要将其解释为正则表达式的特殊运算符。这就是屏幕中的引号如何成为 HTML 的一部分。
我的方法要求我确保视频和音频文件的名称遵循这种模式。这意味着 MP4 文件都需要只包含名字,没有内部点。
我使用正则表达式函数 replace 编写代码,从precontent
数组中取出信息,并根据需要将它放入字符串中的任意位置。完整的switch
语句是
switch (precontent[i][3]) {
case "video":
divelement= document.createElement("div");
divelement.style = "float: right;width:30%;";
videomarkup = videotext1+videotext2+videotext3;
videomarkup = videomarkup.replace(/XXXX/g,name);
divelement.innerHTML = videomarkup;
document.body.appendChild(divelement);
videoreference = document.getElementById(name);
content[i][4] = videoreference;
break;
case "pictureaudio":
divelement = document.createElement("div");
divelement.style = "float: right;width:30%;";
audiomarkup = audiotext1+audiotext2;
audiomarkup = audiomarkup.replace(/XXXX/g,name);
divelement.innerHTML = audiomarkup;
document.body.appendChild(divelement);
audioreference = document.getElementById(name);
savedimagefilename = content[i][5];
content[i][5] = audioreference;
imageobj = new Image();
imageobj.src= savedimagefilename;
content[i][4] = imageobj;
break;
case "picture":
imageobj = new Image();
imageobj.src= precontent[i][4];
content[i][4] = imageobj;
break;
}
注意,pictureaudio
案例做了一些杂耍来创建引用新创建的音频元素和图像元素的内容元素。
然而,这还不足以确保视频和音频在所有浏览器上都显示在正确的位置。也就是说,它对一些人有效,但对另一些人无效。我决定准确定位音频和视频——也就是说,绝对定位。这需要在所有视频和音频元素的style
元素中使用以下 CSS:
video {display:none; position:absolute; top: 60px; right: 20px;}
audio {display:none; position:absolute; top: 60px; right: 20px;}
音频的位置是用于音频控制的。
动态创建这些 HTML 元素有一个潜在的问题。你可能还记得,在家庭剪贴画的第二章中,有一段代码确保在对视频做任何事情之前加载了视频。我没有发现测验有任何问题,可能是因为回答问题需要足够的时间。尽管如此,我还是敦促你记住这个问题,并回头参考第二章。
提示按钮
你可以从我的代码中看出,我对于是提供一个提示还是帮助一个已经放弃的玩家很矛盾。在body
元素中,我包括了
<button onClick="giveup();">Hint? </button>
giveup
函数创建一个新的地图。也就是说,它使用makemap
函数在同一个地方构造对不同 Google 地图的访问。它还删除了旧媒体,并将方向放入answer
元素。
function giveup() {
makemap(content[nextquestion][0],content[nextquestion][1]);
eraseold();
answer.innerHTML="Click at red x to finish this question.";
}
构建应用程序并使之成为您自己的应用程序
让应用程序成为你自己的第一步也是关键的一步是决定内容。使用各种媒体内容和各种图片尺寸(以及视频尺寸)有很多好处,但是有一个更简单的设计还是有好处的。
测验应用程序
下面是测验应用程序的快速摘要:
-
init
:执行初始化,包括调用loadcontent
。 -
loadcontent
:使用变量,最重要的是包含在外部脚本元素中的precontent
数组,为媒体创建新的标记。它还援引了makemap
。questions
数组不需要更多的工作。 -
makemap
:引入地图并设置事件处理,包括对checkit
的调用。 -
asknewquestion
:显示问题。 -
checkit
:将点击的位置与该问题的位置进行比较。 -
dist
:计算两个位置之间的距离。 -
giveup
:这是点击提示按钮的响应。一张新地图被带了进来。擦除所有媒体,并引导玩家点击显示的红色 x 附近。 -
eraseold
:删除当前正在播放的视频、音频或图片。
表 5-2 概述了测验应用程序中的功能。描述mapmediabase.html
应用程序的调用/被调用和调用关系的函数表对所有应用程序都是相似的。
表 5-2
功能 在问答应用
|功能
|
调用/调用者
|
打电话
|
| — | — | — |
| init
| 由<body>
标签中的onLoad
属性的动作调用 | loadcontent
,asknewquestion
|
| makemap
| 由loadcontent and giveup
调用 | |
| checkit
| 由makemap
中的addListener
调用调用 | dist
,asknewquestion, eraseold
|
| dist
| 由checkit
调用 | |
| loadcontent
| 由init
调用 | makemap
|
| asknewquestion
| 由init
和checkit
调用 | |
| eraseold
| 由checkit
和giveup
调用 | |
| giveup
| 通过按钮的动作调用 | eraseold, makemap
|
表 5-3 显示了测验应用程序的代码。
表 5-3
地图问答程序的完整代码
|代码行
|
描述
|
| — | — |
| <!DOCTYPE html>
| HTML5 的 Doctype |
| <html>
| html
标签 |
| <head>
| head
标签 |
| <title>Map Quiz </title>
| 完整的标题元素 |
| <meta charset="UTF-8">
| Meta 标签,HTML5 的标准 |
| <style>
| style
标签 |
| header {font-family:Georgia,"Times New Roman",serif;
| 为语义元素 header 设置样式;字体家族将 Georgia 作为第一选择,Times New Roman 作为后备选择,默认 serif 作为下一个后备选择 |
| font-size:20px;
| 相当大的字体 |
| display:block;
| 在前后设置换行符 |
| }
| 关闭样式指令 |
| video {display:none; position:absolute; top: 60px;``right: 20px;
| 视频的样式指令;最初不显示 |
| }
| 关闭视频指令 |
| audio {display:none; position:absolute; top: 60px;``right: 20px;}
| 音频的样式指令;请注意,这是针对控件的;最初不显示 |
| canvas {position:relative; top:60px}
| canvas
元素的样式指令 |
| #answer {position:relative; font-family:Georgia,``"Times New Roman", Times, serif; font-size:16px;}
| 右上方消息的样式指令 |
| </style>
| 结束样式标签 |
| <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"``type="text/javascript"></script>
| Google Maps API 中引入的脚本元素;注意:您需要获得并使用自己的 API 密钥 |
| <script type="text/javascript" src="mediaquizcontent.js">``</script>
| 带入mediaquizcontent.js
中的内容 |
| <script type="text/javascript" charset="UTF-8">
| 开始脚本标记 |
| var listener;
| 用于设置点击地图的谷歌地图事件 |
| var map;
| 用来装地图 |
| var myOptions;
| 保存地图规范的选项数组 |
| var ctx;
| 画布的上下文 |
| var blatlng;
| 基础latlng
对象 |
| var content = [];
| 一个空数组,将由loadcontent
填充 |
| var answer;
| 参考答案、说明 |
| var v;
| 将保存对视频元素的引用 |
| var audioel;
| 将保存对音频元素的引用(本测验只有一个) |
| var videotext1 = "<video id=\"XXXX\" preload=\"auto\" controls=\"controls\" width=\"400\"><source src=\"XXXX.mp4\" type=\'video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"\'>";
| 视频模板的第一部分 |
| var videotext2="<source src=\"XXXX.theora.ogv\" type=\'video/ogg; codecs=\"theora, vorbis\"\'><source src=\"XXXX.webmvp8.webm\" type=\'video/webm; codec=\"vp8, vorbis\"\'>";
| 视频模板的第二部分 |
| var videotext3="Your browser does not accept the video tag.</video>";
| 视频模板的第三部分 |
| var audiotext1="<audio id=\"XXXX\" controls=\"controls\" preload=\"preload\"><source src=\"XXXX.ogg\" type=\"audio/ogg\" />";
| 音频模板的第一部分 |
| var audiotext2="<source src=\"XXXX.mp3\" type=\"audio/mpeg\" /><source src=\"XXXX.wav\" type=\"audio/wav\" /></audio>";
| 音频模板的第二部分 |
| var nextquestion = -1;
| 问题计数器需要在第 0 个之前开始 |
| function init() {
| init
功能的标题 |
| ctx = document.getElementById("canvas").getContext('2d');
| 将参考设置为canvas
|
| answer = document.getElementById("answer");
| 将参考设置为answer
|
| header = document.getElementById("header");
| 将参考设置为header
(显示问题的位置) |
| loadcontent();
| 使用precontent
数组创建内容 |
| asknewquestion();
| 调用函数来提问,从而开始测验 |
| }
| 关闭init
功能 |
| function asknewquestion() {
| asknewquestion
功能的标题 |
| nextquestion++;
| 递增计数器 |
| if (nextquestion<questions.length) {
| 如果还有更多问题 |
| header.innerHTML=questions[nextquestion];
| 显示问题 |
| }
| 关闭if-still-more-questions
子句 |
| else {
| 其他 |
| header.innerHTML="No more questions.";
| 不再显示问题 |
| }
| 关闭else
子句 |
| }
| 关闭asknewquestion
功能 |
| function loadcontent() {
| loadcontent
功能的标题 |
| var divelement;
| 将保存对新创建的div
元素的引用 |
| makemap(base[0],base[1]);
| 为基准位置调用makemap
|
| var videomarkup;
| 视频元素的完整模板 |
| var videoreference;
| 引用每个新创建的视频元素 |
| var audiomarkup;
| 音频元素的完整模板 |
| var audioreference;
| 引用每个新创建的音频元素 |
| var imageobj;
| 图像对象 |
| var name;
| 从 precontent 获得的名称,用于替换模板中的XXXX
|
| var savedimagefilename;
| 保存的图像文件 |
| for (var i=0;i<precontent.length;i++) {
| 对于循环头,通过precontent
|
| content.push(precontent[i]);
| 添加到内容 |
| name = precontent[i][4];
| 提取名字 |
| switch (precontent[i][3]) {
| 根据类型做switch
|
| case "video":
| 视频案例 |
| divelement= document.createElement("div");
| 创建一个div
|
| divelement.style = "float: right;width:30%;";
| 将媒体放在右边 |
| videomarkup = videotext1+videotext2+videotext3;
| 创建完整的模板 |
| videomarkup = videomarkup.replace(/XXXX/g,name);
| 使用name
进行更换 |
| divelement.innerHTML = videomarkup;
| 将结果放入div
|
| document.body.appendChild(divelement);
| 将div
添加到主体中(这样它就可以被访问),但是请注意,在它变得可见之前,它是不可见的 |
| videoreference = document.getElementById(name);
| 对象引用 |
| content[i][4] = videoreference;
| …并使其成为子数组的第四个元素 |
| break;
| 离开switch
(视频案例结束) |
| case "pictureaudio":
| Pictureaudio
案例 |
| divelement = document.createElement("div");
| 创建一个div
|
| divelement.style = "float: right;width:30%;";
| 将媒体放在右边 |
| audiomarkup = audiotext1+audiotext2;
| 创建完整的模板 |
| audiomarkup = audiomarkup.replace(/XXXX/g,name);
| 使用name
进行更换 |
| divelement.innerHTML = audiomarkup;
| 将结果放入div
|
| document.body.appendChild(divelement);
| 将div
添加到主体中(这样它就可以被访问),但是请注意,在它变得可见之前,它是不可见的 |
| audioreference = document.getElementById(name);
| 对象引用 |
| savedimagefilename = content[i][5];
| 将当前第五元素放入savedimagefilename
|
| content[i][5] = audioreference;
| 使audioreference
成为子数组的第五个元素 |
| imageobj = new Image();
| 创建图像对象 |
| imageobj.src= savedimagefilename;
| 使其来源于savedimagefilename
|
| content[i][4] = imageobj;
| 使其成为子数组的第四个元素 |
| break;
| 离开switch
( pictureaudio
完成) |
| case "picture":
| 相框 |
| imageobj = new Image();
| 创建图像对象 |
| imageobj.src= precontent[i][4];
| 设置其src
|
| content[i][4] = imageobj;
| 设置子数组的第四个元素指向图像 |
| break;
| 离开switch
(图片案例完成) |
| }
| 关闭switch
|
| }
| 关闭for
回路 |
| }
| 关闭loadcontent
功能 |
| var rxmarker = "rx1.png";
| 小红 x |
| var bxmarker = “bx1.png”;
| 小黑 x |
| function makemap(mylat,mylong) {
| makemap
功能的标题 |
| var marker;
| 将保存标记对象 |
| blatlng = new google.maps.LatLng(mylat,mylong);
| 使用函数参数创建latlng
对象 |
| myOptions = { zoom: zoomlevel, center: blatlng, mapTypeId: google.maps.MapTypeId.ROADMAP };
| 设置myOptions
数组 |
| map = new google.maps.Map(document.getElementById("place"), myOptions);
| 把地图拿进来 |
| marker = new google.maps.Marker({``position: blatlng, title: "center", icon: rxmarker, map: map });
| 创建标记 |
| listener = google.maps.event.addListener(map, 'click', function(event) {
| 设置单击地图的事件 |
| checkit(event.latLng);
| …事件处理程序是一个调用checkit
的匿名函数 |
| });
| 失去功能并关闭对addListener
的呼叫 |
| }
| 关闭makemap
|
| function eraseold() {
| eraseold
函数的头(代码与前面的例子相同,但现在在一个函数中) |
| if (v != undefined) {
| 有没有一个古老的v
定义? |
| v.pause();
| 暂停一下 |
| v.style.display = "none";
| 从显示中移除 |
| }
| 关闭条款 |
| if (audioel != undefined) {
| 有没有一个古老的audioel
定义? |
| audioel.pause();
| 暂停一下 |
| audioel.style.display = "none";
| 抹掉上次播放的音频的控制 |
| }
| 关闭条款 |
| ctx.clearRect(0,0,300,300);
| 透明画布 |
| }
| 关闭eraseold
功能 |
| function checkit(clatlng) {
| checkit
的标题 |
| var marker;
| 将在玩家设定的位置保持标记(黑色 x) |
| var latlnga =new google.maps.LatLng(content[nextquestion][0],content[nextquestion][1]);
| 为这个问题的答案构建纬度-经度对象 |
| var distance = dist(clatlng,latlnga);
| 计算距离 |
| eraseold();
| 调用该功能擦除当前显示的任何媒体 |
| var
marker = new google.maps.Marker({``position: clatlng,``title: "Your answer",``icon: bxmarker,``map: map });
| 放置标记 |
| if (distance<maxdistance) {
| 用户的点击是否足够接近? |
| switch (content[nextquestion][3]) {
| 打开与此问题相关的类型 |
| case "video":
| 视频案例 |
| answer.innerHTML=content[nextquestion][2];
| 显示答案(标题) |
| ctx.clearRect(0,0,400,400);
| 清理画布 |
| v = content[nextquestion][4];
| 获取视频参考 |
| v.style.display="block";
| 让它可见 |
| v.currentTime = 0;
| 在开始时设置 |
| v.play();
| 播放视频 |
| break;
| 离开switch
(视频案例完成) |
| case "picture":
| 图片案例(将对图片音频案例使用一些编码) |
| case "pictureaudio":
| Pictureaudio
案例 |
| answer.innerHTML=content[nextquestion][2];
| 显示答案 |
| ctx.clearRect(0,0,400,400);
| 清理画布 |
| var img1 = content[nextquestion][4];
| 获取图像 |
| var iw = img1.width;
| 确定宽度 |
| var ih = img1.height;
| 确定高度 |
| var aspect = iw/ih;
| 计算方面 |
| if (iw>=ih) {
| 如果宽度大于高度,那么宽度将是适合的因素 |
| if (iw>400){``tw = 400;``th = 400/aspect;``}
| 如果宽度大于画布,计算目标尺寸 |
| else {``tw = iw;``th = ih;``}
| 如果宽度不大于 400,目标是原始的 |
| }
| 宽度较大时结束 |
| else {
| 否则(高度是关键尺寸) |
| if (ih>400){``th = 400;``tw = 400*aspect;``}
| 如果高度大于 400,计算目标尺寸 |
| else {``th = ih;``tw = iw;``}
| 否则目标尺寸是原始尺寸 |
| }
| 结束外部 else |
| ctx.drawImage(img1,0,0,iw,ih,0,0,tw,th);
| 绘制从整个源到计算目标的图像 |
| if (content[nextquestion][3]=="picture") {
| 如果这是图片… |
| break;}
| 离开switch
|
| else {
| 否则需要显示和播放音频 |
| audioel = content[nextquestion][5];
| 提取元素 |
| audioel.style.display="block";
| 显示控件 |
| audioel.currentTime = 0;
| 在开始时设置 |
| audioel.play();
| 玩 |
| break;
| 离开开关 |
| }
| 关闭其他未显示的图片 |
| }
| 关闭开关 |
| asknewquestion();
| 问一个新问题(仅当用户的猜测足够接近时) |
| }
| 在maxdistance
内关闭 |
| else {
| 其他 |
| answer.innerHTML= "Not close enough to the answer.";
| 显示消息 |
| }
| 关闭else
|
| }
| 关闭checkit
|
| function dist(point1, point2) {
| dist
功能的标题 |
| var R = 6371; // km
| 用于km
的值 |
| // var R = 3959; // miles
| 在代码中保留注释,以便轻松切换到英里 |
| var lat1 = point1.lat()*Math.PI/180;
| 计算弧度 |
| var lat2 = point2.lat()*Math.PI/180 ;
| 计算弧度 |
| var lon1 = point1.lng()*Math.PI/180;
| 计算弧度 |
| var lon2 = point2.lng()*Math.PI/180;
| 计算弧度 |
| var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +``Math.cos(lat1)*Math.cos(lat2) *``Math.cos(lon2-lon1)) * R;
| 使用余弦定律的标准计算 |
| return d;
| 返回距离 |
| }
| 关闭功能 |
| function giveup() {
| giveup
函数的标题(用于提示) |
| makemap(content[nextquestion][0],content[nextquestion][1]);
| 引入以答案为中心的新地图 |
| eraseold();
| 擦除任何旧媒体 |
| answer.innerHTML="Click at red x to finish this question.";
| 显示说明,因为玩家需要点击进行;这给了玩家一种方式来表明他们有新的地图 |
| }
| 关闭giveup
|
| </script>
| 关闭script
元素 |
| </head>
| 关闭head
元素 |
| <body onLoad="init();">
| 正文标签;加载时调用init
|
| <header id="header">Click</header>
| 标题元素 |
| <div id="place" style="float: left;width:50%; height:400px"></div>
| 地图的位置 |
| <button onClick="giveup();">Hint? </button>
| 按钮表示需要帮助 |
| <div style="float: right;width:30%;height:400px">
| 保留其余元素 |
| <div id="answer">Starting location</div>
| 有答案,那是位置的标题 |
| <p> </p>
| 间隔 |
| <canvas id="canvas" width="400" height="400" >
| 帆布 |
| Your browser doesn't recognize canvas
| 旧浏览器的标准 |
| </canvas>
| 关闭canvas
元素 |
| </div>
| 关闭div
|
| </body>
| 关闭body
|
| </html>
| 关闭html
|
测试和上传应用程序
这一章有一个应用,一个地理测验。它由两个文件组成,一个(mapmediaquiz.html
)包含 HTML、CSS 和大部分代码,另一个(mediaquizcontent.js
)包含表示内容的 JavaScript。中的编码。js 文件引用了媒体。我包含了两个视频剪辑的标准视频文件集、单个音频剪辑的标准音频文件和两个图像文件。我用一个手绘的红色小 x*和一个手绘的黑色小 x 来标记地图上的位置,而不是谷歌地图中默认的泪珠形状。我再重复一遍:如果不获取自己的 API 密匙并更改 script
*元素,你将无法运行源代码。您可以并且应该替换您自己的问题、答案(位置)和媒体,但一定要注意大小和形状问题,并检查我的处理以适应任何大的图像文件。
摘要
在本章中,您继续使用 Google Maps API。您学习了如何执行以下操作:
-
管理地理测验。
-
使用问题、位置和媒体的规范来动态创建 HTML 元素。
-
编写 Google Maps API 事件处理程序,以检测用户是否靠近有视频、音频和图像或者只有图像的位置。
-
将媒体内容的定义与节目本身分开。
-
使用正则表达式生成正确的标记。
-
开始和停止媒体的显示和播放。
在下一章中,你将会读到一个叫做添加到 15 的游戏的实现。主要是一个使用数组和字符串的练习。
六、相加到 15(AddTo15)游戏
在本章中,您将学习以下内容:
-
将一个已知的真实世界游戏实现为一个数字程序,其中“计算机”是玩家之一
-
为“计算机”制定战略
-
插入暂停
-
使用数组和字符串
介绍
两人游戏加到 15 要求玩家轮流从数字 1 到 9 中选择,目标是获得三个加起来等于 15 的数字。我第一次看到这个游戏是在纽约市的数学博物馆,在那里它被实现为一个装置,杆上的数字可以从中心移动到玩家这边。如果玩家赢了,就会有灯光和响亮快乐的声音。如果没人赢,会有更短更安静的声音。这个游戏也可以用一副牌中的 1 到 9 张牌或者用纸笔来玩。这个游戏可以被描述为完美知识之一:过去移动的结果和当前的可能性都是可见的。这个游戏相当于一个众所周知的儿童游戏,我将识别这个游戏并向读者证明它的等价性。(你可以在本章的源代码中找到对此的解释。)
对于本章的例子,我选择让程序管理游戏并且扮演一个玩家的角色。这意味着我需要为“计算机”制定一个策略。我的策略很好,但是玩家仍然有可能赢。后来,在我的工作中,我决定我需要在“计算机”移动之前插入一个停顿,以便人类玩家能够像与对手游戏一样体验这个程序。
图 6-1 显示游戏的开启窗口。
图 6-1
打开窗户
图 6-2 显示了玩家和“计算机”各自移动后的结果。
图 6-2
玩家和电脑移动后
最后,我展示了一个截图,图 6-3 ,这似乎是最常见的结果:数字用尽,没有人赢。在我家,这被描述为“猫赢了”,所以我用这个词来描述告诉结果的消息。
图 6-3
比赛以平局结束
这一章是游戏实现的案例研究,包括用户界面和策略的实现。使用数组以及数组间的引用是非常重要的。
游戏的一般要求
Add to 15 程序和其他类似程序的要求是为玩家提供一个相当直观的界面。对手,我称之为“计算机”,尽管我不喜欢拟人化一台机器,需要有一个策略。我在本章中描述的程序有一个相当强硬的策略。我想我战胜了它,但不是经常。这个程序可能的改进是开发出最佳的策略,使“计算机”永远不会输,尽管可能会打成平手,以及其他不太熟练的策略。有了一组选项,一个增强将是给人类玩家一个为他们的对手挑选技能水平的选择。这需要制定一系列策略,也许包括随机行动。
我的这个程序的第一个版本让“计算机”的移动几乎与玩家的移动同时出现。我插入了一个暂停来给游戏一个我认为更好的“触感”。这种做法适用于许多游戏。当我们在现实世界中玩游戏时,我们不会有意识地暂停,但在数字世界中实现游戏可能需要明确地关注时间。
加到 15 非常简单,因此可以列出所有加到 15 的可能组合。事实上,有 8 个,所以我的程序有一个数组,它的元素是保存有效组的数字的字符串,例如“3 5 7”。(实际上,该数组有九个元素,第一个是空的占位符,因此索引可以从 1 开始,而不是从 0 开始。)游戏的管理和“计算机”策略的实施可以使用该数组中的信息来构建。您将不会看到任何将数字相加的代码!
我的程序有另一个有九个元素的数组,每个数组都有指向该数字属于八个列表中的哪些组的元素。我的程序有一个棋盘数组,从所有九个数字开始;玩家的数组,最初为空;和“计算机”的数组,最初也是空的。玩家和计算机的数组有九个元素,第一个元素没有使用,它表示八种组合中每种组合有多少个元素。
程序有两个不变的数组:groups
和occupied
。它也有一个数组,numbers
,在开始时创建,但之后不再更改。有五个数组会发生变化:board
、computer
、player
、pgroupcount
和cgroupcount
。在下一节中,您将看到这些工具的使用。数组的使用和前后指向是这类应用程序的典型特征。有冗余,但它简化了编码。
HTML5、CSS 和 JavaScript
在这一节中,我将解释用于完成 Add to 15 项目需求的特性。
CSS 中的样式
包含九个数字的椭圆形、红色边框、黄色背景元素被动态创建为span
元素。设置外观的 CSS 是
span {
position:absolute;
top:180px;
border-style: solid;
color: red;
border-radius: 25px;
background-color: yellow;
padding: 5px;
cursor: pointer;
}
用绝对定位动态创建这些元素意味着它们可以很容易地从棋盘上移到玩家或“计算机”的部分。使类型span
与div
相反意味着没有强制换行,它们可以彼此相邻。顺便说一下,我区分填充(元素内部)和边距(元素外部)的技巧是考虑填充的单元格。
JavaScript 数组
正如已经讨论过的,一组数组用于游戏的操作。一些数组在 0 索引位置有一个未使用的槽,只是为了使编码更容易。groups
数组保存了总共 15 种可能的组合:
var groups = [
" ", //placeholder, not used
"3 4 8",
"1 5 9",
"2 6 7",
"1 6 8",
"3 5 7",
"2 4 9",
"2 5 8",
"4 5 6"
];
可以被视为冗余信息的occupied
数组使得某些计算变得更加容易。我确实决定忍受零基索引。被占用的数组用于指示从 0 到 9 的每个值属于哪个组。更具体地,第 N 个子阵列中的值对应于保持 N+1 的组的索引。这是被占用的数组,我稍后会给出一些例子。
var occupied = [ //indexed subtracting 1
[2, 4],
[3, 6, 7],
[1, 5],
[1, 6, 8],
[2, 5, 7, 8],
[3, 4, 8],
[3, 5],
[1, 4, 7],
[2, 6]
];
所以数字 1 与数组[2,4]相关联。这表明 1 属于第二组“1 5 9”,第四组“1 6 8”。数字 5 属于第二、第五、第七和第八组。groups
和occupied
数组不变。
board
、player
和computer
数组保存棋盘上的数字,由玩家选择,或者为“计算机”选择。所以最初的声明是
var player = [];
var computer = [];
var board = [1,2,3,4,5,6,7,8,9];
最后两个数组记录玩家和“计算机”距离完成八个组合中的每一个有多近。所以最初的声明是
var pgroupcount = [0,0,0,0,0,0,0,0,0]; //unused first slot
var cgroupcount = [0,0,0,0,0,0,0,0,0]; //unused first slot
在图 6-2 所示的游戏中,“计算机”持有一个 2。查看occupied
数组,2 出现在组 3、6 和 7 中。cgroupcount
应该是[0,0,0,1,0,0,1,1,0]。玩家先选了 5,那么pgrounpcount
数组就是[0,0,1,0,0,1,0,1,1]。
如果玩家然后选择 6,pgroupcount
将是[0,0,1,1,1,1,0,1,2]。在任一组的计数数组中出现 2 表示有机会获胜——如果玩家有 2 个组成员,获得第三个意味着获胜——或者需要阻挡——如果“计算机”有 2 个组成员,它可以在下一步中获胜。我的代码必须确定丢失数字的身份,并检查它是否还在棋盘上(在board
数组中)。
有了这些阵列的基础设施,我可以解释如何响应玩家的移动,生成“计算机”的移动,并确定游戏是赢了还是结束了。
设置游戏
setUpBoard
函数创建代表九个数字的九个span
元素。对这九个元素的引用保存在一个名为numbers
的数组中。为元素设置了一个额外的属性,名为n
,用于保存具体的数字。作为创建过程的一部分,使用一个for
循环来实现,为“点击”事件调用addEventListener
方法,并设置为当玩家点击数字时调用addToPlayer
函数。
一旦创建,该数组就不会改变。改变的是每个元素的位置,由style.left
和style.top
属性表示。
响应玩家的移动
响应玩家移动的关键功能是addToPlayer
。您可以将addToPlayer
函数视为执行内务处理类型的操作,更新各种数组。选择的数字被添加到player
数组中。调用函数take
,从board
数组中移除元素。通过改变style.top
属性来重新定位对应于该数字的span
元素。需要更改player
和board
数组,但不会改变窗口中数字元素的位置。
本地变量holder
被设置为保存包含数字的组。回想一下,occupied
数组是包含该信息的数组的数组。我使用一个for
循环来遍历holder
并更新pgroupcount
。我的代码检查是否有三个计数。这将表明玩家获胜。如果不是这样,addToPlayer
函数在调用computerMove
之前执行一个setTimeout
语句来暂停。
addToPlayer
函数有一行,其中点击一个块的事件被停止:
ev.target.removeEventListener("click",addToPlayer);
这防止了玩家点击已经被玩家拿走的棋子的不良行为。我必须承认,我最初确实注意到了这个问题。
生成计算机移动
暂停后调用computerMove
功能。我在computerMove
和smartChoice
之间分配了任务。computerMove
函数调用smartChoice
函数。computerMove
功能主要执行与addToPlayer
功能类似的内务处理任务。我注意到,虽然我的程序让玩家先玩,但是computerMove
代码确实会检查棋盘是否是空的。
smartChoice
程序使用数组进行以下操作:
-
棋盘上还有哪个数字(在
board
数组中)能让电脑赢得游戏吗? -
假设不可能立即获胜,棋盘上是否有任何数字意味着玩家可以立即获胜?如果是,播放该号码以阻止玩家。
-
假设不需要立即块,是否有任何一个组的一个元素已经被计算机播放,而其他两个元素都没有被播放器播放?如果是这样,请从两个可用号码中选择一个。
-
假设前面的情况都不适用,而 5 是可用的,就拿它。
-
假设前面的情况都不适用,取偶数。
-
从剩下的数字中随机选择一个。
因此,通过提供更好和/或更多的策略来增强计划将涉及到改变smartChoice
。
类似于addToPlayer
中的动作,computermove
函数有一行代码用于删除点击已经播放过的片段的事件处理:
numbers[n-1].removeEventListener("click",addToPlayer);
这防止了玩家点击已经由计算机播放的片段的不良行为。
与addToPlayer
函数一样,computerMove
函数可以确定游戏是以计算机获胜还是平局结束。
构建应用程序并使之成为您自己的应用程序
您可以通过改进策略和/或添加不同的策略来使该应用成为您自己的应用。你可以向前看第九章,在那里描述了名为localStorage
的 HTML5 设施,并思考如何将它融入到游戏中。本章的主要目的是提供使用交叉引用数组的经验。另一个挑战是提供一种无需重新加载就能重复游戏的方法。你可以在第八章中看到拼图变成视频的例子。另一个增强是记录移动的顺序,可能使用localStorage
,这样你可以尝试不同的策略。
表 6-1 列出了所有的功能,并指出它们是如何被调用的以及它们调用了什么功能。
表 6-1
功能 中添加到 15 个项目
|功能
|
调用/调用者
|
打电话
|
| — | — | — |
| init
| 由<body>
标签中的onLoad
属性的动作调用 | setUpBoard
|
| setUpBoard
| init
| |
| computerMove
| 由setTimeout
的动作调用,在addToPlayer
中调用 | smartChoice
,take
|
| smartChoice
| computerMove
| |
| take
| addToPlayer
,computerMove
| |
| addToPlayer
| 由点击事件的动作addEventListener
调用 | take
|
表 6-2 显示了 Add to 15 游戏的代码,每一行都有注释。
表 6-2
添加到 15 应用程序的完整代码
| 代码行 | 描述 | | `` | 页眉 | | `` | 开始`html`标签 | | `` | 开始`head`标签 | | `Player against Computer
` | 页眉 | | `Player goes first: click on number. First to have a set of 3 adding to 15 wins. Reload for new game.` | 说明 | | `
` | 间隔 | | `Computer` | 计算机区域 | | `
` | 间隔 | | `
` | 水平标尺 | | `Board` | 电路板区域 | | `
` | 间隔 | | `
` | 水平规则 | | `Player` | 玩家区 | | `
` | 间隔 | | `
` | 水平规则 | | `
测试和上传应用程序
这个应用程序的源材料只包含一个 HTML 文档。原始资料包含一个关于 Add to 15 游戏问题的 Word 文档。
摘要
在这一章中,你研究了如何通过给单人游戏者提供一个对手并管理游戏来实现双人游戏。您了解并获得了以下经验:
-
定义和操作数组
-
如何为玩家建立一个用户界面,包括为点击“棋盘”上的物体设置事件和编程暂停
-
对运动员的不良行为采取预防措施
在下一章,我们将进入空间迷人的折纸世界。我们探索如何使用线条画、视频剪辑和在画布上绘制照片来制作一个会说话的鱼的折纸模型。这些技术可以应用于不同类型的方向。