[Windows Mobile .NET CF] 即時交通影像 - Day8

[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;
    }
}

 

畫面大概是這樣

image

那如果我們想要看到會動的影像 (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 列出所有攝影機

image

初始化的程式碼:


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) 如下:

image


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