สวัสดีครับ และยินดีต้อนรับสู่บทความแรกในชุดของ MapOnMobile ซึ่งไอเดียของซีรี่ส์นี้ มาจากการที่เมื่อเร็วๆ นี้ ผมได้พัฒนา Application ให้กับบริษัท ซึ่งต้องการแสดงผลแผนที่บน WPF โดยที่ไม่ใช้ Browser เพื่อที่จะแสดงผลข้อมูลต่างๆ ตามใจชอบอย่างเต็มที่โดยไม่ต้องพึ่ง API และข้อจำกัดของตัว Browser ซึ่งผมคิดว่า แนวคิดนี้ น่าจะเอามาใช้กับการแสดงผลแผนที่บน Windows Mobile ได้เป็นอย่างดีเลยทีเดียว เพราะเรามีข้อจำกัดในการแสดงผลอย่างมาก และแน่นอนว่าการพึ่งพาให้ Browser บน Windows Mobile ทำการแสดงผลแผนที่ให้เรานั้น คงอาจจะเป็นไปไม่ได้เลย
สำหรับบทความในชุดนี้ผมจะพยายามเขียนให้ครอบคลุมตั้งแต่แนวคิดของแผนที่ การออกแบบโปรแกรม และการใช้ Thread และการจัดการบริหารแรม ซึ่งมีเพียงน้อยนิดบน WM ซึ่งหวังว่าจะเป้นประโยชน์กับทั้งผู้เริ่มต้น และผู้ที่เขียนเป็นแล้วครับ
แต่ก่อนที่เราจะไปกันต่อถึงการเขียนโค๊ด ในบทความแรกนี้ มีเรื่องที่เราจะต้องเข้าใจให้ตรงกันก่อนครับ นั่นคือหลักการต่างๆ ที่เกี่ยวของกับโปรเจคของเรา นั่นคือแนวคิดและหลักการการนำแผนที่มาแสดงผลบนโปรแกรมครับ
รู้จักกับ Mecrator Projection
ภาพจาก WikiPedia
เราทราบกันดีว่าโลกเรานั้นกลม แต่ว่าหน้าจอเรามันแบน จะทำอย่างไรจึงจะสามารถแสดงผลโลกกลม บนหน้าจอแบน หรือแผ่นกระดาษได้? ก็คงหนีไม่พ้นกระบวนการที่เรียกว่า Projection นั่นเองครับ หลักการของ Mecrator Projection นั้น มีการใช้กันมาตั้งแต่ปี 1569 เพื่อประโยชน์ในการเดินเรือ เพราะว่ามันสามารถแสดงเส้นทางการเดินเรือที่สำคัญ เป็นเส้นตรงได้ ต่างจากวิธีอื่นๆ โดยแนวคิดของ Mecrator Projection นั้น คือ การเอาแผนที่ ซึ่งเป็นกระดาษแผ่นใหญ่ๆ ม้วนรอบโลกเอาไว้ โดยให้กึ่งกลางของแผนที่แผ่นนี้ แตะกับเส้นศุนย์สูตรพอดี และดึงเอาภาพจากบนโลก มาแสดงผลบนแผนที่แผ่นนี้ครับ
ลองนึกถึงการพยายามพิมพ์ภาพแผนที่โลกบนแผนพลาสติกที่มันยืดๆ ได้และมีความสูงเท่ากับลูกโลกในห้องสมุด เพื่อเอาไปแปะลงบนลูกโลกที่ว่า แน่นอนว่า เราไม่สามารถพิมพ์ภาพด้วยสัดส่วนที่ถูกต้องได้ครับ เพราะตอนที่เราเอาแผนพลาสติกนาบลงไป โดยเริ่มจากบริเวณเส้นศุนย์สูตร เราก็ต้องยืดแผ่นพลาสติกทั้งด้านบนและด้านล่าง เพื่อให้ขอบด้านบน และขอบด้านล่าง สามารถไปปิดขั้วโลกเหนือ และขั้วโลกใต้ได้มิด
การใช้ Mecrator Projection จึงทำให้เกิดความผิดเพี้ยนไปบ้างในเรื่องของขนาดของวัตถุต่างๆ วงกลมสีแดงในภาพด้านข้างนี้ แสดงให้เห็นถึงอัตราความยืด ของวัตถุโดยเฉลี่ลยครับ
ข้อจำกัดอีกอย่างหนึ่ง คือแผนที่แบบ Mecrator นี้ ไม่สามารถแสดงผลบริเวณขั้วโลกได้ครบครับ เนื่องจากความยืดของวัตถในบริเวณดังกล่าวจะเพิ่มเข้าใกล้ค่าอนันต์ (มีลิมิตเป็นอินฟินิตี้) ทำให้เราไม่สามารถวาดแผนที่ออกมาได้ จึงไม่เหมาะกับการใช้งานด้านการบินครับ เพราะว่าไม่ครอบคลุมทั้งโลก และมีหลายสายการบินที่มีเส้นทางบินผ่านขั่วโลกเหนือครับ
ระบบแกนของแผนที่โลก
ผมขอสรุปเป็นข้อๆ ตามนี้ครับ จะได้ไม่เยิ่นเย้อ ดูภาพประกอบได้นะครับ
- หมายเหตุ: โลกไม่ได้กลมดิก แต่ผมจะขอเรียกว่า โลกกลมนะครับ
- Longtitude คือ แกนนอน หรือแกน X ครับ ถ้าผมบอกว่าโลกกลม แน่นอนว่าทุกคนคงจำได้ว่า วงกลมมี 360 องศา แต่ว่าค่าของ Lon. นั้น จะไม่ได้อิงค่าเป็นวงกลมแบบนั้น เนื่องจากเราจะแบ่งโลกเป็นวองซีกตามที่เราชอบเรียกกันว่า ตะวันออก กับ ตะวันตกครับ โดย –180 องศา คือ ตะวันตกสุด ของโลก (เลยอเมริกาไปหน่อย) และ 180 คือตะวันออกสุดของโลก (ซึ่งก็จะไปชนกับฝั่งตะวันตกสุดของโลกพอดี) โดยจุดกึ่งกลางของโลกคือ 0 องศาครับ
ในภาษาพูด เราจะพูดว่า องศาตะวันตก หรือ องศาตะวันออกครับ ถ้าผมบอกว่า 64 องศาตะวันตก ก็คือ –64 และ 86 องศาตะวันออก ก็คือ 86 องศานั่นเองครับ
- Latitude คือแกนตั้ง หรือแกน Y ครับ ถ้าลองนึงภาพโลกในมุมตัดขวาง โลกก็จะกลมเหมือนกัน แต่ว่าผิวของโลกในแนวตั้ง จะมีแค่ครึ่งเดียวเท่านั้น นั่นคือ 180 องศา ต้องลองนึกภาพตอนเราเอาแผนที่พลาสติกห่อโลกครับ แผนที่ที่เราพิมพ์มา มันสามารถหุ้มโลกได้รอบแล้วในแนวนอน พอเราห่อโลกไปแล้ว ตัวแผนที่ก็จะคลุมโลกได้มิดพอดี เหมือนเอากระดาษไปห่อกระป๋องน้ำอัดลมนั่นเอง
เช่นเดียวกับ Longtitude ครับ เราได้แบ่งโลกเป็นซีกโลกเหนือ ซีกโลกใต้เหมือนกัน นั่นก็คือ –90 ถึง 90 และภาษาพูดก็จะเรียกว่า องศาเหนือ องศาใต้เช่นเดียวกัน แต่ด้วยระบบ Mecrator Projection เราจะสามารถแสดงผลได้แค่ ±85.05113 องศาเท่านั้น หรือถ้าต้องการคำนวณ สามารถใช้สูตรนี้ได้ครับ
- การเขียนพิกัดในภาษาเขียน เรามักจะเขียนในรูปของ DMS หรือ องศา นาที วินาที แทนที่จะเขียนเป็นจุดทศนิยมตามปกติ พร้อมกับมี NSWE กำกับเพื่อบอกซีกโลกครับ เช่น 10”10’10N 10”12’19E หมายถึง 10”10’10 องศาเหนือ 10”12’19 องศาตะวันออก
แน่นอนว่าผู้ใช้ทั่วไป มักจะได้พิกัดมาในรูปนี้ ดังนั้น ในการคำนวณ เราจะต้องแปลงเลขพวกนี้ให้อยู่ในรูปทศนิยมเสียก่อน ซึ่งมีสูตรการแปลงดังนี้ครับ
public double ToDecimal()
{
return this.Degree + (this.Minute / 60d) + (this.Second / 3600);
}
ระบบ Tile System (อ้างอิงจาก Virtual Earth)
เนื่องจากว่า ขนาดของแผนที่โลกนั้น มีขนาดใหญ่มาาาากกก จนไม่น่าจะมีคอมพิวเตอร์เครื่องไหนในโลก สามารถแสดงผลแผนที่โลกทั้งแผ่นที่ความละเอียดสูงสุด ได้ในการอ่านข้อมูลครั้งเดียว เราจึงต้องทำการซอยแผนที่เป็นแผ่นย่อยๆ เรียกว่า Tile ครับ โดย Tile แต่ละแผ่นนั้น จะมี 256x256px ซึ่งจริงๆ จะมีขนาดเท่าใดก็ได้ แต่ทั้งสามระบบ (Virtual Earth/GoogleMap/Yahoo Map) นั้นใช้ 256px เท่านั้นหมดครับ ยกเว้น GoogleMap Mobile ที่จะใช้ 64px เพื่อให้ดาวน์โหลดได้เร็ว ซึ่งจริงๆ แล้วเทคโนโลยี DeepZoom ของ Silverlight นั้นก็ใช้วิธีการซอยแบบนี้เช่นเดียวกันครับ
สำหรับไอเดียของ Tile โดยแริ่มแรกเลย แผนที่โลกทั้งแผ่น จะถูกย่อให้เหลือขนาด 1 Tile ก่อน จากนั้น เมื่อเราทำการซูมเข้าไป 1 ระดับ Tile 1 แผ่นนั้น ก็จะโดนแยกเป็น 4 ส่วน (4 Tile) ดังภาพ โดยแต่ละระบบ ก็จะมีระดับต่างกันไป ซึ่งของ Virtial Earth อยู่ในช่วง 1-23 ครับ
และนอกจากนี้ การคำนวณหา Tile ที่เราต้องการนั้น เพียงแค่ใช้การยกกำลังเท่านั้นเอง จึงทำให้การประมวลผล สามารถทำได้อย่างรวดเร็วมาก และการจัดเก็บ Tile ที่ต้องการ ก็ง่ายอีกด้วย
ขนาดของแผนที่ในแต่ละระดับการซูม
เพื่อให้การคำนวณหาพิกัด โดยอ้างอิงจากพิกัดของพิกเซลบนจอภาพสามารถทำได้ เราจำเป็นจะต้องรู้ขนาดของแผนที่ของแต่ละระดับการซูมด้วยครับ ซึ่งสามารถคำนวณได้ตามสูตรนี้ ต้องระวังด้วยว่า GoogleMap แบบ Street View นั้น ใช้ zoomLevel แบบผกผันนะครับ นั่นคือ เลขมาก ซูมน้อย ส่วนของเจ้าอื่น เป็นค่าตามปกติครับ
public long MapSize(int zoomLevel)
{
return (long)Math.Pow(2, zoomLevel) * TileSize;
}
ลองดูจากตารางด้านล่างนี้นะครับ ว่าในแต่ละระดับการซูม แผนที่จะมีขนาดเปลี่ยนแปลงไปอย่างไรบ้าง
| ซูม | ขนาดแผนที่ | จำนวน Tile |
| 0 | 256x256 | 1 |
| 1 | 512x512 | 4 |
| 10 | 262144 x 262144 | 1048576 |
| 23 | 2,147,483,648 x 2,147,483,648 | 16777216 |
จะเห็นได้ว่า เรากำลังจะต้องต่อกรกับข้อมูล (ภาพ) ปริมาณมากมายมหาศาลขนาดไหน เพราะฉะนั้น เราจะต้องมีระบบจัดการแรมที่ดีด้วยนะครับ
การหาพิกัดของโลก บนแผนที่
สมมุติว่า ถ้าเรารู้แล้วว่า เราต้องการดูแผนที่ ที่พิกัด 13° 45′ 8″ N, 100° 29′ 38″ E (พิกัดของกรุงเทพ) เราจะทราบได้อย่างไรว่า พิกัดดังกล่าว คือพิกเซลไหนบนแผนที่? การคำนวณดังกล่าว ก็คือการแปลงแกนนั่นเองครับ
ในการแปลงแกนแบบนี้ โดยมาก เราจะทำการ Normalize ค่าที่เราจะแปลง ให้อยู่ในช่วง 0 ถึง 1 ก่อน (คล้ายกับการเทียบบัญญัตไตรยางค์ [rule of three in arithmetic] เขียนถูกมั๊ยเนี่ย) เพื่อที่เราจะได้รู้ว่า ค่าที่เราต้องการ อยู่ในช่วงไหน ของแกนเก่า จากนั้นถึงเอาไปเทียบกับแกนใหม่อีกทีหนึ่ง
สำหรับค่า Longtitude นั้น ง่ายมากครับ เนื่องจากเป็นค่า Linear และเรารู้อยู่แล้วว่า โลกกลม นั่นก็คือมี 360 องศาถ้าต้องการจะหาว่า องศาที่ต้องการ อยู่ในช่วงไหน ก็ใช้ว่า
double x = (longtitude + 180) / 360;
ซึ่งจะเห็นว่า ผมได้แปลงค่า longtitude ให้กลับมาในช่วง 0-360 ก่อน (จากเดิมอยู่ที่ –180 ถึง 180) แล้วหาค่า x ออกมาครับ ส่วนค่า lontitude นั้น ยากหน่อย ตรงที่เราต้องใช้สูตรของ Mercator Projection มาช่วยครับ
ซึ่งสูตรที่สามารถแปลงเป็นโค๊ดได้ง่ายที่สุด ก็คือสูตรที่สองครับ เนื่องจากใช้เพียงฟังก์ชั่น Sin กับ Log เท่านั้น โดยสามารถแปลงเป็นโค๊ดได้ว่า
double sinLatitude = Math.Sin(coord.Latitude * Math.PI / 180);
double y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);
แต่ผมขอยอมรับตามตรงว่า ผมจำไม่ได้แล้วว่าทำไมถึงแปลงได้แบบนี้ เคยพยายามทำความเข้าใจอยู่พักใหญ่ๆ แต่ตอนนี้ลืมไปแล้วละ ถ้าใครคล่อง Math มาบอกกันบ้างก็ดีนะครับ ส่วนที่ใช้ฟังก์ชั่น Math.Log เฉยๆ เพราะว่า Math.Log จะใช้ฐานเป็น e ถ้าไม่ได้กำหนดฐานครับ ซึ่งก็คือ ln นั่นเอง จากสุตรนี้ ค่าของ x, y ก็จะโดน Normalize ให้อยู่ในช่วงขอพิกัดของแผนที่แบบ Mecrator Projection เรียบร้อยแล้ว
จากนั้น เราก็นำค่าที่ได้ มาหาพิกัดเป็น Pixel โดยเทียบกับจนาดของแผนที่ปัจจุบัน ซึ่งในส่วนที่แล้ว เราได้สร้างฟังก์ชั่น MapSize ขึ้นมาเรียบร้อยแล้ว ก็เพียงแต่นำฟังก์ชั่นนั้นมาใช้ครับ เช่น พิกัดของกรุงเทพ ก็จะคำนวณได้เป็น
x=0.779149691666667, y=0.461427218750532
จากนั้น เราก็ดูว่า ขนาดของแผนที่ในระดับที่เราต้องการ มีขนาดเท่าไหร่ เช่น ถ้าเรากำลังดูแผนที่โลกที่ระดับไกลที่สุด (ระดับ 1) ของ VE แผนที่โลกก็จะมีขนาด 512x512 pixel (2^1 * 256) ซึ่งก็คือ
x=0.779149691666667*512 = 398.924642133333504
y=0.461427218750532*512 = 236.250736000272384
เห็นแล้วใช่มั๊ยครับ ว่าทำไม double จึงจำเป็น
ถ้าเราลองพลอตจุดนี้ลงในแผนที่โลก ขนาด 512x512px (แผนที่จาก Virtual Earth) ก็จะพบว่า จุดที่เราได้นั้น อยู่บริเวณกรุงเทพพอดีเลยครับ
หา Tile ที่พิกัดนั้น ตั้งอยู่
หลังจากที่เราทราบพิกัดบนแผนที่่เรียบร้อยแล้ว ปัญหาต่อไปก็คือการหา Tile ที่พิกัดนั้นตั้งอยู่ เพื่อที่เราจะได้สามารถโหลด Tile ที่ถูกต้องออกมาได้ ซึ่งการหา Tile นั้น ทำได้ง่ายมาก เนื่องจากขนาดของแผนที่นั้น จะมีขนาดตายตัว และเป็นภาพสี่เหลี่ยมจัตุรัส การหา tile นั้น ก็เพียงแค่นำเอาพิกัด หารด้วยขนาดของ Tile แบบไม่เอาเศษนั่นเอง เช่น
x=398.924642133333504 / 256 = 1
y=236.250736000272384 / 256 = 0
เมื่อเราทราบแล้วว่า เราจะต้องใช้ Tile ชิ้นไหน เหลือก็เพียงแค่ การหา URL ที่จะโหลด Tile เหล่านี้มาใช่ นั่นเองครับ
URL ของ Tile
สำหรับ URL ของผู้ให้บริการแต่ละแห่ง ในการดาวน์โหลดแผนที่ ก็จะแตกต่างกันไป ซึ่งผมขอยกตัวอย่างเจ้าใหญ่ๆ สองที่ คือ Microsoft และ Google ครับ แต่ก่อนจะนำ URL เหล่านี้ไปใช้ ผมอยากจะให้ท่านที่สนใจจะนำไอเดียเหล่านี้ไปใช้ เช็คกับ License ของผู้ให้บริการแผนที่เสียก่อนว่า เราสามารถนำแผนที่เขามาใช้ได้อย่างไรบ้างนะครับ
Virtual Earth
รูปแบบ URL:
http://[type][server].ortho.tiles.virtualearth.net/tiles/[type][location].[format]?g=45
- Server: เลข Server ที่จะดาวน์โหลด มี 4 Server คือ 0, 1, 2, 3.
- Type: r = แผนที่ธรรมดา, a = แผนที่ดาวเทียม, h = แผนที่ดาวเทียมแบบมีเส้นทาง.
- Formats: png สำหรับ r และ jpeg สำหรับที่เหลือ
- Location: จากฟังก์ชั่น TileXYToQuadKey ซึ่ง Microsoft มีโค๊ดให้ครับ
/// <summary>
/// Converts tile XY coordinates into a QuadKey at a specified level of detail.
/// </summary>
/// <param name="tileX">Tile X coordinate.</param>
/// <param name="tileY">Tile Y coordinate.</param>
/// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
/// to 23 (highest detail).</param>
/// <returns>A string containing the QuadKey.</returns>
public static string TileXYToQuadKey(int tileX, int tileY, int levelOfDetail)
{
StringBuilder quadKey = new StringBuilder();
for (int i = levelOfDetail; i > 0; i--)
{
char digit = '0';
int mask = 1 << (i - 1);
if ((tileX & mask) != 0)
{
digit++;
}
if ((tileY & mask) != 0)
{
digit++;
digit++;
}
quadKey.Append(digit);
}
return quadKey.ToString();
}
GoogleMap
ของ GoogleMap นั้นจะง่ายกว่า ตรงที่เราสามารถใส่เลข Tile ที่เราคำนวณได้ทันที โดยไม่ต้องแปลงค่าเพิ่มครับ โปรดสังเกตว่า Zoom ของ GoogleMap นั้น เริ่มที่ 17 นั่นคือมุมกว้างที่สุด ไปหา 1 ซึ่งคือมุมแคบที่สุด (ซูมมากที่สุด) สำหรับ Url นี้เป็น URL ของ Street Map ครับ
รูปแบบ URL:
http://[server].google.com/mt?v=[version]&x=[column]&y=[row]&z=[zoom]
- Servers: mt0, mt1, mt2, mt3
- Version: w2.89 แต่ผมแนะนำให้ลองใช้ Fiddler เช็คดูว่า ตอนเราเล่น GoogleMap ตัว Browser นั้นไปโหลดแผนที่จากไหนมา แล้วใช้เลขเวอร์ชั่นตามนั้นครับ
- Row, Column: tile ที่ได้จากการคำนวณ
- Zoom: 1-17
สำหรับ Tile ที่มีพิกัดของกรุงเทพอยู่ ก็คือ
http://r3.ortho.tiles.virtualearth.net/tiles/r1.png?g=45 ดังภาพครับ
ส่วนของ GoogleMap จะเป็นภาพนี้ครับ
http://mt1.google.com/mt?v=w2.89&hl=th&x=1&y=0&z=1&s=zz
(จะเห็นว่า Url มีพารามิเตอร์ s ด้วย ซึ่งเอาไว้ใช้หลอกไม่ให้ Browser Cache ภาพไว้)
สรุป
ในบทความนี้ ผมได้ครอบคลุมถึงหลักการแสดงผลแผนที่ ตลอดจนการคำนวณต่างๆ ที่เกี่ยวข้องไว้ ซึ่งผมเชื่อว่า น่าจะเป็นจุดเริ่มต้น ให้คุณสามารถทดลองเขียนโค๊ด เพื่อดาวน์โหลดแผนที่จากผู้ให้บริการต่างๆ ได้แล้ว และในบทความต่อไป ผมจะแนะนำถึงการออกแบบตัวโปรเจคนี้ เพื่อให้สามารถทำงานได้กับระบบแผนที่หลายๆ เจ้าได้ครับ
สำหรับการวาดแผนที่ ดูต่อได้ภาคสองเลยครับ
http://coresharp.net/blogs/article/archive/2009/03/15/maponmobile2.aspx