[Windows Mobile .NET CF] 即時交通影像 - Day8
既然有附近的停車場資訊, 想當然, 你可以找找看大馬路上的 Camera 資訊.
既然一樣是透過 fiddler 取得眾多的資訊, 我在這裡就不再重複如何做到,
直接寫出重點.
直接存取 URL : http://its.taipei.gov.tw/atis_index/ASPX/GetAction.aspx?Lang=CHT&Action=1
你就會看到一連串的 Camera 資訊如下:
TaipeiCCTV_12,_建國南路-辛亥路,_210.241.67.146:12,_303422,_2768557,
_&_N_42,_北上側59.888公里,_http://cif.nfreeway.gov.tw/live-view/mjpg/video.cgi?camera=42,_278565.56181218,_2756126.97855742,
_&_TaipeiCCTV_71,_延平北路-通河西街(洲美北向,_210.241.67.147:71,_299380,_2776487,
_&_TaipeiCCTV_79,_象山隧道往南3+280,_210.241.67.147:79,_307312,_2768328,
…
再加上 AXIS 公開的 API : http://www.axis.com/techsup/cam_servers/dev/cam_http_api_2.php
我們可以推測, 上面的資料欄位內容大致有下面五個欄位:
1. Camera ID ,
2. Camera 顯示名稱,
3.URL 資訊,
4. TWD67 TM2 X,
5. TWD67 TM2 Y
而那個 URL 資訊就是重點, 若是 IP:Number , 表示是以下面的 URL 存放 motion jpeg:
http://[IP]/axis-cgi/mjpg/video.cgi?camera=[camera number]&fps=1
若是 http 開頭, 就直接是 multiple jpeg 的 URL .
根據 AXIS API , 把 multiple jpeg 的 URL 當中的 mjpg/video.cgi 換成 jpg/image.cgi , 就可以得到 jpeg 的影像.
比方說 建國南路-辛亥路的 jpeg 影像就是
http://210.241.67.146/axis-cgi/jpg/image.cgi?camera=12
這樣相信大家就很清楚, 我們的手機要如何取得影像資訊囉!
好簡單啊, 用一個 picturebox, new 一個 image 就有啦.
private void menuItem3_Click(object sender, EventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
try
{
HttpWebRequest r = WebRequest.Create("http://210.241.67.146/axis-cgi/jpg/image.cgi?camera=12") as HttpWebRequest;
WebResponse resp = r.GetResponse();
using (Stream stream = resp.GetResponseStream())
pictureBox1.Image = new Bitmap(stream);
resp.Close();
}
finally
{
Cursor.Current = Cursors.Default;
}
}
畫面大概是這樣
那如果我們想要看到會動的影像 (video) 呢?
如果你用心花費許多時間去研究 AXIS API, 就可以知道, 其實那個 Video API
它真正的資料是 Multiple JPEG , 就是很多張 JPEG 的資料流.
所以我們只要寫出從該資料流分離出一張一張的 JPEG , 然後不斷更新 picture box ,
就做到了那個官方網頁提供的 Actvie X 的功能啦 (不禁嘆氣, 那個 Active X 只有 windows + IE 才有用啊)
(小小聲的說, 那個 ActiveX 還只能播一分鐘, 這個程式可以播很久…
但是基於公用資源不要太浪費, 大家就省點用吧 XD)
根據 AXIS spec , 以及實際測試, 我寫了這段程式碼用來從 stream 當中分離出 JPEG:
public class MultipleJPEGStream : IDisposable
{
private static int buffersize = 1024;
private Stream baseinputstream;
private MemoryStream msbuffer;
private int NextBufferSize;
public MultipleJPEGStream(Stream baseInputStream)
{
baseinputstream = baseInputStream;
msbuffer = new MemoryStream();
NextBufferSize = 0;
}
//Content-Type: image/jpeg
//Content-Length: 10351
private static byte[] boundry = Encoding.ASCII.GetBytes("--myboundary");
private string readasciiuntilendline(MemoryStream ms)
{
StringBuilder result = new StringBuilder();
long orginalpos = ms.Position;
while (true)
{
int r = ms.ReadByte();
if (r == -1)
{
ms.Position = orginalpos;
return null;
}
if (r == 13)
{
int n = ms.ReadByte();
if (n != 10)
ms.Position = ms.Position - 1;
return result.ToString();
}
result.Append((char)r);
}
}
/// <summary>
/// return null if none.
/// </summary>
/// <returns></returns>
public byte[] GetNextBitmap()
{
int readed = 0;
byte[] buf = new byte[buffersize];
while ((readed = baseinputstream.Read(buf, 0, buf.Length)) != 0)
{
msbuffer.Write(buf, 0, readed);
if (NextBufferSize == 0)
{
// we have to scan the memory, get next buffer size , then re-constructor msbuffer.
if (msbuffer.Length < 100)
continue;
// scan to --myboundary.
byte[] scanbuf = msbuffer.GetBuffer();
int i = 0, j = 0;
while (i < scanbuf.Length)
{
if (scanbuf[i] == boundry[j])
{
j++;
if (j == boundry.Length)
break;
}
else
j = 0;
i++;
}
if (j == boundry.Length)
{
msbuffer.Position = i;
string boundrystr = readasciiuntilendline(msbuffer);
string contenttype = readasciiuntilendline(msbuffer);
string contentlength = readasciiuntilendline(msbuffer);
string emptyline = readasciiuntilendline(msbuffer);
if (string.IsNullOrEmpty(contentlength))
continue;
// now we could parse size and length, and re-constructor msbuffer.
NextBufferSize = Int32.Parse(contentlength.Substring(15));
if ((msbuffer.Length - msbuffer.Position) > NextBufferSize)
return readimagebyte((int)msbuffer.Position);
else
{
msbuffer = copyleftstream(msbuffer);
continue;
}
}
else
{
// give up all content.
msbuffer = new MemoryStream();
continue;
}
}
else if (NextBufferSize > msbuffer.Length)
continue;
// now we got enough size.
return readimagebyte(0);
}
return null;
}
private byte[] readimagebyte(int beginpos)
{
byte[] r = new byte[NextBufferSize];
msbuffer.Seek(beginpos, SeekOrigin.Begin);
msbuffer.Read(r, 0, r.Length);
msbuffer = copyleftstream(msbuffer);
NextBufferSize = 0;
return r;
}
private MemoryStream copyleftstream(MemoryStream ms)
{
int size = (int)(ms.Length - ms.Position);
MemoryStream newstream = new MemoryStream(size);
byte[] buf = new byte[buffersize];
while (true)
{
int readed = ms.Read(buf, 0, buf.Length);
if (readed == 0)
break;
newstream.Write(buf, 0, readed);
}
return newstream;
}
#region IDisposable 成員
public void Dispose()
{
msbuffer.Dispose();
}
#endregion
}
接下來, 我寫了這段程式碼來取得攝影機列表, 為了不用每次都取得攝影機列表,
我也利用 Xml Serialization 將取得的資料存到檔案去, 下次就不用再取一遍了,
另外也因為 UI 即將會用 multi-thread , 所以存取 cameras 的內容時最好 lock 起來再用:
通常商業程式或是 library 都不太建議 lock public variable, 因為這樣外面的 programmer 用起來容易出問題,
但在這裡我就先放棄這項準則, 只要記得使用 cameras 之前最好 lock 再用就好
還有就是計算距離, 所謂二度分帶座標, 好處就是單位就是公尺,
所以計算距離時用簡單的歐幾里得距離公式就可以算出距離 (單位是公尺)
計算方向就用簡單的三角函式 Atan2 就可以算出角度,
再切為八個面向, 這點不是很困難.
public class TrafficCamera
{
public string Id { get; set; }
public string Name { get; set; }
public double PosX { get; set; }
public double PosY { get; set; }
public string ImageURL { get; set; }
public string VideoURL { get; set; }
public string dir { get; set; }
public double distance { get; set; }
}
public class TrafficCameraManager
{
public List<TrafficCamera> cameras;
private string fn;
public TrafficCameraManager(string camerafile)
{
fn = camerafile;
cameras = new List<TrafficCamera>();
load();
}
private string getimageurl(string url)
{
return getvideourl(url).Replace("mjpg/video.cgi", "jpg/image.cgi");
}
private string getvideourl(string url)
{
if (url.StartsWith("http://"))
return url;
else
{
string[] parts = url.Split(':');
if (parts.Length == 2)
return "http://" + parts[0] + "/axis-cgi/mjpg/video.cgi?camera=" + parts[1];
}
return string.Empty;
}
public void UpdateFromNetwork()
{
HttpWebRequest request = WebRequest.Create("http://its.taipei.gov.tw/atis_index/ASPX/GetAction.aspx?Lang=CHT&Action=1") as HttpWebRequest;
WebResponse resp = request.GetResponse();
using (Stream stream = resp.GetResponseStream())
using (StreamReader sr = new StreamReader(stream, Encoding.GetEncoding("big5")))
{
List<TrafficCamera> newcameras = new List<TrafficCamera>();
string content = sr.ReadToEnd();
string[] parts = Regex.Split(content, ",_");
for (int i=0;(i+4)<parts.Length;i += 5) {
newcameras.Add(new TrafficCamera()
{
Id = parts[i],
Name = parts[i+1],
ImageURL = getimageurl(parts[i+2]),
VideoURL = getvideourl(parts[i+2]),
PosX = Convert.ToDouble(parts[i+3]),
PosY = Convert.ToDouble(parts[i+4]),
});
}
lock (cameras)
{
cameras.Clear();
cameras.AddRange(newcameras);
}
}
save();
}
private static double radiantodegreefactor = 180 / Math.PI;
public void UpdateDistance(double x, double y)
{
lock (cameras)
{
foreach (var c in cameras)
{
double xd = (c.PosX-x);
double yd = (c.PosY-y);
c.distance = Convert.ToInt32(Math.Sqrt((xd * xd) + (yd * yd)));
if ((xd == 0) && (yd == 0))
c.dir = "無";
else
{
double deg = Math.Atan2(yd, xd) * radiantodegreefactor;
if (deg < 0)
deg += 360;
c.dir = getdirname(deg);
}
}
}
}
private static string[] dirnames = new string[] {
"東", "東北", "北", "西北", "西", "西南", "南", "東南"};
/// <summary>
/// degree = 0 ~ 360, 切八個方位
/// </summary>
/// <param name="degree"></param>
/// <returns></returns>
private string getdirname(double degree)
{
int dirnum = Convert.ToInt32(degree / 22.5);
if (dirnum == 15)
dirnum = 0;
dirnum = (dirnum + 1) / 2;
if (dirnum > 7)
dirnum = 7;
return dirnames[dirnum];
}
private void save()
{
try
{
using (FileStream fs = new FileStream(fn, FileMode.OpenOrCreate, FileAccess.Write))
{
XmlSerializer ser = new XmlSerializer(typeof(TrafficCamera[]));
ser.Serialize(fs, cameras.ToArray());
}
}
catch
{
// give up if save file failed.
}
}
private void load()
{
if (File.Exists(fn))
{
try
{
using (FileStream fs = new FileStream(fn, FileMode.Open, FileAccess.Read))
{
XmlSerializer ser = new XmlSerializer(typeof(TrafficCamera[]));
TrafficCamera[] cameraarray = ser.Deserialize(fs) as TrafficCamera[];
cameras = new List<TrafficCamera>(cameraarray);
}
}
catch {
// give up if load file failed.
}
}
}
}
功能性的程式碼介紹完了, 開始打造 UI , 首先用 Listview 列出所有攝影機
初始化的程式碼:
private Gps gps;
private TrafficCameraManager tcm;
public Form1()
{
InitializeComponent();
gps = new Gps();
string camerafile = Path.Combine(
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName),
"cameras.xml");
tcm = new TrafficCameraManager(camerafile);
updatelist(false);
}
GPS 啟用/停用的程式碼:
private void menuItem5_Click(object sender, EventArgs e)
{
if (gps.Opened)
gps.Close();
else
{
gps.Open();
gps.LocationChanged += new LocationChangedEventHandler(gps_LocationChanged);
}
menuItem5.Checked = gps.Opened;
}
當 GPS 收到座標變更的程式碼 (我將間隔拉開到至少五秒, 這樣畫面才不會一直閃動):
private DateTime lastupdatetime = DateTime.Now;
void gps_LocationChanged(object sender, LocationChangedEventArgs args)
{
if ((DateTime.Now - lastupdatetime) < TimeSpan.FromSeconds(5))
return;
lastupdatetime = DateTime.Now;
var gpspos = args.Position;
if (gpspos.LatitudeValid && gpspos.LongitudeValid)
{
var pos = Datum.WGS84toTWD67TM2(gpspos.Longitude, gpspos.Latitude);
tcm.UpdateDistance(pos.X, pos.Y);
this.Invoke(new EventHandler((obj,arg) => updatelist(true)));
}
}
private void updatelist(bool updateonly)
{
listView1.BeginUpdate();
try
{
lock (tcm.cameras)
{
if (updateonly)
{
// update.
foreach (ListViewItem item in listView1.Items)
{
var found = tcm.cameras.Find(c => c.Name == item.Text);
if (found != null)
{
item.SubItems[1].Text = found.distance.ToString();
item.SubItems[2].Text = found.dir;
}
}
}
else
{
// replace
listView1.Items.Clear();
foreach (var c in tcm.cameras)
{
listView1.Items.Add(new ListViewItem(new string[] {
c.Name, c.distance.ToString(), c.dir}));
}
}
}
}
finally
{
listView1.EndUpdate();
}
}
我們還可以做按照名稱, 距離, 方向來排序:
private void listView1_ColumnClick(object sender, ColumnClickEventArgs e)
{
lock (tcm.cameras)
{
switch (e.Column)
{
case 0:
// sort by nanme.
tcm.cameras.Sort((x, y) => x.Name.CompareTo(y.Name));
break;
case 1:
// sort by distance
tcm.cameras.Sort((x, y) => x.distance.CompareTo(y.distance));
break;
case 2:
// sort by dir
tcm.cameras.Sort((x, y) => x.dir.CompareTo(y.dir));
break;
}
updatelist(false);
}
}
當點下去某個攝影機時, 我們啟動一個 UI Form 來顯示即時影像動畫:
private void listView1_ItemActivate(object sender, EventArgs e)
{
if (listView1.SelectedIndices.Count == 0)
return;
ListViewItem selected = listView1.Items[listView1.SelectedIndices[0]];
using (ShowVideo sv = new ShowVideo())
{
string selectedname = selected.Text;
var camera = tcm.cameras.Find( c => c.Name == selectedname);
sv.SetVideoURL(camera.VideoURL);
sv.ShowDialog();
}
}
顯示即時影像動畫的 Form, code 與 UI (僅僅是擺一個 PictureBox) 如下:
public partial class ShowVideo : Form
{
Thread updatethread = null;
public ShowVideo()
{
InitializeComponent();
}
public void SetVideoURL(string url)
{
if (updatethread != null)
{
updatethread.Abort();
updatethread = null;
}
Cursor.Current = Cursors.WaitCursor;
updatethread = new Thread(() =>
{
try
{
WebRequest httpRequest = HttpWebRequest.Create(url);
WebResponse httpResponse = httpRequest.GetResponse();
try
{
using (Stream imageStream = httpResponse.GetResponseStream())
using (MultipleJPEGStream mjstream = new MultipleJPEGStream(imageStream))
{
byte[] imagedata;
while ((imagedata = mjstream.GetNextBitmap()) != null)
{
using (MemoryStream imagems = new MemoryStream(imagedata))
this.Invoke(new EventHandler((obj, args) =>
{
pictureBox1.Image = new Bitmap(imagems);
Cursor.Current = Cursors.Default;
}));
}
}
}
finally
{
httpResponse.Close();
}
}
catch (ThreadAbortException) { return; } // ignore, stop thread.
catch (WebException wex)
{
this.Invoke(new EventHandler((obj, args) => MessageBox.Show("無法連線, 原因為:" + wex.ToString())));
return;
}
catch (ObjectDisposedException oex)
{
this.Invoke(new EventHandler((obj, args) => MessageBox.Show("下載失敗, 原因為:" + oex.ToString())));
return;
}
});
updatethread.IsBackground = true;
updatethread.Start();
}
private void menuItem1_Click(object sender, EventArgs e)
{
this.Close();
}
private void ShowVideo_Closing(object sender, CancelEventArgs e)
{
if (updatethread != null)
updatethread.Abort();
}
}
OK! 實際使用狀況像是這樣:
原始碼下載:TrafficCamera.zip