A map looks complicated. It is not. A map is a list of numbers and one small function that turns those numbers into points on a screen. In nine steps you will build a real map application: real places, zoom in and out, drag to move, and tools to draw squares, circles and lines that tell you how many kilometres across they are. 地图看起来很复杂,其实不是。地图就是一串数字,加上一个把这些数字变成屏幕上的点的小函数。 九步之后你会做出一个真正的地图应用:真实地点、可缩放、可拖动,还有画方形、圆形和线的工具,并告诉你它们有多少公里。 แผนที่ดูซับซ้อน แต่จริง ๆ ไม่เลย แผนที่คือรายการตัวเลข กับฟังก์ชันเล็ก ๆ หนึ่งอันที่เปลี่ยนตัวเลขเป็นจุดบนจอ ในเก้าขั้นตอน คุณจะสร้างแอปแผนที่จริง: สถานที่จริง ซูมเข้าออก ลากเลื่อน และเครื่องมือวาดสี่เหลี่ยม วงกลม เส้น ที่บอกระยะเป็นกิโลเมตร
← JavaScript Lab ← Make a space game The same map in Python ▶ Use it now
Thirteen real places in Chiang Mai, the old city moat, and the Ping river. You can zoom, drag, and draw on top. Every shape you draw is stored as latitude and longitude, so it stays on the same street when you zoom. Lines and circles tell you the distance in kilometres.
清迈的十三个真实地点、古城护城河,还有滨河。可以缩放、拖动,并在上面画图。你画的每个图形都用经纬度保存,所以缩放时它还留在同一条街上。线和圆会告诉你多少公里。
สถานที่จริงในเชียงใหม่สิบสามแห่ง คูเมืองเก่า และแม่น้ำปิง ซูมได้ ลากได้ และวาดทับได้ รูปที่คุณวาดถูกเก็บเป็นละติจูดกับลองจิจูด จึงอยู่ที่ถนนเดิมเมื่อซูม เส้นและวงกลมบอกระยะทางเป็นกิโลเมตร
Same rules as before. One file called
index.html, one <canvas>, one <script> at
the bottom. Press ✓ done when a step works.
规则和上次一样:一个 index.html 文件、一块 <canvas>、
底部一个 <script>。一步做成功了就按 ✓ done。
กติกาเหมือนเดิม: ไฟล์ index.html หนึ่งไฟล์ <canvas> หนึ่งอัน
<script> หนึ่งอันไว้ล่างสุด กด ✓ done เมื่อขั้นตอนสำเร็จ
Make a folder called
map and a file called index.html. This is the same skeleton as the
space game. If you built that, you can copy it and change the title.
新建一个文件夹叫 map,里面一个 index.html。
骨架和太空游戏一样。做过那个的话,直接复制过来改标题就行。
สร้างโฟลเดอร์ชื่อ map และไฟล์ index.html
โครงเหมือนเกมอวกาศ ถ้าทำอันนั้นแล้ว คัดลอกมาเปลี่ยนหัวเรื่องได้เลย
charset="UTF-8". Without it, Thai names like
วัดพระสิงห์ turn into strange boxes. This one line is why the page can hold three
languages at once.
一定要保留 charset="UTF-8":没有它,วัดพระสิงห์ 这样的泰文会变成奇怪的方块。
就是这一行让页面能同时装下三种语言。
อย่าลบ charset="UTF-8" ถ้าไม่มี ชื่อไทยอย่าง วัดพระสิงห์ จะกลายเป็นกล่องประหลาด
บรรทัดเดียวนี้แหละที่ทำให้หน้าเว็บเก็บสามภาษาพร้อมกันได้Every place on Earth has two numbers. Latitude says how far north. Longitude says how far east. Chiang Mai is about 18.79 north and 98.99 east. 地球上每个地方都有两个数字。纬度说往北多远,经度说往东多远。 清迈大约是北纬 18.79、东经 98.99。 ทุกที่บนโลกมีตัวเลขสองตัว ละติจูดบอกว่าเหนือแค่ไหน ลองจิจูดบอกว่าตะวันออกแค่ไหน เชียงใหม่อยู่ประมาณ 18.79 เหนือ และ 98.99 ตะวันออก
| place | latitude | longitude |
|---|---|---|
| Tha Phae Gate | 18.78776 | 98.99327 |
| Wat Don Chan | 18.76021 | 99.03257 |
| Chiang Mai Airport | 18.76684 | 98.96488 |
| Beijing 北京 | 39.90571 | 116.39130 |
| Denver, USA | 39.74194 | −105.07103 |
Two more useful facts. One degree of latitude is always about 111 kilometres. One degree of longitude is 111 km at the equator but gets shorter as you go north, because the lines meet at the poles. In Chiang Mai it is about 105 km. 还有两个有用的事实:一度纬度总是约 111 公里。一度经度在赤道是 111 公里, 越往北越短,因为经线在两极汇合。在清迈大约是 105 公里。 อีกสองข้อที่มีประโยชน์: หนึ่งองศาละติจูดยาวประมาณ 111 กิโลเมตร เสมอ ส่วนหนึ่งองศาลองจิจูดยาว 111 กม. ที่เส้นศูนย์สูตร แต่สั้นลงเมื่อขึ้นเหนือ เพราะเส้นบรรจบกันที่ขั้วโลก ที่เชียงใหม่ยาวประมาณ 105 กม.
This is the whole map, in two
functions. toScreen turns a place into x and y. toMap turns x and
y back into a place. Everything else in this tutorial only uses these two.
整张地图就在这两个函数里。toScreen 把地点变成 x 和 y,
toMap 把 x 和 y 变回地点。这个教程剩下的部分只用这两个函数。
ทั้งแผนที่อยู่ในสองฟังก์ชันนี้ toScreen เปลี่ยนสถานที่เป็น x และ y
toMap เปลี่ยน x และ y กลับเป็นสถานที่ ที่เหลือทั้งบทเรียนใช้แค่สองอันนี้
view.lat - lat, not lat - view.lat. Get this the wrong way round
and your city appears upside down — a beautiful bug, and a very common one.
为什么 y 那行是反的:地图上北在上;画布上 y 越往下越大。
所以纬度要翻过来:写 view.lat - lat,不是 lat - view.lat。
写反了城市就上下颠倒——很漂亮的 bug,也非常常见。
ทำไมบรรทัด y ถึงกลับด้าน บนแผนที่ ทิศเหนืออยู่ข้างบน แต่บนแคนวาส y เพิ่มขึ้นเมื่อลงล่าง
ละติจูดจึงต้องกลับด้าน: view.lat - lat ไม่ใช่ lat - view.lat
ถ้าเขียนกลับกัน เมืองจะกลับหัว — บั๊กที่สวยงามและพบบ่อยมากtoMap(toScreen(18.78776, 98.99327).x, toScreen(18.78776,
98.99327).y)
继续之前先测一下:放一个地点进去得到 x、y,再把这个 x、y 放回去,
必须得到同一个地点。在控制台(F12)里输入上面那行。
ทดสอบก่อนไปต่อ ใส่สถานที่เข้าไปให้ได้ x กับ y แล้วใส่ x, y กลับเข้าไปใหม่
ต้องได้สถานที่เดิม พิมพ์บรรทัดข้างบนในคอนโซล (F12)Now the data. An array of objects: many places, and each place is one thing with parts. Then one loop draws them all. 现在是数据:一个数组装很多对象——很多地点,每个地点是一个有各部分的东西。 然后一个循环把它们全画出来。 ทีนี้คือข้อมูล: อาร์เรย์ของอ็อบเจกต์ — หลายสถานที่ และแต่ละที่คือสิ่งเดียวที่มีหลายส่วน แล้วลูปเดียววาดทั้งหมด
COLOURS[p.kind]
looks up the colour by the word in the object. Adding a new kind of place means adding one
line to COLOURS — you never touch the drawing code again.
颜色来自数据,不是来自 if:COLOURS[p.kind] 用对象里的那个词去查颜色。
以后加一种新地点,只要在 COLOURS 里加一行,绘图代码永远不用再动。
สีมาจากข้อมูล ไม่ใช่จาก if COLOURS[p.kind] ค้นสีจากคำในอ็อบเจกต์
เพิ่มประเภทสถานที่ใหม่ก็แค่เพิ่มหนึ่งบรรทัดใน COLOURS ไม่ต้องแตะโค้ดวาดอีกเลยA line on a map is just an array of places joined in order. The old city moat is four corners joined into a square. The Ping river is seven points joined into a curve. 地图上的线只是一串按顺序连起来的地点。古城护城河是四个角连成方形, 滨河是七个点连成一条曲线。 เส้นบนแผนที่คืออาร์เรย์ของสถานที่ที่ต่อกันตามลำดับ คูเมืองเก่าคือสี่มุมต่อเป็นสี่เหลี่ยม แม่น้ำปิงคือเจ็ดจุดต่อเป็นเส้นโค้ง
moveTo lifts the pen and
puts it down somewhere. lineTo drags it. The i === 0 test is what
decides which one — one line of code that turns any list of places into a road, a river or
a wall.
moveTo 一次,lineTo 很多次:moveTo 是抬笔落到某处,
lineTo 是拖着走。i === 0 决定用哪个——这一行代码就能把任何地点列表变成路、河或城墙。
moveTo ครั้งเดียว lineTo หลายครั้ง moveTo คือยกปากกาไปวางที่จุดหนึ่ง
lineTo คือลากไป การตรวจ i === 0 เป็นตัวตัดสิน — โค้ดบรรทัดเดียวเปลี่ยนลิสต์สถานที่ใด ๆ ให้เป็นถนน แม่น้ำ หรือกำแพงYou already wrote zoom. It is
view.zoom, sitting inside ppdX and ppdY. Change that
one number, draw again, and the whole map grows. Nothing else needs touching.
缩放你已经写好了——就是 ppdX 和 ppdY 里的 view.zoom。
改这一个数字、重画一遍,整张地图就变大了。别的什么都不用动。
คุณเขียนซูมไว้แล้ว มันคือ view.zoom ที่อยู่ใน ppdX และ ppdY
เปลี่ยนตัวเลขตัวเดียว วาดใหม่ แล้วทั้งแผนที่ก็ใหญ่ขึ้น ไม่ต้องแก้อะไรอีก
zoom * 1.5 feels the same at every
level. zoom + 1 would be a giant jump when you are zoomed out and almost
nothing when you are zoomed in. Zoom is multiplication — every map app in the world works
this way.
用乘,不要用加:zoom * 1.5 在任何级别手感都一样;
zoom + 1 在缩到很小时是一大跳,在放到很大时几乎没变化。缩放是乘法——
全世界的地图应用都这么做。
ใช้คูณ ไม่ใช่บวก zoom * 1.5 ให้ความรู้สึกเหมือนกันทุกระดับ
ส่วน zoom + 1 จะกระโดดมากตอนซูมออก และแทบไม่ขยับตอนซูมเข้า การซูมคือการคูณ — แอปแผนที่ทั้งโลกทำแบบนี้Three events: press, move, let go. Remember where the press started, then move the view by the difference. One set of listeners covers the mouse and the finger, because we use pointer events. 三个事件:按下、移动、松开。记住按下的位置,然后按差值移动视图。 因为用的是 pointer 事件,一套监听器同时管鼠标和手指。 สามอีเวนต์: กด เลื่อน ปล่อย จำตำแหน่งที่กดไว้ แล้วเลื่อนมุมมองตามผลต่าง ชุดเดียวใช้ได้ทั้งเมาส์และนิ้ว เพราะเราใช้ pointer events
touch-action: none to the canvas in your CSS.
Without it, dragging on a phone scrolls the page instead of moving the map, and the map
feels broken. One CSS line fixes it.
在 CSS 里给画布加 touch-action: none:
没有它,在手机上拖动会滚动页面而不是移动地图,地图会像坏了一样。一行 CSS 就解决。
ใส่ touch-action: none ให้แคนวาสใน CSS
ถ้าไม่มี การลากบนมือถือจะเลื่อนหน้าเว็บแทนที่จะเลื่อนแผนที่ แล้วแผนที่จะเหมือนเสีย แก้ด้วย CSS บรรทัดเดียวNow the part you asked for. One
variable remembers which tool is chosen. Press starts a shape, move changes it, let go
saves it into the shapes array.
现在是重点部分。一个变量记住选了哪个工具:按下开始一个图形,移动改变它,松开把它存进 shapes 数组。
มาถึงส่วนที่รอ ตัวแปรหนึ่งตัวจำว่าเลือกเครื่องมือไหน กดเริ่มรูป เลื่อนเปลี่ยนรูป ปล่อยแล้วเก็บลงอาร์เรย์ shapes
a and b are latitude and longitude, not x and y. So when
you zoom or drag, the shape is drawn again from its real place on Earth and stays on the
same street. Store pixels instead and every shape slides off the map the moment you zoom.
这是整页最重要的概念。看看存的是什么:a 和 b 是经纬度,
不是 x、y。所以缩放或拖动时,图形会按它在地球上的真实位置重画,还留在同一条街上。
如果存的是像素,一缩放所有图形就都滑走了。
นี่คือแนวคิดสำคัญที่สุดในหน้านี้ ดูว่าเก็บอะไรไว้: a และ b
คือละติจูดกับลองจิจูด ไม่ใช่ x กับ y เวลาซูมหรือลาก รูปจึงถูกวาดใหม่จากตำแหน่งจริงบนโลกและอยู่ที่ถนนเดิม
ถ้าเก็บเป็นพิกเซล รูปทุกอันจะเลื่อนหลุดทันทีที่ซูมMath.min and Math.abs for the square?
Because people drag in all four directions. Drag up and to the left and the width would be
a negative number. Two small functions make every direction work.
方形为什么要用 Math.min 和 Math.abs?
因为人会往四个方向拖。往左上拖,宽度就成了负数。两个小函数让四个方向都能用。
ทำไมสี่เหลี่ยมต้องใช้ Math.min กับ Math.abs
เพราะคนลากได้ทั้งสี่ทิศ ลากขึ้นไปทางซ้ายแล้วความกว้างจะติดลบ ฟังก์ชันเล็ก ๆ สองตัวทำให้ทุกทิศใช้ได้The earth is round, so you cannot just subtract the numbers. This is the haversine formula. 6371 is the radius of the earth in kilometres. It gives the straight line, like a bird flies — the road is always longer. 地球是圆的,所以不能直接相减。这是 haversine(半正矢)公式, 6371 是地球半径(公里)。它算的是直线距离,像鸟飞的那样——走路的距离总是更长。 โลกกลม จึงลบตัวเลขตรง ๆ ไม่ได้ นี่คือสูตร haversine ส่วน 6371 คือรัศมีโลกเป็นกิโลเมตร มันให้ระยะเส้นตรงอย่างที่นกบิน — ระยะถนนยาวกว่าเสมอ
localStorage only holds text. That is why
JSON.stringify turns your array into text on the way in, and
JSON.parse turns it back on the way out. Forget one of them and you will save
the words [object Object] for ever.
localStorage 只能存文本:所以存进去时用 JSON.stringify 把数组变成文本,
取出来时用 JSON.parse 变回来。少写一个,你就会永远存着 [object Object] 这几个字。
localStorage เก็บได้แต่ข้อความ จึงต้องใช้ JSON.stringify แปลงอาร์เรย์เป็นข้อความตอนเก็บ
และ JSON.parse แปลงกลับตอนอ่าน ลืมอันใดอันหนึ่งแล้วคุณจะเก็บคำว่า [object Object] ไว้ตลอดกาลThis is the map from the nine steps, running on this page. Choose a tool, then drag on the map. Move pans the map, and the other three draw. Use the wheel or the buttons to zoom. Your shapes are saved on this device, so they are still here tomorrow.
这就是九个步骤做出来的地图,正跑在这一页上。先选工具,再在地图上拖动:Move 是平移,另外三个是绘图。用滚轮或按钮缩放。你画的图形保存在这台设备上,明天还在。
นี่คือแผนที่จากเก้าขั้นตอน กำลังทำงานบนหน้านี้ เลือกเครื่องมือแล้วลากบนแผนที่ Move คือเลื่อนแผนที่ อีกสามอันคือวาด ใช้ล้อเมาส์หรือปุ่มเพื่อซูม รูปที่วาดถูกบันทึกไว้ในเครื่องนี้ พรุ่งนี้ก็ยังอยู่
Five changes, in order of difficulty. The first one takes one minute. 五个改动,按难度排序。第一个只要一分钟。 ห้าการเปลี่ยนแปลง เรียงตามความยาก อันแรกใช้เวลาหนึ่งนาที
PLACES.
加上你家:打开地图网站,右键点你家那条街,复制那两个数字,在 PLACES 里加一行。
เพิ่มบ้านคุณเอง: เปิดเว็บแผนที่ คลิกขวาที่ถนนบ้านคุณ คัดลอกตัวเลขสองตัว แล้วเพิ่มหนึ่งบรรทัดใน PLACESthai. One
more fillText line under the English one.
同时显示泰文名:每个地点对象里已经有 thai 了。在英文那行下面再加一行 fillText。
แสดงชื่อไทยด้วย: อ็อบเจกต์สถานที่มี thai อยู่แล้ว เพิ่ม fillText อีกบรรทัดใต้ชื่ออังกฤษcolour to the shape object
when you make it, and read it in drawShape.
给图形上色:创建图形对象时加一个 colour,在 drawShape 里读出来。
ใส่สีให้รูปที่วาด: เพิ่ม colour ตอนสร้างอ็อบเจกต์รูป แล้วอ่านค่าใน drawShapepointerdown, loop through
PLACES and find the one whose screen point is nearest the click.
点击图钉显示名字:在 pointerdown 里遍历 PLACES,
找到屏幕位置离点击最近的那个。
คลิกหมุดเพื่อดูชื่อ: ใน pointerdown วนลูป PLACES หาอันที่จุดบนจออยู่ใกล้จุดคลิกที่สุดdistanceKm between each pair. That is exactly what the Python
version does.
画一条三座寺庙的步行路线:拿护城河那段代码,换成三个地点名,
再把每一对之间的 distanceKm 加起来。Python 版做的正是这件事。
วาดเส้นทางเดินสามวัด: เอาโค้ดคูเมืองมา ใส่ชื่อสถานที่สามแห่ง แล้วบวก distanceKm ของแต่ละคู่
เวอร์ชัน Python ทำแบบนี้เป๊ะ ๆtoScreen is the whole idea;
everything else uses it.
地图 = 两个数字 + 一个函数:toScreen 就是全部思路,其他都在用它。
แผนที่คือตัวเลขสองตัวกับฟังก์ชันเดียว toScreen คือแนวคิดทั้งหมด ที่เหลือใช้มันzoom * 1.5, never zoom + 1.
缩放用乘法:zoom * 1.5,绝不是 zoom + 1。
การซูมคือการคูณ zoom * 1.5 ไม่ใช่ zoom + 1Thank you for mapping your city with us. Put your own house on it tonight — that is when it becomes yours. See you in the next tutorial. 感谢和我们一起绘制你的城市。今晚就把你家加上去——那一刻它才真正属于你。下个教程见。 ขอบคุณที่ทำแผนที่เมืองของคุณไปด้วยกัน คืนนี้ลองใส่บ้านคุณลงไป — ตอนนั้นแหละมันถึงเป็นของคุณจริง ๆ แล้วเจอกันบทเรียนหน้า