Map 類(lèi)MapRegion 類(lèi)定義了兩個(gè)依賴屬性:BoundingBox 和 RegionName。BoundingBox 屬性定義包含區(qū)域的矩形范圍,RegionName 則存儲(chǔ)區(qū)域名稱(chēng)。該類(lèi)還定義了兩個(gè)集合屬性:Polygons 和 FipsCodes。Polygons 集合存儲(chǔ)定義區(qū)域的多邊形,F(xiàn)ipsCodes 則存儲(chǔ)與區(qū)域相關(guān)的一組數(shù)字 FIPS(聯(lián)邦信息處理標(biāo)準(zhǔn))代碼。該類(lèi)使用 FIPS 代碼,因?yàn)樵趹?yīng)用程序中使用的多邊形數(shù)據(jù)是從美國(guó)人口普查局下載的,它使用 FIPS 代碼來(lái)唯一標(biāo)識(shí)美國(guó)的地理實(shí)體。您可以從itl.nist.gov/fipspubs/by-num.htm 下載關(guān)于聯(lián)邦信息處理標(biāo)準(zhǔn)的詳細(xì)信息。此外,還可以從census.gov/geo/www/cob 訪問(wèn)關(guān)于美國(guó)人口普查局的公共域 TIGER 地理數(shù)據(jù)庫(kù)的技術(shù)信息。MapPolygon 類(lèi)定義了兩個(gè)屬性。第一個(gè)是名為 Region 的只讀依賴屬性,存儲(chǔ)對(duì)包含多邊形的區(qū)域的引用。第二個(gè)是名為 Points 的集合屬性,存儲(chǔ)定義多邊形頂點(diǎn)的一組 Point 結(jié)構(gòu)。在圖 4 中可以看到該類(lèi)的完整源代碼。Region 屬性作為只讀依賴屬性公開(kāi)。這使得 WPF 屬性系統(tǒng)能夠檢測(cè)對(duì)屬性值的更改,但不允許 WPF 修改該屬性值。只讀依賴屬性是通過(guò)調(diào)用 DependencyProperty 類(lèi)的 RegisterReadOnly 方法來(lái)聲明的。與 Register 方法不同,RegisterReadOnly 會(huì)返回 DependencyPropertyKey 實(shí)例而不是返回 DependencyProperty。依賴屬性鍵是可實(shí)現(xiàn)對(duì)只讀依賴屬性進(jìn)行修改的對(duì)象。這些對(duì)于實(shí)現(xiàn)類(lèi)(需要內(nèi)部更新屬性值,并由其他類(lèi)型檢測(cè)到更改,但不允許對(duì)值進(jìn)行外部修改)非常有用。因此,DependencyPropertyKey 通常不會(huì)由定義它們的類(lèi)公開(kāi)。這便是 RegionPropertyKey 字段標(biāo)記為私有的原因。之后,第二個(gè)共享字段 RegionProperty 會(huì)用于公開(kāi)提供它的只讀別名。Region 屬性為只讀屬性,因?yàn)樗O(shè)計(jì)為無(wú)法從外部進(jìn)行編輯。實(shí)際上,當(dāng)在 MapRegion 類(lèi)的區(qū)域集合中添加或刪除多邊形時(shí),它會(huì)自動(dòng)設(shè)置和清除值。這是通過(guò)以下方式實(shí)現(xiàn)的:處理多邊形集合的 CollectionChangedEvent 并將適當(dāng)?shù)?Region 實(shí)例傳播到受影響的多邊形。事件處理程序的實(shí)現(xiàn)如圖 5 所示。它通過(guò)調(diào)用存儲(chǔ)于處理程序 NotifyCollectionChangedEventArgs 參數(shù)中 NewItems 和 OldItems 集合上的 SetRegion 擴(kuò)展方法來(lái)實(shí)現(xiàn)。之后該擴(kuò)展方法會(huì)在目標(biāo)集合中每一項(xiàng)上調(diào)用 MapPolygon 類(lèi)的 SetRegion 方法。該實(shí)例方法已標(biāo)記為友元,以避免被調(diào)用。該擴(kuò)展方法的實(shí)現(xiàn)如下所示:<Extension()> _Sub SetRegion(ByVal x As IEnumerable(Of MapPolygon), _ByVal r As MapRegion)For Each item In xitem.SetRegion(r)NextEnd Sub 與 Map 和 MapRegion 類(lèi)上的集合屬性不同,MapPolygon 類(lèi)上的 Points 集合不是 ObservableCollection(of T)。而是使用了內(nèi)置 WPF PointsCollection 類(lèi)。這與 WPF Polygon 控件用于存儲(chǔ)它所含一組頂點(diǎn)的類(lèi)型相同。通過(guò)在數(shù)據(jù)模型中使用此類(lèi),便可實(shí)現(xiàn)將 Polygon 控件與 MapPolygon 實(shí)例直接進(jìn)行數(shù)據(jù)綁定。
導(dǎo)入地圖數(shù)據(jù)應(yīng)用程序從存儲(chǔ)于磁盤(pán)上的 XML 文件加載其地圖數(shù)據(jù)。我通過(guò)從美國(guó)人口普查局下載的原始 ASCII 制圖邊界文件創(chuàng)建文件,其中說(shuō)明了美國(guó)每個(gè)州和縣(及等同范圍)的邊界。州邊界數(shù)據(jù)用于繪制美國(guó)地圖,縣邊界數(shù)據(jù)則用于實(shí)現(xiàn)地圖上人口數(shù)據(jù)可視化。然后我取得下載的數(shù)據(jù),并將其轉(zhuǎn)換為 XML 文件以便處理。本文的下載提供了將文件轉(zhuǎn)換為 XML 的代碼??蓮?a target="_blank" >census.gov/geo/www/cob/bdy_files.html 下載原始數(shù)據(jù)文件。使用 Visual Studio Create Schema 功能,我便能夠自動(dòng)生成描述 XML 文件內(nèi)容的 XML 架構(gòu)。這樣我便能夠使用新的 Visual Basic XML IntelliSense® 功能,它可依據(jù)項(xiàng)目中包括的任何架構(gòu)文件顯示 XML 成員訪問(wèn)表達(dá)式(參見(jiàn)圖 6)。
圖 6 Visual Basic XML IntelliSense (單擊該圖像獲得較小視圖)
圖 6 Visual Basic XML IntelliSense (單擊該圖像獲得較大視圖)
地圖數(shù)據(jù)格式描述 XML 地圖文件的架構(gòu)定義了一種簡(jiǎn)單格式,其中包含一個(gè)根 File 標(biāo)記,它隨之又包含一系列 Region 標(biāo)記。每個(gè) Region 標(biāo)記都包含一系列 FipsCode 和 Polygon 標(biāo)記,每個(gè) Polygon 標(biāo)記則包含一系列 Vertex 和 Island 標(biāo)記。Region 標(biāo)記用于描述簡(jiǎn)單的區(qū)域。它定義了兩個(gè)必需的屬性:Type 和 Name。Type 屬性提供區(qū)域類(lèi)型的描述。就我的應(yīng)用程序而言,我通常使用值 State 或 County。但是,在一般情況下,所有值均可用于 Type 屬性。Name 屬性則說(shuō)明區(qū)域的名稱(chēng)。區(qū)域中定義的每個(gè) FipsCode 標(biāo)記均說(shuō)明其中一個(gè) FIPS 代碼項(xiàng)。Region 內(nèi)的最后一個(gè) FipsCode 標(biāo)記定義了區(qū)域的數(shù)字 ID。其他 FipsCode 標(biāo)記則遞歸描述區(qū)域各父項(xiàng)的數(shù)字 ID。就我的應(yīng)用程序而言,所有區(qū)域最多具有兩個(gè)相關(guān)的 FipsCode 值。特別是,描述州(或者州等同范圍)的區(qū)域通常具有一個(gè)項(xiàng),描述與該州相關(guān)的 FIPS 代碼。描述縣(或縣等同范圍)的 Regions 通常具有兩個(gè)項(xiàng),第一項(xiàng)表示包含縣的州 FIPS 代碼,第二項(xiàng)則表示縣本身的 FIPS 代碼。任何特定區(qū)域的數(shù)字 ID 通常都保證在其直接父項(xiàng)的范圍內(nèi)唯一。區(qū)域的每個(gè) Polygon 標(biāo)記描述了區(qū)域內(nèi)包含的單個(gè)多邊形。多邊形由一系列 Vertex 標(biāo)記組成,每個(gè)標(biāo)記描述了單個(gè)頂點(diǎn)。Vertex 標(biāo)記定義了五個(gè)必需的屬性:Ordinal、Longitude、Latitude、X 和 Y。Ordinal 屬性定義了多邊形頂點(diǎn)的順序。其主要目的是,當(dāng)頂點(diǎn)由不保留順序的查詢處理時(shí),能夠?qū)旤c(diǎn)進(jìn)行正確排序。它目前尚未用于我的應(yīng)用程序。Longitude 和 Latitude 屬性描述了頂點(diǎn)的經(jīng)度和緯度坐標(biāo)。同樣,X 和 Y 屬性定義了投影到矩形空間的頂點(diǎn)坐標(biāo)和應(yīng)用程序用于繪制地圖的對(duì)象。X 和 Y 屬性是通過(guò)圖 7 所示的簡(jiǎn)單投影來(lái)計(jì)算的。此投影不是特別準(zhǔn)確,可能不適用于某些情形。在這些情況下,可以使用 Longitude 和 Latitude 而非存儲(chǔ)于文件中的 X 和 Y 值來(lái)直接計(jì)算新值。就我的應(yīng)用程序而言,并不需要高度的地理精確度,這便已足夠。
圖 7 極其簡(jiǎn)單的地圖投影多邊形內(nèi)的 Island 標(biāo)記定義了包含在其中但不屬于它所含區(qū)域的空洞。它包含了描述空洞邊界的 Vertex 標(biāo)記的集合。我主要出于完整性目的將它們包含在數(shù)據(jù)文件中;但并沒(méi)有用于我的應(yīng)用程序。這意味著實(shí)現(xiàn)地圖數(shù)據(jù)可視化時(shí),地圖上會(huì)有極小的區(qū)域顯示稍有錯(cuò)誤的值。對(duì)于我的應(yīng)用程序而言,這不是個(gè)問(wèn)題,因此我便忽略而過(guò)了。如果您的應(yīng)用程序在這方面的準(zhǔn)確性至關(guān)重要,或者是多邊形島很普遍的映射區(qū)域,則只需控制地圖的呈現(xiàn)順序便能輕松解決這一問(wèn)題。首先繪制包含島的多邊形,然后繪制上部的島,這樣便可以準(zhǔn)確呈現(xiàn)多邊形島。在很多情況下,這可能需要額外的處理工作,以便確定重疊區(qū)域并構(gòu)造出適當(dāng)?shù)捻樞颉?div style="height:15px;">
導(dǎo)入數(shù)據(jù)使用 LINQ 將地圖數(shù)據(jù)導(dǎo)入應(yīng)用程序比較簡(jiǎn)單。執(zhí)行這一操作的代碼如圖 8 所示。它定義了接受以下兩個(gè)參數(shù)的過(guò)程 LoadFile:file 和 list。file 參數(shù)是一個(gè)字符串,包含從磁盤(pán)加載 XML 文件的完整路徑。list 參數(shù)則是對(duì) MapRegion 實(shí)例集合的引用。該過(guò)程會(huì)打開(kāi) XML 文件進(jìn)行處理,并將其定義的所有區(qū)域插入到提供的集合。LoadFile 主體極其簡(jiǎn)單。此過(guò)程的工作方式如下:首先將提供的文件加載到 XDocument 實(shí)例,定義查詢以遍歷文檔并將其轉(zhuǎn)換為 MapRegion 實(shí)例集合,最后執(zhí)行查詢并將其結(jié)果插入提供的輸出集合。該查詢由多個(gè)不同的片斷組成。第一個(gè)片斷是 From 子句,使用 XML 成員訪問(wèn)表達(dá)式 doc.<File>.<Region>.<Polygon>.<Vertex> 來(lái)檢索文檔中所有的多邊形 Vertex 標(biāo)記,作為查詢的基礎(chǔ)。值得注意的是,它并沒(méi)有使用 doc...<Vertex>,該表達(dá)式可以獲取文檔中所有的 Vertex 標(biāo)記(包括 Island 標(biāo)記內(nèi)部所定義的)。實(shí)際上,它只包括 Polygon 標(biāo)記內(nèi)部直接定義的 Vertex 標(biāo)記。查詢的第二部分是 Let 子句。此子句引入了兩個(gè)新變量,Polygon 和 Region,它們已分配給包含每個(gè)頂點(diǎn)的 Polygon 和 Region 標(biāo)記。查詢的第三部分是首個(gè) Group By 子句。它能夠輕松地將頂點(diǎn)列表按其包含的多邊形進(jìn)行分組,然后計(jì)算多邊形的邊界框。包含多邊形的區(qū)域也包含在分組鍵中,使它能夠用于未來(lái)的查詢。多邊形的邊界框是通過(guò)計(jì)算其頂點(diǎn) X 和 Y 屬性的最小和最大值來(lái)計(jì)算的。這通過(guò)調(diào)用自定義聚合函數(shù) MinDBL 和 MaxDBL 來(lái)完成,兩者由兩個(gè)擴(kuò)展方法所定義:<Extension()> _Function MinDBL(Of T)(ByVal x As IEnumerable(Of T), _ByVal y As Func(Of T, String)) As DoubleReturn x.Min(Function(z) CDbl(y(z)))End Function<Extension()> _Function MaxDBL(Of T)(ByVal x As IEnumerable(Of T), _ByVal y As Func(Of T, String)) As DoubleReturn x.Max(Function(z) CDbl(y(z)))End Function 這些方法用于定義聚合函數(shù),聚合函數(shù)首先將提供的參數(shù)轉(zhuǎn)換為 Double 類(lèi)型,再根據(jù)它計(jì)算最小或最大值。Group By 子句之后是 Select 子句,可按結(jié)果將原始分組轉(zhuǎn)換為更好用的格式。特別是,它定義了一個(gè)子查詢,使用 Group By 產(chǎn)生的分組,只從中投影出頂點(diǎn),然后使用 ToMapPolygon 擴(kuò)展方法將產(chǎn)生的集合轉(zhuǎn)換為 MapPolygon 類(lèi)的實(shí)例:<Extension()> _Function ToMapPolygon(ByVal items As IEnumerable(Of XElement)) As _MapPolygonDim ret As New MapPolygonret.Points.AddRange(From item In items Select item.ToPoint())Return retEnd Function<Extension()> _Function ToPoint(ByVal x As XElement) As PointReturn New Point(x.@X, x.@Y)End Function 它還會(huì)將原始 MinX、MinY、MaxX 和 MaxY 變量轉(zhuǎn)換為 Rect 類(lèi)的實(shí)例。查詢的下一部分是它的第二個(gè) Group By 子句,可聚合先前根據(jù) Region 選擇的結(jié)果,并計(jì)算包含所有區(qū)域多邊形的邊界框。邊界框是通過(guò) Enclose 自定義聚合函數(shù)計(jì)算的,該函數(shù)定義如圖 9 所示。查詢的最后一部分是最終的 Select 子句,可為從第二個(gè) Group By 返回的每個(gè)結(jié)果構(gòu)造 MapRegion 實(shí)例。它通過(guò)兩個(gè)子查詢來(lái)完成這項(xiàng)工作,其中一個(gè)可投影出存儲(chǔ)于每個(gè) Group 成員中的多邊形實(shí)例,另一個(gè)則提取存儲(chǔ)于 Region 中的一組 FIPS 代碼。