From b71faf5486003f318d6d2c63382ca4226d02a6cd Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Thu, 19 Dec 2019 14:12:32 +0100 Subject: [PATCH 01/51] Fix SharpMapServer project --- SharpMapServer/SharpMapServer.csproj | 34 +++++++++++++++++++++------- SharpMapServer/Web.config | 17 ++++++++++---- SharpMapServer/packages.config | 6 ++++- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/SharpMapServer/SharpMapServer.csproj b/SharpMapServer/SharpMapServer.csproj index 2e2aed09..80b5146a 100644 --- a/SharpMapServer/SharpMapServer.csproj +++ b/SharpMapServer/SharpMapServer.csproj @@ -1,5 +1,5 @@  - + Debug @@ -13,7 +13,7 @@ Properties SharpMapServer SharpMapServer - v4.0 + v4.7.2 false @@ -27,6 +27,7 @@ + true @@ -36,6 +37,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -44,26 +46,42 @@ TRACE prompt 4 + false + + ..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll + + + ..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll + ..\packages\GeoAPI.Core.1.7.5\lib\net40-client\GeoAPI.dll ..\packages\GeoAPI.CoordinateSystems.1.7.5\lib\net40-client\GeoAPI.CoordinateSystems.dll - - ..\packages\NetTopologySuite.Core.1.15.1\lib\net40-client\NetTopologySuite.dll + + + ..\packages\NetTopologySuite.Core.1.15.3\lib\net45\NetTopologySuite.dll ..\packages\ProjNET4GeoAPI.1.4.1\lib\net40-client\ProjNET.dll - - + + + + ..\packages\System.Drawing.Common.4.6.0\lib\net461\System.Drawing.Common.dll + + + + + + @@ -189,8 +207,8 @@ SharpMap.Web - {C83777FC-AABB-47D9-911F-D76255D4D541} - SharpMap.VS2010 + {c83777fc-aabb-47d9-911f-d76255d4d541} + SharpMap diff --git a/SharpMapServer/Web.config b/SharpMapServer/Web.config index ba96a0c8..a145eaf4 100644 --- a/SharpMapServer/Web.config +++ b/SharpMapServer/Web.config @@ -1,4 +1,4 @@ - + - + + - + \ No newline at end of file diff --git a/SharpMapServer/packages.config b/SharpMapServer/packages.config index 27be6d6c..483c46fc 100644 --- a/SharpMapServer/packages.config +++ b/SharpMapServer/packages.config @@ -1,8 +1,12 @@  + + - + + + \ No newline at end of file From 479c420c39255e1ba3c20f703225d6069701d272 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Thu, 19 Dec 2019 14:26:48 +0100 Subject: [PATCH 02/51] Rename SharpMapServer SharpMap.Server --- SharpMapServer.sln => SharpMap.Server.sln | 2 +- .../AdminServices.cs | 0 .../App_Data/settings.xml | 0 {SharpMapServer => SharpMap.Server}/Default.aspx | 0 {SharpMapServer => SharpMap.Server}/Default.aspx.cs | 0 .../Default.aspx.designer.cs | 0 {SharpMapServer => SharpMap.Server}/Demo.aspx | 0 {SharpMapServer => SharpMap.Server}/Demo.aspx.cs | 0 .../Demo.aspx.designer.cs | 0 .../External References/System.Data.SQLite.DLL | Bin .../External References/System.Data.SQLite.Linq.dll | Bin {SharpMapServer => SharpMap.Server}/Global.asax | 0 {SharpMapServer => SharpMap.Server}/Global.asax.cs | 0 .../Model/SharpMapContext.cs | 0 {SharpMapServer => SharpMap.Server}/Model/User.cs | 0 .../Model/WmsCapabilities.cs | 0 .../Model/WmsLayer.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../SharpMap.Server.csproj | 0 {SharpMapServer => SharpMap.Server}/WMSServer.cs | 0 .../Web.Debug.config | 0 .../Web.Release.config | 0 {SharpMapServer => SharpMap.Server}/Web.config | 0 .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin .../images/ui-bg_glass_65_ffffff_1x400.png | Bin .../images/ui-bg_glass_75_dadada_1x400.png | Bin .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin .../images/ui-bg_glass_75_ffffff_1x400.png | Bin .../images/ui-bg_highlight-soft_75_cccccc_1x100.png | Bin .../images/ui-bg_inset-soft_95_fef1ec_1x100.png | Bin .../custom-theme/images/ui-icons_222222_256x240.png | Bin .../custom-theme/images/ui-icons_2e83ff_256x240.png | Bin .../custom-theme/images/ui-icons_454545_256x240.png | Bin .../custom-theme/images/ui-icons_888888_256x240.png | Bin .../custom-theme/images/ui-icons_cd0a0a_256x240.png | Bin .../css/custom-theme/jquery-ui-1.8.16.custom.css | 0 .../css/img/add_point_off.png | Bin .../css/img/add_point_on.png | Bin .../css/img/blank.gif | Bin .../css/img/close.gif | Bin .../css/img/drag-rectangle-off.png | Bin .../css/img/drag-rectangle-on.png | Bin .../css/img/draw_line_off.png | Bin .../css/img/draw_line_on.png | Bin .../css/img/draw_point_off.png | Bin .../css/img/draw_point_on.png | Bin .../css/img/draw_polygon_off.png | Bin .../css/img/draw_polygon_on.png | Bin .../css/img/editing_tool_bar.png | Bin .../css/img/move_feature_off.png | Bin .../css/img/move_feature_on.png | Bin .../css/img/navigation_history.png | Bin .../css/img/overview_replacement.gif | Bin .../css/img/pan-panel-NOALPHA.png | Bin .../css/img/pan-panel.png | Bin .../css/img/pan_off.png | Bin .../css/img/pan_on.png | Bin .../css/img/panning-hand-off.png | Bin .../css/img/panning-hand-on.png | Bin .../css/img/remove_point_off.png | Bin .../css/img/remove_point_on.png | Bin .../css/img/ruler.png | Bin .../css/img/save_features_off.png | Bin .../css/img/save_features_on.png | Bin .../css/img/view_next_off.png | Bin .../css/img/view_next_on.png | Bin .../css/img/view_previous_off.png | Bin .../css/img/view_previous_on.png | Bin .../css/img/zoom-panel-NOALPHA.png | Bin .../css/img/zoom-panel.png | Bin .../css/openlayers.css | 0 .../images/SharpMapBanner.png | Bin .../images/blank.gif | Bin .../images/cloud-popup-relative.png | Bin .../images/drag-rectangle-off.png | Bin .../images/drag-rectangle-on.png | Bin .../images/east-mini.png | Bin .../images/layer-switcher-maximize.png | Bin .../images/layer-switcher-minimize.png | Bin .../images/marker-blue.png | Bin .../images/marker-gold.png | Bin .../images/marker-green.png | Bin .../images/marker.png | Bin .../images/measuring-stick-off.png | Bin .../images/measuring-stick-on.png | Bin .../images/north-mini.png | Bin .../images/panning-hand-off.png | Bin .../images/panning-hand-on.png | Bin .../images/slider.png | Bin .../images/south-mini.png | Bin .../images/west-mini.png | Bin .../images/zoom-minus-mini.png | Bin .../images/zoom-plus-mini.png | Bin .../images/zoom-world-mini.png | Bin .../images/zoombar.png | Bin .../js/OpenLayers.js | 0 .../js/SharpMapServer.js | 0 .../js/jquery-1.6.2.min.js | 0 .../js/jquery-1.6.4.js | 0 .../js/jquery-ui-1.8.16.custom.min.js | 0 {SharpMapServer => SharpMap.Server}/packages.config | 0 {SharpMapServer => SharpMap.Server}/states.dbf | Bin {SharpMapServer => SharpMap.Server}/states.prj | Bin {SharpMapServer => SharpMap.Server}/states.shp | Bin {SharpMapServer => SharpMap.Server}/states.shx | Bin 106 files changed, 1 insertion(+), 1 deletion(-) rename SharpMapServer.sln => SharpMap.Server.sln (93%) rename {SharpMapServer => SharpMap.Server}/AdminServices.cs (100%) rename {SharpMapServer => SharpMap.Server}/App_Data/settings.xml (100%) rename {SharpMapServer => SharpMap.Server}/Default.aspx (100%) rename {SharpMapServer => SharpMap.Server}/Default.aspx.cs (100%) rename {SharpMapServer => SharpMap.Server}/Default.aspx.designer.cs (100%) rename {SharpMapServer => SharpMap.Server}/Demo.aspx (100%) rename {SharpMapServer => SharpMap.Server}/Demo.aspx.cs (100%) rename {SharpMapServer => SharpMap.Server}/Demo.aspx.designer.cs (100%) rename {SharpMapServer => SharpMap.Server}/External References/System.Data.SQLite.DLL (100%) rename {SharpMapServer => SharpMap.Server}/External References/System.Data.SQLite.Linq.dll (100%) rename {SharpMapServer => SharpMap.Server}/Global.asax (100%) rename {SharpMapServer => SharpMap.Server}/Global.asax.cs (100%) rename {SharpMapServer => SharpMap.Server}/Model/SharpMapContext.cs (100%) rename {SharpMapServer => SharpMap.Server}/Model/User.cs (100%) rename {SharpMapServer => SharpMap.Server}/Model/WmsCapabilities.cs (100%) rename {SharpMapServer => SharpMap.Server}/Model/WmsLayer.cs (100%) rename {SharpMapServer => SharpMap.Server}/Properties/AssemblyInfo.cs (100%) rename SharpMapServer/SharpMapServer.csproj => SharpMap.Server/SharpMap.Server.csproj (100%) rename {SharpMapServer => SharpMap.Server}/WMSServer.cs (100%) rename {SharpMapServer => SharpMap.Server}/Web.Debug.config (100%) rename {SharpMapServer => SharpMap.Server}/Web.Release.config (100%) rename {SharpMapServer => SharpMap.Server}/Web.config (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_glass_75_ffffff_1x400.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-icons_222222_256x240.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-icons_2e83ff_256x240.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-icons_454545_256x240.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-icons_888888_256x240.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/images/ui-icons_cd0a0a_256x240.png (100%) rename {SharpMapServer => SharpMap.Server}/css/custom-theme/jquery-ui-1.8.16.custom.css (100%) rename {SharpMapServer => SharpMap.Server}/css/img/add_point_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/add_point_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/blank.gif (100%) rename {SharpMapServer => SharpMap.Server}/css/img/close.gif (100%) rename {SharpMapServer => SharpMap.Server}/css/img/drag-rectangle-off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/drag-rectangle-on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_line_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_line_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_point_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_point_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_polygon_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/draw_polygon_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/editing_tool_bar.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/move_feature_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/move_feature_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/navigation_history.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/overview_replacement.gif (100%) rename {SharpMapServer => SharpMap.Server}/css/img/pan-panel-NOALPHA.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/pan-panel.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/pan_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/pan_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/panning-hand-off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/panning-hand-on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/remove_point_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/remove_point_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/ruler.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/save_features_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/save_features_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/view_next_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/view_next_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/view_previous_off.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/view_previous_on.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/zoom-panel-NOALPHA.png (100%) rename {SharpMapServer => SharpMap.Server}/css/img/zoom-panel.png (100%) rename {SharpMapServer => SharpMap.Server}/css/openlayers.css (100%) rename {SharpMapServer => SharpMap.Server}/images/SharpMapBanner.png (100%) rename {SharpMapServer => SharpMap.Server}/images/blank.gif (100%) rename {SharpMapServer => SharpMap.Server}/images/cloud-popup-relative.png (100%) rename {SharpMapServer => SharpMap.Server}/images/drag-rectangle-off.png (100%) rename {SharpMapServer => SharpMap.Server}/images/drag-rectangle-on.png (100%) rename {SharpMapServer => SharpMap.Server}/images/east-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/layer-switcher-maximize.png (100%) rename {SharpMapServer => SharpMap.Server}/images/layer-switcher-minimize.png (100%) rename {SharpMapServer => SharpMap.Server}/images/marker-blue.png (100%) rename {SharpMapServer => SharpMap.Server}/images/marker-gold.png (100%) rename {SharpMapServer => SharpMap.Server}/images/marker-green.png (100%) rename {SharpMapServer => SharpMap.Server}/images/marker.png (100%) rename {SharpMapServer => SharpMap.Server}/images/measuring-stick-off.png (100%) rename {SharpMapServer => SharpMap.Server}/images/measuring-stick-on.png (100%) rename {SharpMapServer => SharpMap.Server}/images/north-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/panning-hand-off.png (100%) rename {SharpMapServer => SharpMap.Server}/images/panning-hand-on.png (100%) rename {SharpMapServer => SharpMap.Server}/images/slider.png (100%) rename {SharpMapServer => SharpMap.Server}/images/south-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/west-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/zoom-minus-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/zoom-plus-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/zoom-world-mini.png (100%) rename {SharpMapServer => SharpMap.Server}/images/zoombar.png (100%) rename {SharpMapServer => SharpMap.Server}/js/OpenLayers.js (100%) rename {SharpMapServer => SharpMap.Server}/js/SharpMapServer.js (100%) rename {SharpMapServer => SharpMap.Server}/js/jquery-1.6.2.min.js (100%) rename {SharpMapServer => SharpMap.Server}/js/jquery-1.6.4.js (100%) rename {SharpMapServer => SharpMap.Server}/js/jquery-ui-1.8.16.custom.min.js (100%) rename {SharpMapServer => SharpMap.Server}/packages.config (100%) rename {SharpMapServer => SharpMap.Server}/states.dbf (100%) rename {SharpMapServer => SharpMap.Server}/states.prj (100%) rename {SharpMapServer => SharpMap.Server}/states.shp (100%) rename {SharpMapServer => SharpMap.Server}/states.shx (100%) diff --git a/SharpMapServer.sln b/SharpMap.Server.sln similarity index 93% rename from SharpMapServer.sln rename to SharpMap.Server.sln index bce85f19..00a48c30 100644 --- a/SharpMapServer.sln +++ b/SharpMap.Server.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.27703.2047 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMap", "SharpMap\SharpMap.csproj", "{C83777FC-AABB-47D9-911F-D76255D4D541}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMapServer", "SharpMapServer\SharpMapServer.csproj", "{FCA0E48D-9803-4F63-BA14-AB8F8810709B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMap.Server", "SharpMap.Server\SharpMap.Server.csproj", "{FCA0E48D-9803-4F63-BA14-AB8F8810709B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpMap.Web", "SharpMap.Web\SharpMap.Web.csproj", "{292EF671-4063-4952-8DE0-423DF72A0950}" EndProject diff --git a/SharpMapServer/AdminServices.cs b/SharpMap.Server/AdminServices.cs similarity index 100% rename from SharpMapServer/AdminServices.cs rename to SharpMap.Server/AdminServices.cs diff --git a/SharpMapServer/App_Data/settings.xml b/SharpMap.Server/App_Data/settings.xml similarity index 100% rename from SharpMapServer/App_Data/settings.xml rename to SharpMap.Server/App_Data/settings.xml diff --git a/SharpMapServer/Default.aspx b/SharpMap.Server/Default.aspx similarity index 100% rename from SharpMapServer/Default.aspx rename to SharpMap.Server/Default.aspx diff --git a/SharpMapServer/Default.aspx.cs b/SharpMap.Server/Default.aspx.cs similarity index 100% rename from SharpMapServer/Default.aspx.cs rename to SharpMap.Server/Default.aspx.cs diff --git a/SharpMapServer/Default.aspx.designer.cs b/SharpMap.Server/Default.aspx.designer.cs similarity index 100% rename from SharpMapServer/Default.aspx.designer.cs rename to SharpMap.Server/Default.aspx.designer.cs diff --git a/SharpMapServer/Demo.aspx b/SharpMap.Server/Demo.aspx similarity index 100% rename from SharpMapServer/Demo.aspx rename to SharpMap.Server/Demo.aspx diff --git a/SharpMapServer/Demo.aspx.cs b/SharpMap.Server/Demo.aspx.cs similarity index 100% rename from SharpMapServer/Demo.aspx.cs rename to SharpMap.Server/Demo.aspx.cs diff --git a/SharpMapServer/Demo.aspx.designer.cs b/SharpMap.Server/Demo.aspx.designer.cs similarity index 100% rename from SharpMapServer/Demo.aspx.designer.cs rename to SharpMap.Server/Demo.aspx.designer.cs diff --git a/SharpMapServer/External References/System.Data.SQLite.DLL b/SharpMap.Server/External References/System.Data.SQLite.DLL similarity index 100% rename from SharpMapServer/External References/System.Data.SQLite.DLL rename to SharpMap.Server/External References/System.Data.SQLite.DLL diff --git a/SharpMapServer/External References/System.Data.SQLite.Linq.dll b/SharpMap.Server/External References/System.Data.SQLite.Linq.dll similarity index 100% rename from SharpMapServer/External References/System.Data.SQLite.Linq.dll rename to SharpMap.Server/External References/System.Data.SQLite.Linq.dll diff --git a/SharpMapServer/Global.asax b/SharpMap.Server/Global.asax similarity index 100% rename from SharpMapServer/Global.asax rename to SharpMap.Server/Global.asax diff --git a/SharpMapServer/Global.asax.cs b/SharpMap.Server/Global.asax.cs similarity index 100% rename from SharpMapServer/Global.asax.cs rename to SharpMap.Server/Global.asax.cs diff --git a/SharpMapServer/Model/SharpMapContext.cs b/SharpMap.Server/Model/SharpMapContext.cs similarity index 100% rename from SharpMapServer/Model/SharpMapContext.cs rename to SharpMap.Server/Model/SharpMapContext.cs diff --git a/SharpMapServer/Model/User.cs b/SharpMap.Server/Model/User.cs similarity index 100% rename from SharpMapServer/Model/User.cs rename to SharpMap.Server/Model/User.cs diff --git a/SharpMapServer/Model/WmsCapabilities.cs b/SharpMap.Server/Model/WmsCapabilities.cs similarity index 100% rename from SharpMapServer/Model/WmsCapabilities.cs rename to SharpMap.Server/Model/WmsCapabilities.cs diff --git a/SharpMapServer/Model/WmsLayer.cs b/SharpMap.Server/Model/WmsLayer.cs similarity index 100% rename from SharpMapServer/Model/WmsLayer.cs rename to SharpMap.Server/Model/WmsLayer.cs diff --git a/SharpMapServer/Properties/AssemblyInfo.cs b/SharpMap.Server/Properties/AssemblyInfo.cs similarity index 100% rename from SharpMapServer/Properties/AssemblyInfo.cs rename to SharpMap.Server/Properties/AssemblyInfo.cs diff --git a/SharpMapServer/SharpMapServer.csproj b/SharpMap.Server/SharpMap.Server.csproj similarity index 100% rename from SharpMapServer/SharpMapServer.csproj rename to SharpMap.Server/SharpMap.Server.csproj diff --git a/SharpMapServer/WMSServer.cs b/SharpMap.Server/WMSServer.cs similarity index 100% rename from SharpMapServer/WMSServer.cs rename to SharpMap.Server/WMSServer.cs diff --git a/SharpMapServer/Web.Debug.config b/SharpMap.Server/Web.Debug.config similarity index 100% rename from SharpMapServer/Web.Debug.config rename to SharpMap.Server/Web.Debug.config diff --git a/SharpMapServer/Web.Release.config b/SharpMap.Server/Web.Release.config similarity index 100% rename from SharpMapServer/Web.Release.config rename to SharpMap.Server/Web.Release.config diff --git a/SharpMapServer/Web.config b/SharpMap.Server/Web.config similarity index 100% rename from SharpMapServer/Web.config rename to SharpMap.Server/Web.config diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png b/SharpMap.Server/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png b/SharpMap.Server/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png b/SharpMap.Server/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png b/SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png b/SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_glass_75_ffffff_1x400.png b/SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_ffffff_1x400.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_glass_75_ffffff_1x400.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_glass_75_ffffff_1x400.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/SharpMap.Server/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/SharpMapServer/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png b/SharpMap.Server/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png rename to SharpMap.Server/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png diff --git a/SharpMapServer/css/custom-theme/images/ui-icons_222222_256x240.png b/SharpMap.Server/css/custom-theme/images/ui-icons_222222_256x240.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-icons_222222_256x240.png rename to SharpMap.Server/css/custom-theme/images/ui-icons_222222_256x240.png diff --git a/SharpMapServer/css/custom-theme/images/ui-icons_2e83ff_256x240.png b/SharpMap.Server/css/custom-theme/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-icons_2e83ff_256x240.png rename to SharpMap.Server/css/custom-theme/images/ui-icons_2e83ff_256x240.png diff --git a/SharpMapServer/css/custom-theme/images/ui-icons_454545_256x240.png b/SharpMap.Server/css/custom-theme/images/ui-icons_454545_256x240.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-icons_454545_256x240.png rename to SharpMap.Server/css/custom-theme/images/ui-icons_454545_256x240.png diff --git a/SharpMapServer/css/custom-theme/images/ui-icons_888888_256x240.png b/SharpMap.Server/css/custom-theme/images/ui-icons_888888_256x240.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-icons_888888_256x240.png rename to SharpMap.Server/css/custom-theme/images/ui-icons_888888_256x240.png diff --git a/SharpMapServer/css/custom-theme/images/ui-icons_cd0a0a_256x240.png b/SharpMap.Server/css/custom-theme/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from SharpMapServer/css/custom-theme/images/ui-icons_cd0a0a_256x240.png rename to SharpMap.Server/css/custom-theme/images/ui-icons_cd0a0a_256x240.png diff --git a/SharpMapServer/css/custom-theme/jquery-ui-1.8.16.custom.css b/SharpMap.Server/css/custom-theme/jquery-ui-1.8.16.custom.css similarity index 100% rename from SharpMapServer/css/custom-theme/jquery-ui-1.8.16.custom.css rename to SharpMap.Server/css/custom-theme/jquery-ui-1.8.16.custom.css diff --git a/SharpMapServer/css/img/add_point_off.png b/SharpMap.Server/css/img/add_point_off.png similarity index 100% rename from SharpMapServer/css/img/add_point_off.png rename to SharpMap.Server/css/img/add_point_off.png diff --git a/SharpMapServer/css/img/add_point_on.png b/SharpMap.Server/css/img/add_point_on.png similarity index 100% rename from SharpMapServer/css/img/add_point_on.png rename to SharpMap.Server/css/img/add_point_on.png diff --git a/SharpMapServer/css/img/blank.gif b/SharpMap.Server/css/img/blank.gif similarity index 100% rename from SharpMapServer/css/img/blank.gif rename to SharpMap.Server/css/img/blank.gif diff --git a/SharpMapServer/css/img/close.gif b/SharpMap.Server/css/img/close.gif similarity index 100% rename from SharpMapServer/css/img/close.gif rename to SharpMap.Server/css/img/close.gif diff --git a/SharpMapServer/css/img/drag-rectangle-off.png b/SharpMap.Server/css/img/drag-rectangle-off.png similarity index 100% rename from SharpMapServer/css/img/drag-rectangle-off.png rename to SharpMap.Server/css/img/drag-rectangle-off.png diff --git a/SharpMapServer/css/img/drag-rectangle-on.png b/SharpMap.Server/css/img/drag-rectangle-on.png similarity index 100% rename from SharpMapServer/css/img/drag-rectangle-on.png rename to SharpMap.Server/css/img/drag-rectangle-on.png diff --git a/SharpMapServer/css/img/draw_line_off.png b/SharpMap.Server/css/img/draw_line_off.png similarity index 100% rename from SharpMapServer/css/img/draw_line_off.png rename to SharpMap.Server/css/img/draw_line_off.png diff --git a/SharpMapServer/css/img/draw_line_on.png b/SharpMap.Server/css/img/draw_line_on.png similarity index 100% rename from SharpMapServer/css/img/draw_line_on.png rename to SharpMap.Server/css/img/draw_line_on.png diff --git a/SharpMapServer/css/img/draw_point_off.png b/SharpMap.Server/css/img/draw_point_off.png similarity index 100% rename from SharpMapServer/css/img/draw_point_off.png rename to SharpMap.Server/css/img/draw_point_off.png diff --git a/SharpMapServer/css/img/draw_point_on.png b/SharpMap.Server/css/img/draw_point_on.png similarity index 100% rename from SharpMapServer/css/img/draw_point_on.png rename to SharpMap.Server/css/img/draw_point_on.png diff --git a/SharpMapServer/css/img/draw_polygon_off.png b/SharpMap.Server/css/img/draw_polygon_off.png similarity index 100% rename from SharpMapServer/css/img/draw_polygon_off.png rename to SharpMap.Server/css/img/draw_polygon_off.png diff --git a/SharpMapServer/css/img/draw_polygon_on.png b/SharpMap.Server/css/img/draw_polygon_on.png similarity index 100% rename from SharpMapServer/css/img/draw_polygon_on.png rename to SharpMap.Server/css/img/draw_polygon_on.png diff --git a/SharpMapServer/css/img/editing_tool_bar.png b/SharpMap.Server/css/img/editing_tool_bar.png similarity index 100% rename from SharpMapServer/css/img/editing_tool_bar.png rename to SharpMap.Server/css/img/editing_tool_bar.png diff --git a/SharpMapServer/css/img/move_feature_off.png b/SharpMap.Server/css/img/move_feature_off.png similarity index 100% rename from SharpMapServer/css/img/move_feature_off.png rename to SharpMap.Server/css/img/move_feature_off.png diff --git a/SharpMapServer/css/img/move_feature_on.png b/SharpMap.Server/css/img/move_feature_on.png similarity index 100% rename from SharpMapServer/css/img/move_feature_on.png rename to SharpMap.Server/css/img/move_feature_on.png diff --git a/SharpMapServer/css/img/navigation_history.png b/SharpMap.Server/css/img/navigation_history.png similarity index 100% rename from SharpMapServer/css/img/navigation_history.png rename to SharpMap.Server/css/img/navigation_history.png diff --git a/SharpMapServer/css/img/overview_replacement.gif b/SharpMap.Server/css/img/overview_replacement.gif similarity index 100% rename from SharpMapServer/css/img/overview_replacement.gif rename to SharpMap.Server/css/img/overview_replacement.gif diff --git a/SharpMapServer/css/img/pan-panel-NOALPHA.png b/SharpMap.Server/css/img/pan-panel-NOALPHA.png similarity index 100% rename from SharpMapServer/css/img/pan-panel-NOALPHA.png rename to SharpMap.Server/css/img/pan-panel-NOALPHA.png diff --git a/SharpMapServer/css/img/pan-panel.png b/SharpMap.Server/css/img/pan-panel.png similarity index 100% rename from SharpMapServer/css/img/pan-panel.png rename to SharpMap.Server/css/img/pan-panel.png diff --git a/SharpMapServer/css/img/pan_off.png b/SharpMap.Server/css/img/pan_off.png similarity index 100% rename from SharpMapServer/css/img/pan_off.png rename to SharpMap.Server/css/img/pan_off.png diff --git a/SharpMapServer/css/img/pan_on.png b/SharpMap.Server/css/img/pan_on.png similarity index 100% rename from SharpMapServer/css/img/pan_on.png rename to SharpMap.Server/css/img/pan_on.png diff --git a/SharpMapServer/css/img/panning-hand-off.png b/SharpMap.Server/css/img/panning-hand-off.png similarity index 100% rename from SharpMapServer/css/img/panning-hand-off.png rename to SharpMap.Server/css/img/panning-hand-off.png diff --git a/SharpMapServer/css/img/panning-hand-on.png b/SharpMap.Server/css/img/panning-hand-on.png similarity index 100% rename from SharpMapServer/css/img/panning-hand-on.png rename to SharpMap.Server/css/img/panning-hand-on.png diff --git a/SharpMapServer/css/img/remove_point_off.png b/SharpMap.Server/css/img/remove_point_off.png similarity index 100% rename from SharpMapServer/css/img/remove_point_off.png rename to SharpMap.Server/css/img/remove_point_off.png diff --git a/SharpMapServer/css/img/remove_point_on.png b/SharpMap.Server/css/img/remove_point_on.png similarity index 100% rename from SharpMapServer/css/img/remove_point_on.png rename to SharpMap.Server/css/img/remove_point_on.png diff --git a/SharpMapServer/css/img/ruler.png b/SharpMap.Server/css/img/ruler.png similarity index 100% rename from SharpMapServer/css/img/ruler.png rename to SharpMap.Server/css/img/ruler.png diff --git a/SharpMapServer/css/img/save_features_off.png b/SharpMap.Server/css/img/save_features_off.png similarity index 100% rename from SharpMapServer/css/img/save_features_off.png rename to SharpMap.Server/css/img/save_features_off.png diff --git a/SharpMapServer/css/img/save_features_on.png b/SharpMap.Server/css/img/save_features_on.png similarity index 100% rename from SharpMapServer/css/img/save_features_on.png rename to SharpMap.Server/css/img/save_features_on.png diff --git a/SharpMapServer/css/img/view_next_off.png b/SharpMap.Server/css/img/view_next_off.png similarity index 100% rename from SharpMapServer/css/img/view_next_off.png rename to SharpMap.Server/css/img/view_next_off.png diff --git a/SharpMapServer/css/img/view_next_on.png b/SharpMap.Server/css/img/view_next_on.png similarity index 100% rename from SharpMapServer/css/img/view_next_on.png rename to SharpMap.Server/css/img/view_next_on.png diff --git a/SharpMapServer/css/img/view_previous_off.png b/SharpMap.Server/css/img/view_previous_off.png similarity index 100% rename from SharpMapServer/css/img/view_previous_off.png rename to SharpMap.Server/css/img/view_previous_off.png diff --git a/SharpMapServer/css/img/view_previous_on.png b/SharpMap.Server/css/img/view_previous_on.png similarity index 100% rename from SharpMapServer/css/img/view_previous_on.png rename to SharpMap.Server/css/img/view_previous_on.png diff --git a/SharpMapServer/css/img/zoom-panel-NOALPHA.png b/SharpMap.Server/css/img/zoom-panel-NOALPHA.png similarity index 100% rename from SharpMapServer/css/img/zoom-panel-NOALPHA.png rename to SharpMap.Server/css/img/zoom-panel-NOALPHA.png diff --git a/SharpMapServer/css/img/zoom-panel.png b/SharpMap.Server/css/img/zoom-panel.png similarity index 100% rename from SharpMapServer/css/img/zoom-panel.png rename to SharpMap.Server/css/img/zoom-panel.png diff --git a/SharpMapServer/css/openlayers.css b/SharpMap.Server/css/openlayers.css similarity index 100% rename from SharpMapServer/css/openlayers.css rename to SharpMap.Server/css/openlayers.css diff --git a/SharpMapServer/images/SharpMapBanner.png b/SharpMap.Server/images/SharpMapBanner.png similarity index 100% rename from SharpMapServer/images/SharpMapBanner.png rename to SharpMap.Server/images/SharpMapBanner.png diff --git a/SharpMapServer/images/blank.gif b/SharpMap.Server/images/blank.gif similarity index 100% rename from SharpMapServer/images/blank.gif rename to SharpMap.Server/images/blank.gif diff --git a/SharpMapServer/images/cloud-popup-relative.png b/SharpMap.Server/images/cloud-popup-relative.png similarity index 100% rename from SharpMapServer/images/cloud-popup-relative.png rename to SharpMap.Server/images/cloud-popup-relative.png diff --git a/SharpMapServer/images/drag-rectangle-off.png b/SharpMap.Server/images/drag-rectangle-off.png similarity index 100% rename from SharpMapServer/images/drag-rectangle-off.png rename to SharpMap.Server/images/drag-rectangle-off.png diff --git a/SharpMapServer/images/drag-rectangle-on.png b/SharpMap.Server/images/drag-rectangle-on.png similarity index 100% rename from SharpMapServer/images/drag-rectangle-on.png rename to SharpMap.Server/images/drag-rectangle-on.png diff --git a/SharpMapServer/images/east-mini.png b/SharpMap.Server/images/east-mini.png similarity index 100% rename from SharpMapServer/images/east-mini.png rename to SharpMap.Server/images/east-mini.png diff --git a/SharpMapServer/images/layer-switcher-maximize.png b/SharpMap.Server/images/layer-switcher-maximize.png similarity index 100% rename from SharpMapServer/images/layer-switcher-maximize.png rename to SharpMap.Server/images/layer-switcher-maximize.png diff --git a/SharpMapServer/images/layer-switcher-minimize.png b/SharpMap.Server/images/layer-switcher-minimize.png similarity index 100% rename from SharpMapServer/images/layer-switcher-minimize.png rename to SharpMap.Server/images/layer-switcher-minimize.png diff --git a/SharpMapServer/images/marker-blue.png b/SharpMap.Server/images/marker-blue.png similarity index 100% rename from SharpMapServer/images/marker-blue.png rename to SharpMap.Server/images/marker-blue.png diff --git a/SharpMapServer/images/marker-gold.png b/SharpMap.Server/images/marker-gold.png similarity index 100% rename from SharpMapServer/images/marker-gold.png rename to SharpMap.Server/images/marker-gold.png diff --git a/SharpMapServer/images/marker-green.png b/SharpMap.Server/images/marker-green.png similarity index 100% rename from SharpMapServer/images/marker-green.png rename to SharpMap.Server/images/marker-green.png diff --git a/SharpMapServer/images/marker.png b/SharpMap.Server/images/marker.png similarity index 100% rename from SharpMapServer/images/marker.png rename to SharpMap.Server/images/marker.png diff --git a/SharpMapServer/images/measuring-stick-off.png b/SharpMap.Server/images/measuring-stick-off.png similarity index 100% rename from SharpMapServer/images/measuring-stick-off.png rename to SharpMap.Server/images/measuring-stick-off.png diff --git a/SharpMapServer/images/measuring-stick-on.png b/SharpMap.Server/images/measuring-stick-on.png similarity index 100% rename from SharpMapServer/images/measuring-stick-on.png rename to SharpMap.Server/images/measuring-stick-on.png diff --git a/SharpMapServer/images/north-mini.png b/SharpMap.Server/images/north-mini.png similarity index 100% rename from SharpMapServer/images/north-mini.png rename to SharpMap.Server/images/north-mini.png diff --git a/SharpMapServer/images/panning-hand-off.png b/SharpMap.Server/images/panning-hand-off.png similarity index 100% rename from SharpMapServer/images/panning-hand-off.png rename to SharpMap.Server/images/panning-hand-off.png diff --git a/SharpMapServer/images/panning-hand-on.png b/SharpMap.Server/images/panning-hand-on.png similarity index 100% rename from SharpMapServer/images/panning-hand-on.png rename to SharpMap.Server/images/panning-hand-on.png diff --git a/SharpMapServer/images/slider.png b/SharpMap.Server/images/slider.png similarity index 100% rename from SharpMapServer/images/slider.png rename to SharpMap.Server/images/slider.png diff --git a/SharpMapServer/images/south-mini.png b/SharpMap.Server/images/south-mini.png similarity index 100% rename from SharpMapServer/images/south-mini.png rename to SharpMap.Server/images/south-mini.png diff --git a/SharpMapServer/images/west-mini.png b/SharpMap.Server/images/west-mini.png similarity index 100% rename from SharpMapServer/images/west-mini.png rename to SharpMap.Server/images/west-mini.png diff --git a/SharpMapServer/images/zoom-minus-mini.png b/SharpMap.Server/images/zoom-minus-mini.png similarity index 100% rename from SharpMapServer/images/zoom-minus-mini.png rename to SharpMap.Server/images/zoom-minus-mini.png diff --git a/SharpMapServer/images/zoom-plus-mini.png b/SharpMap.Server/images/zoom-plus-mini.png similarity index 100% rename from SharpMapServer/images/zoom-plus-mini.png rename to SharpMap.Server/images/zoom-plus-mini.png diff --git a/SharpMapServer/images/zoom-world-mini.png b/SharpMap.Server/images/zoom-world-mini.png similarity index 100% rename from SharpMapServer/images/zoom-world-mini.png rename to SharpMap.Server/images/zoom-world-mini.png diff --git a/SharpMapServer/images/zoombar.png b/SharpMap.Server/images/zoombar.png similarity index 100% rename from SharpMapServer/images/zoombar.png rename to SharpMap.Server/images/zoombar.png diff --git a/SharpMapServer/js/OpenLayers.js b/SharpMap.Server/js/OpenLayers.js similarity index 100% rename from SharpMapServer/js/OpenLayers.js rename to SharpMap.Server/js/OpenLayers.js diff --git a/SharpMapServer/js/SharpMapServer.js b/SharpMap.Server/js/SharpMapServer.js similarity index 100% rename from SharpMapServer/js/SharpMapServer.js rename to SharpMap.Server/js/SharpMapServer.js diff --git a/SharpMapServer/js/jquery-1.6.2.min.js b/SharpMap.Server/js/jquery-1.6.2.min.js similarity index 100% rename from SharpMapServer/js/jquery-1.6.2.min.js rename to SharpMap.Server/js/jquery-1.6.2.min.js diff --git a/SharpMapServer/js/jquery-1.6.4.js b/SharpMap.Server/js/jquery-1.6.4.js similarity index 100% rename from SharpMapServer/js/jquery-1.6.4.js rename to SharpMap.Server/js/jquery-1.6.4.js diff --git a/SharpMapServer/js/jquery-ui-1.8.16.custom.min.js b/SharpMap.Server/js/jquery-ui-1.8.16.custom.min.js similarity index 100% rename from SharpMapServer/js/jquery-ui-1.8.16.custom.min.js rename to SharpMap.Server/js/jquery-ui-1.8.16.custom.min.js diff --git a/SharpMapServer/packages.config b/SharpMap.Server/packages.config similarity index 100% rename from SharpMapServer/packages.config rename to SharpMap.Server/packages.config diff --git a/SharpMapServer/states.dbf b/SharpMap.Server/states.dbf similarity index 100% rename from SharpMapServer/states.dbf rename to SharpMap.Server/states.dbf diff --git a/SharpMapServer/states.prj b/SharpMap.Server/states.prj similarity index 100% rename from SharpMapServer/states.prj rename to SharpMap.Server/states.prj diff --git a/SharpMapServer/states.shp b/SharpMap.Server/states.shp similarity index 100% rename from SharpMapServer/states.shp rename to SharpMap.Server/states.shp diff --git a/SharpMapServer/states.shx b/SharpMap.Server/states.shx similarity index 100% rename from SharpMapServer/states.shx rename to SharpMap.Server/states.shx From a1f9164341510b07dd88dc3715a4d5c04538bdb1 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 20 Dec 2019 11:10:43 +0100 Subject: [PATCH 03/51] Fix handling of spatial tree in web context --- SharpMap/Data/Providers/ShapeFile.cs | 54 +++++++++++++++++++--------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/SharpMap/Data/Providers/ShapeFile.cs b/SharpMap/Data/Providers/ShapeFile.cs index 9b6e0356..1176fd9e 100644 --- a/SharpMap/Data/Providers/ShapeFile.cs +++ b/SharpMap/Data/Providers/ShapeFile.cs @@ -20,6 +20,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using GeoAPI; using GeoAPI.Geometries; @@ -379,7 +380,7 @@ private void Dispose(bool disposing) if (Web.HttpCacheUtility.IsWebContext) { if (Web.HttpCacheUtility.TryGetValue(_filename, out ISpatialIndex tree)) - disposeTree = tree == _tree; + disposeTree = !ReferenceEquals(tree, _tree); } if (disposeTree && _tree is IDisposable disposableTree) @@ -1029,18 +1030,18 @@ internal static int SwapByteOrder(int i) /// /// /// A spatial index - private ISpatialIndex CreateSpatialIndexFromFile(string filename) + private ISpatialIndex CreateSpatialIndexFromFile(string filename) { var tree = SpatialIndexFactory.Load(filename); if (tree == null) { - tree = SpatialIndexFactory.Create(GetExtents(), GetFeatureCount(), GetAllFeatureBoundingBoxes()); - tree.SaveIndex(Filename); - } + tree = SpatialIndexFactory.Create(GetExtents(), GetFeatureCount(), GetAllFeatureBoundingBoxes()); + tree.SaveIndex(filename); + } return tree; } - + //private void LoadSpatialIndex() //{ // LoadSpatialIndex(false, false); @@ -1096,7 +1097,20 @@ public static ISpatialIndexFactory SpatialIndexFactory [Obsolete("Use SpatialIndexFactory")] public static SpatialIndexCreation SpatialIndexCreationOption { get; set; } - private void LoadSpatialIndex(bool forceRebuild) + private static readonly Dictionary _lockWebCache = new Dictionary(); + + [MethodImpl(MethodImplOptions.Synchronized)] + private static object GetOrCreateLock(string filename) + { + if (!_lockWebCache.TryGetValue(filename, out object lockValue)) + { + lockValue = new object(); + _lockWebCache.Add(filename, lockValue); + } + return lockValue; + } + + private void LoadSpatialIndex(bool forceRebuild) { // if we want a new tree, force its creation if (_tree != null && forceRebuild) @@ -1107,10 +1121,13 @@ private void LoadSpatialIndex(bool forceRebuild) if (forceRebuild) { - _tree = SpatialIndexFactory.Create(GetExtents(), GetFeatureCount(), GetAllFeatureBoundingBoxes()); - _tree.SaveIndex(_filename); - if (Web.HttpCacheUtility.IsWebContext) - Web.HttpCacheUtility.TryAddValue(_filename, _tree, TimeSpan.FromDays(1)); + lock (GetOrCreateLock(_filename)) + { + _tree = SpatialIndexFactory.Create(GetExtents(), GetFeatureCount(), GetAllFeatureBoundingBoxes()); + _tree.SaveIndex(_filename); + if (Web.HttpCacheUtility.IsWebContext) + Web.HttpCacheUtility.TryAddValue(_filename, _tree, TimeSpan.FromDays(1)); + } return; } @@ -1118,12 +1135,15 @@ private void LoadSpatialIndex(bool forceRebuild) // need to rebuild it for each request if (Web.HttpCacheUtility.IsWebContext) { - if (!Web.HttpCacheUtility.TryGetValue(_filename, out _tree)) - { - _tree = CreateSpatialIndexFromFile(_filename); - Web.HttpCacheUtility.TryAddValue(_filename, _tree, TimeSpan.FromDays(1)); - } - } + lock (GetOrCreateLock(_filename)) + { + if (!Web.HttpCacheUtility.TryGetValue(_filename, out _tree)) + { + _tree = CreateSpatialIndexFromFile(_filename); + Web.HttpCacheUtility.TryAddValue(_filename, _tree, TimeSpan.FromDays(1)); + } + } + } else { _tree = CreateSpatialIndexFromFile(_filename); From 52755adf1b4a09d057e812b1cdc015c73043b9e6 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 20 Dec 2019 11:12:25 +0100 Subject: [PATCH 04/51] Improve Wms Server example --- .../Controllers/BuildingsController.cs | 4 ++-- Examples/SharpMap.Demo.Wms/Global.asax.cs | 8 ++++++- .../Handlers/AbstractStdMapHandler.cs | 7 ++----- .../Helpers/ShapefileHelper.cs | 21 +++++++++++++------ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Examples/SharpMap.Demo.Wms/Controllers/BuildingsController.cs b/Examples/SharpMap.Demo.Wms/Controllers/BuildingsController.cs index 946f11fd..0934fbc6 100644 --- a/Examples/SharpMap.Demo.Wms/Controllers/BuildingsController.cs +++ b/Examples/SharpMap.Demo.Wms/Controllers/BuildingsController.cs @@ -23,7 +23,7 @@ public class BuildingsController : Controller public BuildingsController() { - GeometryServiceProvider.Instance = new NtsGeometryServices(); + GeometryServiceProvider.SetInstanceIfNotAlreadySetDirectly(new NtsGeometryServices()); } private Point GeoToPixel(double lat, double lon, int zoom) @@ -90,4 +90,4 @@ public JsonResult GetData(float w, float n, float e, float s, int z) return this.Json(new { meta, data }, JsonRequestBehavior.AllowGet); } } -} \ No newline at end of file +} diff --git a/Examples/SharpMap.Demo.Wms/Global.asax.cs b/Examples/SharpMap.Demo.Wms/Global.asax.cs index 0ff97729..c88bfce2 100644 --- a/Examples/SharpMap.Demo.Wms/Global.asax.cs +++ b/Examples/SharpMap.Demo.Wms/Global.asax.cs @@ -15,8 +15,14 @@ public static void RegisterRoutes(RouteCollection routes) protected void Application_Start() { + SharpMap.Session.Instance + .SetGeometryServices(NetTopologySuite.NtsGeometryServices.Instance) + .SetCoordinateSystemServices(CoordinateSystems.CoordinateSystemServices + .FromSpatialRefSys(new ProjNet.CoordinateSystems.CoordinateSystemFactory(), + new ProjNet.CoordinateSystems.Transformations.CoordinateTransformationFactory())); + AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); } } -} \ No newline at end of file +} diff --git a/Examples/SharpMap.Demo.Wms/Handlers/AbstractStdMapHandler.cs b/Examples/SharpMap.Demo.Wms/Handlers/AbstractStdMapHandler.cs index 4b157f00..db07b875 100644 --- a/Examples/SharpMap.Demo.Wms/Handlers/AbstractStdMapHandler.cs +++ b/Examples/SharpMap.Demo.Wms/Handlers/AbstractStdMapHandler.cs @@ -15,10 +15,7 @@ public abstract class AbstractStdMapHandler : IHttpHandler static AbstractStdMapHandler() { - lock (SyncLock) - { - GeometryServiceProvider.Instance = new NtsGeometryServices(); - } + GeometryServiceProvider.SetInstanceIfNotAlreadySetDirectly(NtsGeometryServices.Instance); } public abstract void ProcessRequest(HttpContext context); @@ -63,4 +60,4 @@ public bool IsReusable get { return false; } } } -} \ No newline at end of file +} diff --git a/Examples/SharpMap.Demo.Wms/Helpers/ShapefileHelper.cs b/Examples/SharpMap.Demo.Wms/Helpers/ShapefileHelper.cs index 7b937721..47cd69d9 100644 --- a/Examples/SharpMap.Demo.Wms/Helpers/ShapefileHelper.cs +++ b/Examples/SharpMap.Demo.Wms/Helpers/ShapefileHelper.cs @@ -1,3 +1,5 @@ +using ProjNet.CoordinateSystems; + namespace SharpMap.Demo.Wms.Helpers { using System; @@ -48,6 +50,7 @@ static ShapefileHelper() PointSize = 10 } }; + Nyc = new Dictionary { { "nyc/poly_landmarks.shp", landmarks }, @@ -58,11 +61,15 @@ static ShapefileHelper() public static Map Spherical() { - ICoordinateTransformation transformation = ProjHelper.LatLonToGoogle(); + //ICoordinateTransformation transformation = ProjHelper.LatLonToGoogle(); HttpContext context = HttpContext.Current; Map map = new Map(new Size(1, 1)); IDictionary dict = Nyc; + var ctFac = new ProjNet.CoordinateSystems.Transformations.CoordinateTransformationFactory(); + var pos = ctFac.CreateFromCoordinateSystems(GeographicCoordinateSystem.WGS84, ProjectedCoordinateSystem.WebMercator); + var neg = ctFac.CreateFromCoordinateSystems(ProjectedCoordinateSystem.WebMercator, GeographicCoordinateSystem.WGS84); + foreach (string layer in dict.Keys) { string format = String.Format("~/App_Data/{0}", layer); @@ -75,11 +82,13 @@ public static Map Spherical() ShapeFile source = new ShapeFile(path, true); VectorLayer item = new VectorLayer(name, source) { - SRID = 4326, - TargetSRID = 900913, - CoordinateTransformation = transformation, + //SRID = 4326, + //TargetSRID = 900913, + CoordinateTransformation = pos, + ReverseCoordinateTransformation = neg, Style = (VectorStyle)data.Style, - SmoothingMode = SmoothingMode.AntiAlias + SmoothingMode = SmoothingMode.AntiAlias, + IsQueryEnabled = true }; map.Layers.Add(item); } @@ -113,4 +122,4 @@ public static Map Default() return map; } } -} \ No newline at end of file +} From fcd92d28d8d27cbf150ca25c91f157e16ad622b9 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 20 Dec 2019 11:14:10 +0100 Subject: [PATCH 05/51] Improve Web related functionality --- SharpMap.Web/SharpMap.Web.csproj | 4 ++++ SharpMap.Web/Web/Wms/WmsServer.cs | 25 ++++++++++++++++++++----- SharpMap/Web/WebUtilities.cs | 3 +++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/SharpMap.Web/SharpMap.Web.csproj b/SharpMap.Web/SharpMap.Web.csproj index 540ea1a9..f1e4a0e5 100644 --- a/SharpMap.Web/SharpMap.Web.csproj +++ b/SharpMap.Web/SharpMap.Web.csproj @@ -12,6 +12,10 @@ This package contains UI Compontents for Ajax Web. + + + + diff --git a/SharpMap.Web/Web/Wms/WmsServer.cs b/SharpMap.Web/Web/Wms/WmsServer.cs index 735916ec..d1c2af15 100644 --- a/SharpMap.Web/Web/Wms/WmsServer.cs +++ b/SharpMap.Web/Web/Wms/WmsServer.cs @@ -115,7 +115,7 @@ public static Encoding FeatureInfoResponseEncoding /// Map to serve on WMS /// Description of map service /// - ///Delegate for Getfeatureinfo intersecting, when null, the WMS will default to ICanQueryLayer implementation + ///Delegate for GetFeatureInfo intersecting, when null, the WMS will default to implementation public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription description, int pixelSensitivity, InterSectDelegate intersectDelegate) { _intersectDelegate = intersectDelegate; @@ -175,7 +175,7 @@ public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription /// Map to serve on WMS /// Description of map service /// - /// Delegate for Getfeatureinfo intersecting, when null, the WMS will default to ICanQueryLayer implementation + ///Delegate for GetFeatureInfo intersecting, when null, the WMS will default to implementation /// The context the is running in. public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription description, int pixelSensitivity, InterSectDelegate intersectDelegate, HttpContext context) { @@ -556,7 +556,7 @@ public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription WmsException.ThrowWmsException("Required parameter CRS not specified", context); return; } - if (context.Request.Params["CRS"] != "EPSG:" + map.Layers[0].TargetSRID) + if (!ConsideredEqual(context.Request.Params["CRS"], $"EPSG:{map.Layers[0].TargetSRID}")) { WmsException.ThrowWmsException(WmsException.WmsExceptionCode.InvalidCRS, "CRS not supported", context); @@ -749,7 +749,7 @@ public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription //Set layers on/off var layersString = context.Request.Params["LAYERS"]; - if (!String.IsNullOrEmpty(layersString)) + if (!string.IsNullOrEmpty(layersString)) //If LAYERS is empty, use default layer on/off settings { var layers = layersString.Split(new[] {','}); @@ -786,10 +786,11 @@ public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription lay.Enabled = true; } } + //Render map var img = map.GetMap(); - //Png can't stream directy. Going through a memorystream instead + //Png can't stream directly. Going through a MemoryStream instead byte[] buffer; using (var ms = new MemoryStream()) { @@ -812,6 +813,20 @@ public static void ParseQueryString(Map map, Capabilities.WmsServiceDescription } } + private static bool ConsideredEqual(string requestedCrs, string mapCrs) + { + if (string.Equals(requestedCrs, mapCrs, StringComparison.InvariantCultureIgnoreCase)) + return true; + + if (requestedCrs == "EPSG:900913" && mapCrs == "EPSG:3857") + return true; + + if (requestedCrs == "EPSG:3857" && mapCrs == "EPSG:900913") + return true; + + return false; + } + private static void PrepareDataSourceForCql(IBaseProvider provider, string cqlFilterString) { //for layers with a filterprovider diff --git a/SharpMap/Web/WebUtilities.cs b/SharpMap/Web/WebUtilities.cs index a735d236..7ce3ec3d 100644 --- a/SharpMap/Web/WebUtilities.cs +++ b/SharpMap/Web/WebUtilities.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Reflection; +using System.Runtime.CompilerServices; using System.Xml; namespace SharpMap.Web @@ -111,6 +112,7 @@ private static object GetCurrentCache() return GetHttpContextCache(/*httpContext*/); } + [MethodImpl(MethodImplOptions.Synchronized)] internal static bool TryGetValue(string key, out T instance) where T: class { @@ -143,6 +145,7 @@ internal static bool TryAddValue(string key, T instance) return TryAddValue(key, instance, TimeSpan.FromDays(1)); } + [MethodImpl(MethodImplOptions.Synchronized)] internal static bool TryAddValue(string key, T instance, TimeSpan timeSpan) where T : class { From 5d17a90bffddd583b30575ebac36a8764de24330 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 20 Dec 2019 11:16:49 +0100 Subject: [PATCH 06/51] Fix disposal of QuadTree --- SharpMap/Utilities/Indexing/SpatialIndexing.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SharpMap/Utilities/Indexing/SpatialIndexing.cs b/SharpMap/Utilities/Indexing/SpatialIndexing.cs index 25c287fa..9b1674b6 100644 --- a/SharpMap/Utilities/Indexing/SpatialIndexing.cs +++ b/SharpMap/Utilities/Indexing/SpatialIndexing.cs @@ -610,7 +610,9 @@ public uint Depth public void Dispose() { //this._box = null; + _child0?.Dispose(); _child0 = null; + _child1?.Dispose(); _child1 = null; _objList = null; } From 4c57eea7627a4f5e52e88ddd6fe7bc8ddd525ce2 Mon Sep 17 00:00:00 2001 From: Tim C Date: Sun, 22 Dec 2019 21:09:08 +1300 Subject: [PATCH 07/51] Kml Style and StyleMap updates incl load styles from external resources --- SharpMap.Data.Providers.Kml/KmlProvider.cs | 1722 +++++++++++--------- 1 file changed, 955 insertions(+), 767 deletions(-) diff --git a/SharpMap.Data.Providers.Kml/KmlProvider.cs b/SharpMap.Data.Providers.Kml/KmlProvider.cs index f56d7d33..7b55b288 100644 --- a/SharpMap.Data.Providers.Kml/KmlProvider.cs +++ b/SharpMap.Data.Providers.Kml/KmlProvider.cs @@ -1,767 +1,955 @@ -// Copyright 2014 - Robert Smart (www.cnl-software.com) -// -// This file is part of SharpMap.Data.Providers.Kml. -// SharpMap.Data.Providers.Kml is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap.Data.Providers.Kml is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using GeoAPI.Geometries; -using SharpKml.Dom; -using SharpKml.Engine; -using SharpMap.Rendering.Thematics; -using SharpMap.Styles; -using LinearRing = SharpKml.Dom.LinearRing; -using LineString = SharpKml.Dom.LineString; -using Point = SharpKml.Dom.Point; -using Polygon = SharpKml.Dom.Polygon; -using Style = SharpKml.Dom.Style; - -namespace SharpMap.Data.Providers -{ - /// - /// Kml/Kmz provider - /// - public class KmlProvider : IProvider - { - #region Static factory methods - - /// - /// Creates a KmlProvider from a file - /// - /// The path to the file - /// A Kml provider - /// - /// - public static KmlProvider FromKml(string filename) - { - if (string.IsNullOrEmpty(filename)) - throw new ArgumentNullException("filename"); - if (!File.Exists(filename)) - throw new FileNotFoundException("File not found", "filename"); - - using (var s = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - return FromKml(s); - } - } - - /// - /// Creates a KmlProvider from a Kml stream - /// - /// The stream to read from - /// - public static KmlProvider FromKml(Stream stream) - { - if (stream == null) - throw new ArgumentNullException("stream"); - - return new KmlProvider(KmlFile.Load(stream)); - } - - /// - /// Creates a KmlProvider from a file - /// - /// The path to the file - /// The internal file to read - /// A Kml provider - /// - /// - public static KmlProvider FromKmz(string filename, string internalFile = null) - { - if (string.IsNullOrEmpty(filename)) - throw new ArgumentNullException("filename"); - if (!File.Exists(filename)) - throw new FileNotFoundException("File not found", "filename"); - - using (var s = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - return FromKmz(s, internalFile); - } - } - - /// - /// Creates a KmlProvider from a Kmz stream - /// - /// The stream to read from - /// The internal file to read - /// - public static KmlProvider FromKmz(Stream stream, string internalFile = null) - { - var kmz = KmzFile.Open(stream); - if (string.IsNullOrEmpty(internalFile)) - return new KmlProvider(kmz.GetDefaultKmlFile()); - - //NOTE:DON'T KNOW IF THIS IS CORRECT! - using (var ms = new MemoryStream(kmz.ReadFile(internalFile))) - { - return new KmlProvider(KmlFile.Load(ms)); - } - } - #endregion - - #region static constructor and fields - - private static readonly FeatureDataTable _schemaTable; - - static KmlProvider() - { - _schemaTable = new FeatureDataTable(); - AddColumnsToFeatureDataTable(_schemaTable); - } - - #endregion - - #region private fields and constants - private IGeometryFactory _geometryFactory; - private Dictionary> _geometrys; - private Dictionary _kmlStyles; - private List _styleMaps; - - private const string DefaultStyleId = "{6787C5B3-6482-4B96-9C2D-2C6236D2AC50}"; - private const string DefaultPointStyleId = "{E2892545-7CF4-48A1-B8F0-5A0BF06EF0E1}"; - - #endregion - - /// - /// Method to create a theme for the Layer - /// - /// A theme - /// - /// - /// - /// - public ITheme GetKmlTheme() - { - //todo layer will need to do this - //Layer.Theme = assetTheme; - return new CustomTheme(GetKmlStyle); - } - - /// - /// Gets or sets a value indicating that are to be treated as polygons - /// - public bool RingsArePolygons { get; set; } - - /// - /// Creates an instance of this class using the provided KmlFile - /// - /// The KmlFile - public KmlProvider(KmlFile kmlFile) - { - ParseKml(kmlFile); - } - - /// - /// Method to parse the KmlFile - /// - /// The file to parse - private void ParseKml(KmlFile kmlFile) - { - var kml = kmlFile.Root as Kml; - if (kml == null) - { - throw new Exception("Kml file is null! Please check that the file conforms to http://www.opengis.net/kml/2.2 standards"); - } - - var doc = kml.Feature as Document; - if (doc == null) - { - throw new Exception("Kml file does not have a document node! please check that the file conforms to http://www.opengis.net/kml/2.2 standards"); - } - - _geometryFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(4326); - ConnectionID = doc.Name; - if (doc.Description != null && !string.IsNullOrEmpty(doc.Description.Text)) - ConnectionID += " (" + doc.Description.Text + ")"; - - ExtractStyles(kml); - ExtractStyleMaps(kml); - ExtractGeometries(kml); - - } - - private void ExtractStyleMaps(Element kml) - { - _styleMaps = new List(); - foreach (var style in kml.Flatten().OfType()) - { - var styleMap = new StyleMap { Id = style.Id }; - _styleMaps.Add(styleMap); - style.ToList().ForEach(x => - { - if (x.State != null) - - switch (x.State.Value) - { - case StyleState.Normal: - styleMap.NormalStyleUrl = x.StyleUrl.ToString().Replace("#", ""); - break; - case StyleState.Highlight: - styleMap.HighlightStyleUrl = x.StyleUrl.ToString().Replace("#", ""); - break; - } - - }); - } - } - - /// - /// Style map class - /// - private class StyleMap - { - public string Id { get; set; } - public string NormalStyleUrl { get; set; } - public string HighlightStyleUrl { get; set; } - } - - //todo needs buffing up - private void ExtractStyles(Element kml) - { - _kmlStyles = new Dictionary(); - - _kmlStyles.Add(DefaultStyleId, DefaultVectorStyle()); - _kmlStyles.Add(DefaultPointStyleId, DefaultPointStyle()); - - var symbolDict = new Dictionary(); - - foreach (var style in kml.Flatten().OfType + + 1 + + 14.76679054044666,8.647733316197623,0 14.99402575335166,8.762810855977225,0 15.13946004407134,8.539054532876513,0 + + + + + Polygon 1 with StyleUrl + KML_styles.kml#polygonsymbology + + 1 + + + + 15.45930089777413,8.649774762626734,0 15.78812145713398,8.826902630484089,0 15.56553132250632,9.117659207419846,0 15.28843281990799,8.916977130845003,0 15.45930089777413,8.649774762626734,0 + + + + + + + Polygon 2 with Style overrides + KML_styles.kml#polygonsymbology + + + 1 + + + + 15.99240773337751,8.791408965716004,0 16.2407273758592,8.921982810633205,0 15.97838555087566,9.197063754094813,0 15.75799832823521,9.060468835708274,0 15.99240773337751,8.791408965716004,0 + + + + + + + Polygon 3 inline style fill only + + + 1 + + + + 16.45141663527584,8.99085343604397,0 16.62822777773094,9.105369181051785,0 16.3836231863226,9.367059726191357,0 16.22294327719263,9.238531882204896,0 16.45141663527584,8.99085343604397,0 + + + + + + + Polygon 4 inline style outline only + + + 1 + + + + 16.75311822571322,9.148653889350184,0 16.91888588059678,9.219599002061271,0 16.70384488661618,9.49955818101844,0 16.55253261598985,9.408508348830811,0 16.75311822571322,9.148653889350184,0 + + + + + + + Polygon 5 with island + KML_styles.kml#polygonsymbology + + 1 + + + + 17.156109304262,9.251432883064757,0 17.32967734249761,9.340659686674064,0 17.15439083960273,9.617539674318666,0 16.97897194415396,9.494094316822185,0 17.156109304262,9.251432883064757,0 + + + + + + + 17.18499558552173,9.330908287399483,0 17.24428094114781,9.374604608036631,0 17.17390026104265,9.5020304960033,0 17.10654405342015,9.460253752017769,0 17.18499558552173,9.330908287399483,0 + + + + + + + diff --git a/UnitTests/TestData/KML_geom_external_styleurl_http.kml b/UnitTests/TestData/KML_geom_external_styleurl_http.kml new file mode 100644 index 00000000..b0e19adf --- /dev/null +++ b/UnitTests/TestData/KML_geom_external_styleurl_http.kml @@ -0,0 +1,20 @@ + + + + Style Map Tests + + Africa 1 + https://developers.google.com/kml/documentation/KML_Samples.kml#globeIcon + + 14.56324920473027,8.823766947866202,0 + + + + Africa 2 + https://developers.google.com/kml/documentation/KML_Samples.kml#downArrowIcon + + 17.40955322880677,9.619815130815919,0 + + + + diff --git a/UnitTests/TestData/KML_geom_external_styleurl_invalid.kml b/UnitTests/TestData/KML_geom_external_styleurl_invalid.kml new file mode 100644 index 00000000..3ace5b06 --- /dev/null +++ b/UnitTests/TestData/KML_geom_external_styleurl_invalid.kml @@ -0,0 +1,146 @@ + + + + Style Map Tests + Placemarks using invalid styles. Placemarks will be rendered using default symbology. + + Africa 1 + bollocks#sm_square + + 1 + 14.56324920473027,8.823766947866202,0 + + + + Africa 2 + bollocks#sm_circle + + 1 + 17.40955322880677,9.619815130815919,0 + + + + Line 1 with StyleUrl + bollocks#linesymbology + + + 14.7322444643474,8.741122422265486,0 14.98189150254889,8.876022509822219,0 15.1636208882102,8.614709363012477,0 + + + + + Line 2 with StyleUrl and overrides + bollocks#linesymbology + + + 1 + + 14.76679054044666,8.647733316197623,0 14.99402575335166,8.762810855977225,0 15.13946004407134,8.539054532876513,0 + + + + + Polygon 1 with StyleUrl + bollocks#polygonsymbology + + 1 + + + + 15.45930089777413,8.649774762626734,0 15.78812145713398,8.826902630484089,0 15.56553132250632,9.117659207419846,0 15.28843281990799,8.916977130845003,0 15.45930089777413,8.649774762626734,0 + + + + + + + Polygon 2 with Style overrides + bollocks#polygonsymbology + + + 1 + + + + 15.99240773337751,8.791408965716004,0 16.2407273758592,8.921982810633205,0 15.97838555087566,9.197063754094813,0 15.75799832823521,9.060468835708274,0 15.99240773337751,8.791408965716004,0 + + + + + + + Polygon 3 inline style fill only + + + 1 + + + + 16.45141663527584,8.99085343604397,0 16.62822777773094,9.105369181051785,0 16.3836231863226,9.367059726191357,0 16.22294327719263,9.238531882204896,0 16.45141663527584,8.99085343604397,0 + + + + + + + Polygon 4 inline style outline only + + + 1 + + + + 16.75311822571322,9.148653889350184,0 16.91888588059678,9.219599002061271,0 16.70384488661618,9.49955818101844,0 16.55253261598985,9.408508348830811,0 16.75311822571322,9.148653889350184,0 + + + + + + + Polygon 5 with island + bollocks#polygonsymbology + + 1 + + + + 17.156109304262,9.251432883064757,0 17.32967734249761,9.340659686674064,0 17.15439083960273,9.617539674318666,0 16.97897194415396,9.494094316822185,0 17.156109304262,9.251432883064757,0 + + + + + + + 17.18499558552173,9.330908287399483,0 17.24428094114781,9.374604608036631,0 17.17390026104265,9.5020304960033,0 17.10654405342015,9.460253752017769,0 17.18499558552173,9.330908287399483,0 + + + + + + + diff --git a/UnitTests/TestData/KML_geom_internal_styleurl.kml b/UnitTests/TestData/KML_geom_internal_styleurl.kml new file mode 100644 index 00000000..ffb596dc --- /dev/null +++ b/UnitTests/TestData/KML_geom_internal_styleurl.kml @@ -0,0 +1,213 @@ + + + + Style Map Tests + Placemarks using Styles defined in the same document + + + + + + + + + normal + #sym_circle + + + highlight + #sym_circle_highlight + + + + + normal + #sym_square + + + highlight + #sym_square_highlight + + + + Africa 1 + #sm_square + + 1 + 14.56324920473027,8.823766947866202,0 + + + + Africa 2 + #sm_circle + + 1 + 17.40955322880677,9.619815130815919,0 + + + + Line 1 with StyleUrl + #linesymbology + + + 14.7322444643474,8.741122422265486,0 14.98189150254889,8.876022509822219,0 15.1636208882102,8.614709363012477,0 + + + + + Line 2 with StyleUrl and overrides + #linesymbology + + + 1 + + 14.76679054044666,8.647733316197623,0 14.99402575335166,8.762810855977225,0 15.13946004407134,8.539054532876513,0 + + + + + Polygon 1 with StyleUrl + #polygonsymbology + + 1 + + + + 15.45930089777413,8.649774762626734,0 15.78812145713398,8.826902630484089,0 15.56553132250632,9.117659207419846,0 15.28843281990799,8.916977130845003,0 15.45930089777413,8.649774762626734,0 + + + + + + + Polygon 2 with Style overrides + #polygonsymbology + + + 1 + + + + 15.99240773337751,8.791408965716004,0 16.2407273758592,8.921982810633205,0 15.97838555087566,9.197063754094813,0 15.75799832823521,9.060468835708274,0 15.99240773337751,8.791408965716004,0 + + + + + + + Polygon 3 inline style fill only + + + 1 + + + + 16.45141663527584,8.99085343604397,0 16.62822777773094,9.105369181051785,0 16.3836231863226,9.367059726191357,0 16.22294327719263,9.238531882204896,0 16.45141663527584,8.99085343604397,0 + + + + + + + Polygon 4 inline style outline only + + + 1 + + + + 16.75311822571322,9.148653889350184,0 16.91888588059678,9.219599002061271,0 16.70384488661618,9.49955818101844,0 16.55253261598985,9.408508348830811,0 16.75311822571322,9.148653889350184,0 + + + + + + + Polygon 5 with island + #polygonsymbology + + 1 + + + + 17.156109304262,9.251432883064757,0 17.32967734249761,9.340659686674064,0 17.15439083960273,9.617539674318666,0 16.97897194415396,9.494094316822185,0 17.156109304262,9.251432883064757,0 + + + + + + + 17.18499558552173,9.330908287399483,0 17.24428094114781,9.374604608036631,0 17.17390026104265,9.5020304960033,0 17.10654405342015,9.460253752017769,0 17.18499558552173,9.330908287399483,0 + + + + + + + diff --git a/UnitTests/TestData/KML_styles.kml b/UnitTests/TestData/KML_styles.kml new file mode 100644 index 00000000..09bbbfce --- /dev/null +++ b/UnitTests/TestData/KML_styles.kml @@ -0,0 +1,74 @@ + + + + Style Map Tests + Styles and Style Maps referenced by other KML files + + + + + + + + + normal + #sym_circle + + + highlight + #sym_circle_highlight + + + + + normal + #sym_square + + + highlight + #sym_square_highlight + + + + diff --git a/UnitTests/TestData/KMZ_geom_external_styleurl_file.kmz b/UnitTests/TestData/KMZ_geom_external_styleurl_file.kmz new file mode 100644 index 0000000000000000000000000000000000000000..75a231a5e5d37cb10c7a5291d7eab1c4252a139c GIT binary patch literal 1491 zcmV;^1uXhdO9KQH000080Gnc#PeE1C*<%F&0HqQD00#g70Az1tE^BRU%~(xu+eQ#Q zD?t7OpFun&i?;z)F-tgd$y%j_m&WzFpF`lsK{67)fNnT9UKF zoq2EG&MJR$zO9oFW!G12^LQ>&nkQwmY*$sYc|3pf`nR9m{K?|sgZw?rVA}ML=kJE$ z^ih`WcDuAaEt^f%r%gFzu*hU8=cC!9&G~`lrfoNMnJ(LHG%U}DePi4@)^9Pi+XLGj zr){^%B$3XZ;^90j_t$oLwk?|h<5}Jm+j8-0xTwqIMRA(EF8iU+vv@Z=UX}f_t4;@G zVDYjpmgTnS-uKB_j~pa%TA!@Sb=8!sq-v6`tc#)gP$th_JWtkDUFO-ohgooWjU(WmdX|Nhi(-}e8U6&-wEnZyY%+iDC7(MEb zhbc>nW|g#1pRPg)eRTq(+c-?{neusgYnJb-)$lG*XdGPGWrv1&wm1F+6QrOoYbZb& zJndb52{L1>C#?vu850tX>@u?eF|Cn2VdfR^vYJ_oXoW{kkki^*3+yJ$*egTuL{@OX zUvqE`z5`-#{C+`u+13}Ew)sC)KOLuj7OctjacOPOhO+ymJ!@7)ck%mWY9l&D_vaNl zHv#|%bgHyxB8N<(UN}O4u|kURXOYO5T3Moy*qty}Xx{T}-KrbQP&p@8tl&YX^ zSJ}P`?)&w6w?e%Q{`;xbcrPq=f9?`5>=tW$io&i#-oo{@i$nW^a{soZ^j@NjgA!P) zMW}c$0?iBv7-~FF#H7JSTDIDOV}VD|Zx@uoFiVomayeWY)%5im`Et4hnzvbJme5o5_H2Pya3wh&1iM|s=g<2)FE*u zbZ_I%$I5iAF6%YXA$LZxQeUN7H*qp>JP)&&j;ufY(KWQ4S0w6erbp=&h;?NP8&9__pfq=l!+mDUm+k3l={Ja`Nv z@QfT|Ug+{KUERK{@5x6!Zmv~dqYb`~J@s3HZd0L+z-fULT9p7MY9Bib@{jIzlntA* z1k`9_aO$)sY&fh4t&#z{79%xU2@Q1sdLCyK%kjVl=rB_SSu&HjV2ksRAEiWi7Ksry zH+0c}Sd40R#H#KA@mXFq%~vMRjkE@lMGToC(b$8XWt;;XkHO%eCmmuGIGDMZKC8lx zO^z5cE2fY!2StRq5p*a9ocjnFKst#hPv{mj&J56ENg@^ua;?!H;L=*-C#&06b;tDG zl&H@Z%;;QK#NO928D02BZK00;n^VwO)qRnXaE1pol05&!@P00000 t0000103ZMW000000Az1tE^BRUP)h{{000000RRC2H2?qrhy?%u0083dwblRt literal 0 HcmV?d00001 diff --git a/UnitTests/TestData/KMZ_geom_internal_styleurl.kmz b/UnitTests/TestData/KMZ_geom_internal_styleurl.kmz new file mode 100644 index 0000000000000000000000000000000000000000..3124d220d45896478ead0a9a0cf962a3eb8eb79f GIT binary patch literal 1733 zcmV;$20HmrO9KQH0000809>q;PgjUSO;rW}07V%900#g70Az1tE^BRU%~{)S+eQ$5 zR)G8ip}xB0-Y)^IAVpCiK#Lk}@*J=dB@v-Wm89$V?|XKal1N3i<=BYBAoh|wJ3E)N zXJ(W1?Uzj%{g*dwQCF8!8k1?1SMz#NRIAJB#}B{#Y^HB#uV1B~AqLT^y_~MQZu=%l zZf!3N&0?XSAN>RPSSRsVN7F|CSe#pJFzgFr0XVM&U%kG*|r@96Yt9c7Wy!?>uihR zcIQODbG)rqY2vj6<;h-q81C&!lf&?o51-b>YF*-Q_j2AGY8=lXv zm-FVSK9o3a@Z+DltXH?a8^7b*(2Y4Jo7r+nNR~;NCgDBI-4u&%JrijX24UZkg46qM z>+*J0SLbf6m&@hjx1OX|*ygmp49`$snv7(3R!{b3J4*9eKT7&lR_rYLoZrr>y4hr9 znz(U~0=N2OQ_g-m00w~AeeE~jX*t%nN9#i>KMf@BP~JNQPY&4_xHaO>N4gc(KC4^D zG<2?8``Qx3eEoU1Y>Ign(L*^J?RJ;_S9K9;8^)_IZx&5<^JlZjn;8_1-goG#8&veH z%i9?hu~eK1%ZSjNFnwWS!?;$;3T>2Pgk2B^z{8Rq4EsGX9C~t}U+hl5PQ&dfe(6{c zD|@fq8Cc97UONKsZTq8g26Q=nUVY{;ViNq0OL4W1D>M@LW3 z#`M6(quJ!t+CgPFkt#%{&C3!Ia#{y(_hnKkZHW{F*;LBKA3^7wQy^3;#`{!Sfi!B# zxujkSQ-%^FrPUfK?NiCEPz0KYrrg0oa;F-=N9Fq#lOadYWh|uSgc!sQ2Z#HhV=NF<{Lu)V52rSUGAbm3 z$*u8Zs4QVP8Nvvx4pLHqStxhs3giiDp%lReheEYALIo)>k3brzwM1KY7A;lS(h-o5 zgzzABhFv?)zDnUPiNgq_$*E97I2VKv6Eq%YiWQ`V2ZYyO}SLN*<#rw>DJB+jk(rFlZL3reBm=W)sAO6r_Q(z`4P$ z5Sfu&a6o`t9k7O)#F%yu2W}jUh~&yrpiNS3WRT(p4F{(NY{?wfxY7h=jG^FBK&gi< zV-|!7hO{7DNHG8ui?9LM*Qy}(uvIS!H8F--pXtY-?OrB4ke0jW-h;NUZk?mqCsO8v zSa&DP0p0)p!BLI|i&8+$m3-!FIhtw7(LHlXsOc#cfHp?rDj}`X#$l6M3FHw*6lIzQ zDNVQ$XuOp|E9azy#T~@B!vIaaDk&MdGle^du{a+P0)C0%Sm&-LJ5>*x^^!Q3p)Dxd z5}o`vbOYZx%{r!1QCx9|KwIU~2Npq9L&VX02LjSDXGktuE?kGTa>b4dg0@Z}pBf_( zq^JbXAYT?2E6x4A63N0!;RsIkG==hNL1aQuH|L??3M+biB$WnUJj1GI$nhjCs_Kcs zGXcneF7+;RkUi?Z(HyrF!-F%poiYO-DQ=U-hffRFEf7OE2%GjUV-1cC{D#vG{q;PgjUSO;rW}07V%900#g7000000096X000000001FZ(}ZN bZER3W1qJ{B000310RS}s004Rh00000B6cFH literal 0 HcmV?d00001 diff --git a/UnitTests/TestData/KMZ_styles.kmz b/UnitTests/TestData/KMZ_styles.kmz new file mode 100644 index 0000000000000000000000000000000000000000..c698fa9a0b55efbdeefc7d7cc2950bd0908850f9 GIT binary patch literal 607 zcmWIWW@Zs#U|`^2xD+?dztC$>;Cn^}hFW$81|VWc$xqhH&doU*dNJ#^fk^GQg#QYA zo=4_w+Yl%+{g6b@)_{oLG9i)ETvJX?zGC_BcWiWDa(mBG69MNBSNFOK$M9?K`$3bJme7G2AbO z&f2oE#Hn;!99Va>tWD$?$5G+5RdOe`*KxI7S-I!R?5wt2JJn-cdps_y?rTlDbMkN6 z4ez5{+bxu4h1vXm8h!Ml?b?gV<@W^dr0*^|C%bvmw+&2;PX(@8N~v3p(@_W_EGJ|CtmQ06gC-PaWxz4q>V@vO?_T3sjj(@NsswF?&ZIMqzm;owX3 zDBmF9wxs3V%?I0=OfN5ODO|rWUM2jJ)S;-rW7=8f&d--zYkDwGb;9F`wk?b0n!Z0Y z~p($xAoflUk8`j2kZ>q@-TN<@7fhgJG$G0FPyF0r1g7)y+MIO+0BJN`<_n7w^{pt zag6_j{|fRxjg$Z8`Q)5^cRu_USMm9**B_*;&WatJT(5Ge*6PDQ?f`E_CJ_eQsREcV d7#JB8Kok%_(@20fD;tQ*2!y6UT7(J20|51O4+#JO literal 0 HcmV?d00001 From e49e40cd73cf6b4d71fda1fa6c716f0cd72e6045 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Mon, 30 Dec 2019 14:56:24 +0100 Subject: [PATCH 09/51] Fix GdalRasterLayer rotation issue #164 For angles close to +/-90deg there is still an offset glitch. Fixes #164 --- SharpMap.Extensions/Layers/GdalRasterLayer.cs | 98 ++++++++++--------- UnitTests/Issues/GitHub.cs | 5 +- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/SharpMap.Extensions/Layers/GdalRasterLayer.cs b/SharpMap.Extensions/Layers/GdalRasterLayer.cs index 4bcff13a..1002a28d 100644 --- a/SharpMap.Extensions/Layers/GdalRasterLayer.cs +++ b/SharpMap.Extensions/Layers/GdalRasterLayer.cs @@ -35,8 +35,7 @@ using Point = System.Drawing.Point; using Polygon = GeoAPI.Geometries.IPolygon; -using System.IO.MemoryMappedFiles; -using System.Runtime.InteropServices; +using SharpMap.Utilities; namespace SharpMap.Layers { @@ -310,6 +309,10 @@ public Point StretchPoint #endregion + /// + /// Creates an instance of this class + /// + /// The name of the layer protected GdalRasterLayer(string layerName) { SpotPoint = new PointF(0, 0); @@ -872,7 +875,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, Bitmap bitmap = null; var bitmapTl = new Point(); var bitmapSize = new Size(); - + Rectangle rect = new Rectangle(); const int pixelSize = 3; //Format24bppRgb = byte[b,g,r] if (dataset != null) @@ -902,13 +905,12 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, var displayImageSize = gdalImageRect.Size; // convert ground coordinates to map coordinates to figure out where to place the bitmap - var bitmapBr = new Point((int)map.WorldToImage(trueImageBbox.BottomRight()).X + 1, - (int)map.WorldToImage(trueImageBbox.BottomRight()).Y + 1); - bitmapTl = new Point((int)map.WorldToImage(trueImageBbox.TopLeft()).X, - (int)map.WorldToImage(trueImageBbox.TopLeft()).Y); + var pos1 = Point.Truncate(map.WorldToImage(trueImageBbox.TopLeft())); + var pos2 = Point.Truncate(map.WorldToImage(trueImageBbox.BottomRight())); + rect = Rectangle.FromLTRB(pos1.X, pos1.Y, pos2.X, pos2.Y); - bitmapSize.Width = bitmapBr.X - bitmapTl.X; - bitmapSize.Height = bitmapBr.Y - bitmapTl.Y; + bitmapTl = rect.Location; + bitmapSize = Size.Add(rect.Size, new Size(1,1)); // check to see if image is on its side if (bitmapSize.Width > bitmapSize.Height && displayImageSize.Width < displayImageSize.Height) @@ -939,17 +941,16 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, { unsafe { - var cr = _noDataInitColor.R; - var cg = _noDataInitColor.G; - var cb = _noDataInitColor.B; - + byte cr = _noDataInitColor.R; + byte cg = _noDataInitColor.G; + byte cb = _noDataInitColor.B; // create 3 dimensional buffer [band][x pixel][y pixel] var buffer = new double[Bands][][]; for (int i = 0; i < Bands; i++) { buffer[i] = new double[displayImageSize.Width][]; - for (var j = 0; j < displayImageSize.Width; j++) + for (int j = 0; j < displayImageSize.Width; j++) buffer[i][j] = new double[displayImageSize.Height]; } @@ -967,15 +968,13 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, var intermediateValue = new double[Bands]; // get data from image - for (var i = 0; i < Bands; i++) + for (int i = 0; i < Bands; i++) { using (var band = dataset.GetRasterBand(i + 1)) { - int hasVal; - //get nodata value if present - band.GetNoDataValue(out noDataValues[i], out hasVal); - if (hasVal == 0) noDataValues[i] = Double.NaN; + band.GetNoDataValue(out noDataValues[i], out int hasVal); + if (hasVal == 0) noDataValues[i] = double.NaN; //Get the scale value if present band.GetScale(out scales[i], out hasVal); @@ -1021,7 +1020,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, { using (var colEntry = colorTable.GetColorEntry(col)) { - colorTableCache[col] = new short[]{ + colorTableCache[col] = new [] { colEntry.c1, colEntry.c2, colEntry.c3 }; } @@ -1038,14 +1037,9 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, } // store these values to keep from having to make slow method calls - var bitmapTlx = bitmapTl.X; - var bitmapTly = bitmapTl.Y; double imageTop = g2I.MinY; double imageLeft = g2I.MinX; - double dblMapPixelWidth = map.PixelWidth; - double dblMapPixelHeight = map.PixelHeight; - double dblMapMinX = map.Envelope.MinX; - double dblMapMaxY = map.Envelope.MaxY; + // get inverse values var geoTop = geoTransform.Inverse[3]; @@ -1065,16 +1059,16 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, if (ReverseCoordinateTransformation != null) inverseTransform = ReverseCoordinateTransformation.MathTransform; - var rowsRead = 0; - var displayImageStep = displayImageSize.Height; + int rowsRead = 0; + int displayImageStep = displayImageSize.Height; while (rowsRead < displayImageStep) { - var rowsToRead = displayImageStep; + int rowsToRead = displayImageStep; if (rowsRead + rowsToRead > displayImageStep) rowsToRead = displayImageStep - rowsRead; var tempBuffer = new double[displayImageSize.Width * rowsToRead]; - for (var i = 0; i < Bands; i++) + for (int i = 0; i < Bands; i++) { // read the buffer using (var band = dataset.GetRasterBand(i + 1)) @@ -1093,27 +1087,28 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, // parse temp buffer into the image x y value buffer long pos = 0; - var newRowsRead = rowsRead + rowsToRead; - for (var y = rowsRead; y < newRowsRead; y++) + int newRowsRead = rowsRead + rowsToRead; + for (int y = rowsRead; y < newRowsRead; y++) { - for (var x = 0; x < displayImageSize.Width; x++) + for (int x = 0; x < displayImageSize.Width; x++) { buffer[i][x][y] = tempBuffer[pos++]; } } } - rowsRead = rowsRead + rowsToRead; + rowsRead += rowsToRead; - for (var pixY = 0d; pixY < bitmapBr.Y - bitmapTl.Y; pixY++) + double dx = dblXScale * geoTransform.HorizontalPixelResolution + dblYScale * geoTransform.XRotation; + double dy = dblXScale * geoTransform.YRotation; + for (int pixY = 0; pixY < bitmapData.Height; pixY++) { - var row = (byte*) bitmapData.Scan0 + ((int) Math.Round(pixY)*bitmapData.Stride); + var rowPtr = bitmapData.Scan0 + pixY * bitmapData.Stride; + var row = (byte*) rowPtr; - for (var pixX = 0; pixX < bitmapBr.X - bitmapTl.X; pixX++) + double gndX = geoTransform.ProjectedX(0, (int)(dblYScale * pixY)); + double gndY = geoTransform.ProjectedY(0, (int)(dblYScale * pixY)); + for (int pixX = 0; pixX < bitmap.Width; pixX++) { - // same as Map.ImageToGround(), but much faster using stored values...rather than called each time - var gndX = dblMapMinX + (pixX + bitmapTlx)*dblMapPixelWidth; - var gndY = dblMapMaxY - (pixY + bitmapTly)*dblMapPixelHeight; - // transform ground point if needed if (inverseTransform != null) { @@ -1128,7 +1123,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, geoTop + geoYRot*gndX + geoVertPixRes*gndY); if (!g2I.Contains(imageCoord)) - continue; + goto Incrementation; var imagePt = new Point((int)((imageCoord.X - imageLeft) / dblXScale), (int)((imageCoord.Y - imageTop) / dblYScale)); @@ -1220,6 +1215,10 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, TimeSpan took = DateTime.Now - writeStart; totalTimeSetPixel += took.TotalMilliseconds; } + + Incrementation: + gndX += dx; + gndY += dy; } } } @@ -1242,11 +1241,14 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, DateTime drawGdiStart = DateTime.Now; - bitmap.MakeTransparent(_noDataInitColor); - if (TransparentColor != Color.Empty) - bitmap.MakeTransparent(TransparentColor); - g.DrawImage(bitmap, bitmapTl); - + bitmap.MakeTransparent(_noDataInitColor); + if (TransparentColor != Color.Empty) + bitmap.MakeTransparent(TransparentColor); + + g.DrawImage(bitmap, bitmapTl); + using (var p = new Pen(new SolidBrush(Color.Crimson), 4f)) + g.DrawRectangle(p, rect); + if (_logger.IsDebugEnabled) { TimeSpan took = DateTime.Now - drawStart; @@ -1755,8 +1757,8 @@ protected unsafe void WritePixel(double x, double[] intVal, int iPixelSize, int[ // RGB else { - if (ShowClip) { + if (ShowClip) if (DoublesAreEqual(intVal[0], 0) && DoublesAreEqual(intVal[1], 0) && DoublesAreEqual(intVal[2], 0)) { intVal[0] = intVal[1] = 0; diff --git a/UnitTests/Issues/GitHub.cs b/UnitTests/Issues/GitHub.cs index b30a732a..e00285f1 100644 --- a/UnitTests/Issues/GitHub.cs +++ b/UnitTests/Issues/GitHub.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; using GeoAPI.Geometries; using NetTopologySuite.Features; using NUnit.Framework; @@ -90,7 +91,7 @@ public void TestIssue116() map.MapTransform = mapTransform; using (var img = map.GetMap()) - img.Save(System.IO.Path.Combine(UnitTestsFixture.GetImageDirectory(this), $"TestIssue116.{f}deg.png")); + img.Save(System.IO.Path.Combine(UnitTestsFixture.GetImageDirectory(this), $"TestIssue116.{(f < 0 ? "N" : "P")}{((int)Math.Abs(f)):D3}deg.png")); } } } From 0e042e2b431feaff1738af561b26dfc25d0a075e Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Mon, 30 Dec 2019 17:45:21 +0100 Subject: [PATCH 10/51] Improve GdalRasterLayer rotation Fixes #168 --- SharpMap.Extensions/Layers/GdalRasterLayer.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/SharpMap.Extensions/Layers/GdalRasterLayer.cs b/SharpMap.Extensions/Layers/GdalRasterLayer.cs index 1002a28d..454224f4 100644 --- a/SharpMap.Extensions/Layers/GdalRasterLayer.cs +++ b/SharpMap.Extensions/Layers/GdalRasterLayer.cs @@ -893,7 +893,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, var trueImageBbox = displayBbox.Intersection(_envelope); // put display bounds into current projection - Envelope shownImageBbox = trueImageBbox; + Envelope shownImageBbox = trueImageBbox.Copy(); if (ReverseCoordinateTransformation != null) { shownImageBbox = GeometryTransform.TransformBox(trueImageBbox, ReverseCoordinateTransformation.MathTransform); @@ -912,6 +912,9 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, bitmapTl = rect.Location; bitmapSize = Size.Add(rect.Size, new Size(1,1)); + double disRatio = (double) displayImageSize.Width / displayImageSize.Height; + double bmsRatio = (double) bitmapSize.Width / bitmapSize.Height; + // check to see if image is on its side if (bitmapSize.Width > bitmapSize.Height && displayImageSize.Width < displayImageSize.Height) { @@ -922,6 +925,8 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, { displayImageSize.Width = bitmapSize.Width; displayImageSize.Height = bitmapSize.Height; + if (Math.Abs(disRatio - bmsRatio) > 1e-5) + displayImageSize.Height = (int) (bmsRatio * displayImageSize.Height); } /* @@ -1049,8 +1054,8 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, var geoXRot = geoTransform.Inverse[2]; var geoYRot = geoTransform.Inverse[4]; - var dblXScale = (g2I.Width) / (displayImageSize.Width - 1); - var dblYScale = (g2I.Height) / (displayImageSize.Height - 1); + double dblXScale = (displayImageSize.Width - 1) / (g2I.Width); + double dblYScale = (displayImageSize.Height - 1) / (g2I.Height); // get inverse transform // NOTE: calling transform.MathTransform.Inverse() once and storing it @@ -1098,17 +1103,21 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, } rowsRead += rowsToRead; - double dx = dblXScale * geoTransform.HorizontalPixelResolution + dblYScale * geoTransform.XRotation; - double dy = dblXScale * geoTransform.YRotation; + double mapPixelWidth = map.PixelWidth; + double mapPixelHeight = map.PixelHeight; + for (int pixY = 0; pixY < bitmapData.Height; pixY++) { var rowPtr = bitmapData.Scan0 + pixY * bitmapData.Stride; var row = (byte*) rowPtr; - double gndX = geoTransform.ProjectedX(0, (int)(dblYScale * pixY)); - double gndY = geoTransform.ProjectedY(0, (int)(dblYScale * pixY)); + double gndY = Envelope.MaxY - pixY * mapPixelHeight; + //if (gndY > map.Envelope.MaxY) continue; + double gndX = Envelope.MinX; + for (int pixX = 0; pixX < bitmap.Width; pixX++) { + // transform ground point if needed if (inverseTransform != null) { @@ -1125,8 +1134,8 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, if (!g2I.Contains(imageCoord)) goto Incrementation; - var imagePt = new Point((int)((imageCoord.X - imageLeft) / dblXScale), - (int)((imageCoord.Y - imageTop) / dblYScale)); + var imagePt = new Point((int)((imageCoord.X - imageLeft) * dblXScale), + (int)((imageCoord.Y - imageTop) * dblYScale)); // Apply color correction for (var i = 0; i < Bands; i++) @@ -1217,8 +1226,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, } Incrementation: - gndX += dx; - gndY += dy; + gndX += mapPixelWidth; } } } From 5feff8a7c7bf392bc5418e858f7ba4a5dd36face Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Thu, 2 Jan 2020 10:20:19 +0100 Subject: [PATCH 11/51] Fix for GdalRasterLayer rotation issue Closes #168 --- SharpMap.Extensions/Layers/GdalRasterLayer.cs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/SharpMap.Extensions/Layers/GdalRasterLayer.cs b/SharpMap.Extensions/Layers/GdalRasterLayer.cs index 454224f4..50eef4b2 100644 --- a/SharpMap.Extensions/Layers/GdalRasterLayer.cs +++ b/SharpMap.Extensions/Layers/GdalRasterLayer.cs @@ -860,7 +860,12 @@ public void ReprojectToMap(Map map) protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, Envelope displayBbox, ICoordinateSystem mapProjection, MapViewport map) { - DateTime drawStart = DateTime.Now; + //check if image is in bounding box + if (displayBbox.MinX > _envelope.MaxX || displayBbox.MaxX < _envelope.MinX || + displayBbox.MaxY < _envelope.MinY || displayBbox.MinY > _envelope.MaxY) + return; + + var drawStart = DateTime.Now; double totalReadDataTime = 0; double totalTimeSetPixel = 0; @@ -874,18 +879,13 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, Bitmap bitmap = null; var bitmapTl = new Point(); - var bitmapSize = new Size(); - Rectangle rect = new Rectangle(); + var rect = new Rectangle(); const int pixelSize = 3; //Format24bppRgb = byte[b,g,r] if (dataset != null) { - //check if image is in bounding box - if ((displayBbox.MinX > _envelope.MaxX) || (displayBbox.MaxX < _envelope.MinX) - || (displayBbox.MaxY < _envelope.MinY) || (displayBbox.MinY > _envelope.MaxY)) - return; - // init histo + // init histogram var histogram = new List(); for (int i = 0; i < Bands + 1; i++) histogram.Add(new int[256]); @@ -893,7 +893,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, var trueImageBbox = displayBbox.Intersection(_envelope); // put display bounds into current projection - Envelope shownImageBbox = trueImageBbox.Copy(); + var shownImageBbox = trueImageBbox.Copy(); if (ReverseCoordinateTransformation != null) { shownImageBbox = GeometryTransform.TransformBox(trueImageBbox, ReverseCoordinateTransformation.MathTransform); @@ -910,7 +910,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, rect = Rectangle.FromLTRB(pos1.X, pos1.Y, pos2.X, pos2.Y); bitmapTl = rect.Location; - bitmapSize = Size.Add(rect.Size, new Size(1,1)); + var bitmapSize = Size.Add(rect.Size, new Size(1,1)); double disRatio = (double) displayImageSize.Width / displayImageSize.Height; double bmsRatio = (double) bitmapSize.Width / bitmapSize.Height; @@ -939,8 +939,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, return; //initialize bitmap - BitmapData bitmapData; - bitmap = InitializeBitmap(bitmapSize, PixelFormat.Format24bppRgb, out bitmapData); + bitmap = InitializeBitmap(bitmapSize, PixelFormat.Format24bppRgb, out var bitmapData); try { @@ -1078,16 +1077,13 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, // read the buffer using (var band = dataset.GetRasterBand(i + 1)) { - DateTime start = DateTime.Now; + var start = DateTime.Now; band.ReadRaster(imageRect.Left, imageRect.Top, imageRect.Width, imageRect.Height, tempBuffer, displayImageSize.Width, rowsToRead, 0, 0); if (_logger.IsDebugEnabled) - { - TimeSpan took = DateTime.Now - start; - totalReadDataTime += took.TotalMilliseconds; - } + totalReadDataTime += (DateTime.Now - start).TotalMilliseconds; } // parse temp buffer into the image x y value buffer @@ -1103,17 +1099,19 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, } rowsRead += rowsToRead; + //double dblMapMinX = map.Envelope.MinX; + //double dblMapMaxY = map.Envelope.MaxY; double mapPixelWidth = map.PixelWidth; double mapPixelHeight = map.PixelHeight; + var envelope = map.Envelope.Intersection(Envelope); for (int pixY = 0; pixY < bitmapData.Height; pixY++) { var rowPtr = bitmapData.Scan0 + pixY * bitmapData.Stride; var row = (byte*) rowPtr; - double gndY = Envelope.MaxY - pixY * mapPixelHeight; - //if (gndY > map.Envelope.MaxY) continue; - double gndX = Envelope.MinX; + double gndY = envelope.MaxY - pixY * mapPixelHeight; + double gndX = envelope.MinX; for (int pixX = 0; pixX < bitmap.Width; pixX++) { @@ -1121,7 +1119,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, // transform ground point if needed if (inverseTransform != null) { - var dblPoint = inverseTransform.Transform(new[] {gndX, gndY}); + double[] dblPoint = inverseTransform.Transform(new[] {gndX, gndY}); gndX = dblPoint[0]; gndY = dblPoint[1]; } @@ -1138,7 +1136,7 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, (int)((imageCoord.Y - imageTop) * dblYScale)); // Apply color correction - for (var i = 0; i < Bands; i++) + for (int i = 0; i < Bands; i++) { intermediateValue[i] = buffer[i][imagePt.X][imagePt.Y]; @@ -1254,8 +1252,8 @@ protected virtual void GetPreview(Dataset dataset, Size size, Graphics g, bitmap.MakeTransparent(TransparentColor); g.DrawImage(bitmap, bitmapTl); - using (var p = new Pen(new SolidBrush(Color.Crimson), 4f)) - g.DrawRectangle(p, rect); + //using (var p = new Pen(new SolidBrush(Color.Crimson), 4f)) + // g.DrawRectangle(p, rect); if (_logger.IsDebugEnabled) { From 05bb880b2a4793c421ff08baba92018a47f9d9dd Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 3 Jan 2020 09:03:31 +0100 Subject: [PATCH 12/51] Add unit test related to MapViewportLock Add CS1591 to NoWarn for UnitTests project Remove use of obsolete constructor --- UnitTests/Data/Providers/SQLServer2008Tests.cs | 5 ++++- UnitTests/Data/Providers/ThreadingTest.cs | 4 ++-- UnitTests/MapTest.cs | 17 +++++++++++++++++ UnitTests/UnitTests.csproj | 14 +++++++++----- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/UnitTests/Data/Providers/SQLServer2008Tests.cs b/UnitTests/Data/Providers/SQLServer2008Tests.cs index 854adbaa..15e11758 100644 --- a/UnitTests/Data/Providers/SQLServer2008Tests.cs +++ b/UnitTests/Data/Providers/SQLServer2008Tests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Data.SqlClient; +using SharpMap.Data.Providers; namespace UnitTests.Data.Providers { @@ -18,7 +19,9 @@ public void OneTimeSetUp() [NUnit.Framework.TestCase("sde.gisadmin.di", "sde.gisadmin", "di")] public void VerifySchemaDetection(string schemaTable, string tableSchema, string table) { - SharpMap.Data.Providers.SqlServer2008 sq = new SharpMap.Data.Providers.SqlServer2008("", schemaTable, "oidcolumn"); + SharpMap.Data.Providers.SqlServer2008 sq = new SharpMap.Data.Providers.SqlServer2008("", + schemaTable, "geom", "oidcolumn", + SqlServerSpatialObjectType.Geometry, 4326, SqlServer2008ExtentsMode.SpatialIndex); NUnit.Framework.Assert.AreEqual(tableSchema, sq.TableSchema); NUnit.Framework.Assert.AreEqual(table, sq.Table); NUnit.Framework.Assert.AreEqual("oidcolumn", sq.ObjectIdColumn); diff --git a/UnitTests/Data/Providers/ThreadingTest.cs b/UnitTests/Data/Providers/ThreadingTest.cs index eb9ce135..8e9ab8f2 100644 --- a/UnitTests/Data/Providers/ThreadingTest.cs +++ b/UnitTests/Data/Providers/ThreadingTest.cs @@ -26,7 +26,7 @@ public ShapeFileWithMemoryCacheThreadingTest() [Test, Description("Simulates two threads using the same provider at the same time.")] public void TestTwoOpenClose() { - ///Simulates two threads using the same provider at the same time.. + //Simulates two threads using the same provider at the same time.. var provider = new ShapeFile(TestDataPath, false, true); provider.Open(); provider.Open(); @@ -72,7 +72,7 @@ public ShapeFileThreadingTest() [Test, Description("Simulates two threads using the same provider at the same time.")] public void TestTwoOpenClose() { - ///Simulates two threads using the same provider at the same time.. + //Simulates two threads using the same provider at the same time.. var provider = new ShapeFile(TestDataPath, true, false); provider.Open(); provider.Open(); diff --git a/UnitTests/MapTest.cs b/UnitTests/MapTest.cs index 7d423d0a..80e91c07 100644 --- a/UnitTests/MapTest.cs +++ b/UnitTests/MapTest.cs @@ -802,6 +802,23 @@ public void ZoomToBoxWithEnforcedMaximumExtents() Assert.AreEqual(new BoundingBox(-120, 120, -90, 90), map.Envelope); } + [Test] + public void ZoomWithMapViewportLock() + { + Map map = new Map(new Size(100, 50)); + //map.MaximumZoom = 100; + map.ZoomToBox(new Envelope(-200, 200, -100, 100)); + var vpl = new MapViewportLock(map); + vpl.Lock(); + Assert.IsTrue(vpl.IsLocked); + + double zoom = map.Zoom; + map.Zoom *= 1.1; + Assert.That(map.Zoom, Is.EqualTo(zoom)); + + map.Center = new Coordinate(10, 10); + Assert.That(map.Center, Is.EqualTo(new Coordinate(0, 0))); + } [Test] diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index e3edc592..7ba69506 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -19,6 +19,10 @@ $(DefineConstants);LINUX + + 1701;1702;1591 + + @@ -91,13 +95,13 @@ - - + + - - + + - + From 393d1d88b279bebd860f8d81df41ab781160109b Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Tue, 14 Jan 2020 09:40:06 +0100 Subject: [PATCH 13/51] Change versioning upon Build --- .gitignore | 5 +- .../BuildTools/MSBuild.Community.Tasks.dll | Bin 0 -> 631296 bytes .../MSBuild.Community.Tasks.targets | 148 ++++++++++++++++++ SharpMap.Common.props | 65 +++++++- .../SharpMap.Extensions.csproj | 4 +- .../SharpMap.SqlServerSpatialObjects.csproj | 12 +- SharpMap.targets | 12 +- 7 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 BuildTools/BuildTools/MSBuild.Community.Tasks.dll create mode 100644 BuildTools/BuildTools/MSBuild.Community.Tasks.targets diff --git a/.gitignore b/.gitignore index b45580a6..37b8aa57 100644 --- a/.gitignore +++ b/.gitignore @@ -208,4 +208,7 @@ I[V|v][V|v]*.* **/SqlServerTypes/readme.htm # Testrunner -testrunner/** \ No newline at end of file +testrunner/** + +# Generated packages +SharpMap.Packages/** \ No newline at end of file diff --git a/BuildTools/BuildTools/MSBuild.Community.Tasks.dll b/BuildTools/BuildTools/MSBuild.Community.Tasks.dll new file mode 100644 index 0000000000000000000000000000000000000000..1423bfeb9e07e0aa892eb0ce38cda5f112933b7b GIT binary patch literal 631296 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
  • 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

    }qS2l)BZ}G=NVdcsW`Z#q14{3s$R2 zj#<5 zCX9h^MHX9zo6&VXguk*Yi`6k=;`Nxu!;KynlWFnZH5Bt zCxvZBzI8KJ#5TD_4O79ZdqR-MQ%|z_m~~?XF_E&mg99jy1uPMpxtrI3-0i%5?icZ= zF-0Swz&22Ydcv~CVnoHYsbwKlCw-ISft*m(3j|i3_bcVH|)N!O%IHM?dK<(Ro8$^k46a2>JP21#Q7Pp-e z@?jqu1y#Ze>ClBJ$J|Id{3GjuZ4NLXkYTeXU|8WSyyJ3+{>4@)ljMIO-9kEltS7>Q z)Z8a%vF(AJH`X&Z#50%%L-fd&>5bQdgVthc&nZH#6`yV;+m4VpcVy{8pOb?d!Da|N zO^>46M97x47&XZ?@}EeTCLgpPAdD~sS%MTGf*5*b{LJqSB{Q~QB+vj>o-IAPj{!3y zfRsrGTBPewV~SOV5C<5j6*&(dX7{|rvlMx6pBlIM2P2^q@kRq~BV?S5V=@IE0V-fi zk9;;*N-|_J4QcC^OK7VVHsn!Qs02D_h^cuvbBCT;a2bIXk_;nIYHK}E7=Bb+9!uki z<)4qpC9u~!ICkp}C&ppf0Ee|6lJ0SqAKm_i%Q31a#IF{f6c0g%Z;^924X_!e8=UM0 z(=b^-v)e`8r3lNBgf7A+?m%4=V5pM~!^rI+2b*sK+!ye7*y!(ws&g*W(xA-uWx znnqLuJZKu#>62DzDHb)0+Szi%Q#Ip(3)lP{hD~HXlAQHmzT|4P9anH%+GceY!X|VV+L;P;7;q`SEz*bh+&!@T;nJQ^JTY2Em_W^kdW$^}>|S zU%5wlZL(>)g;Rr`@lpf1o^BucYjWOZO5ob$FP^d7iU$&Ho9Wr$m~TI7w=iyuFF1B9 z=oP10@6g|E;y^#|SO;FydM?T#<`cto$j%6Ex1|{Z@CRP9wP8)5b-|z8-du)Lis42i(HVJ3vt|%WY%olWEd+@*@O4B6r+FXw~#4@j&rrX;LechkP_CX5r-_ z{M&SRV%N1`tUZKjThk5EbJ~QT&5PP>fv|+f8*NE%@MJo7?~)8QsXF~%fd{@w4z%7 zSda$sggI-wrX|LO+u9(UW3|}vEG9j|oMoe9!&38n#OA*-4>nh5J4omswPj{O3%AKz zwf0DsQqm*1kYr!P=;>Idc5|;$QStr4Ho9DW56p+_e-&ri5 z!GOJ(qp8kop7$^$%|d`!uuT>ZYi2;B z>JB~grMHYJC2a$KL=p_`o3=3T_fpN`--Mx=e;gdVxLF9M3Oli1BAqkX0B`at;fxJG0kyE%j1{56K`;f#kc_&ku{*?fD1XzWz;szAMxbSv7? zOq_4gLhu0sV~{CnEguNTW>nEGc6*L`n=`#yke)9ss*ar-Gy}U^e9G@#nJ*h~(s}&K z!3T98Q*;LX9!l&=rLA8?9kGVB!cK1c;#-d;L%GGk983B3b_bggR=`Jt=82KASl2d3 zvXPJR4UO62O6UU}MKE0Ou4tSGYub^Ve!~|eCiMLf`wGQc=YAe3*=%3LSwWCt=|x`j zHZ*}ALO0+8nnYQ0?p!xpKjZ?^BH(NmaAo2sdwc_2jCv z-|^S)`s?@n^#}g?L#}f+ulE3z;&beLra=$^?uT}-oii*HjlK&xTMUq5geQM?g z-OP<9H!5H$%BGu=UhU79tFw0p(3HC}_hzm#_f@?c$ycvb=dSqH|H5D2pZw%gU6fJ#74qtL?(0-pe*^@Vaz$aPU;m5NyP`7J ze_I1nSW&6}j($~WUgI&J3pW+@S&%YN^G~QY8k#?z*NEk*cjirAd?Htwzt?W6bKg|e z!UQ8c|H&LRf|vSQ-7wU4Bf#owt?C{!?#uO1exFcshz^u;16>qQXMF+Dirsi1K1*Jp zc{SX;7H(b#zQ6FfODih#?<9A=mr2`WNu7dDDLp;8q_WS(+gIc5=i}{b@%D8AeYKJ+ z0wbOJco@(_A?07eD*XpFde#zHIH$o>PDF zFPHbt{q2{3^XUD5@V|V&>u>&goe?4+z*Rs2A+vM_d4?XlBe&x>3 zyzPGTPe1Uzy~Y3dy}M5S+yDKycAVVs%m4lVbAS1tEM4)*=ej?5Cv>BD-y*t_{@KF?9yI5e0^xfjcT)IMAT+UX= zo{0bf+ffKV`#A|XCNljrKz7<@9cPDvUMA$3L#{2uvAtFQH|zhMO1gf+&Aa`aE=G=< z&B2!&h{NgR!v>v?3IpAH1j#`+^UXc#-vvc$nnT&3i=W0s^*<_DqjPnaKs1Pe%7PfK z{b5Y%IOxX=lk$Y3L-~F!3-v5MoReaGTJ>zv*lpAQ?fOp)^@N@_3vXNW-wY7&hGURP zDYk+a%@C%X86C#?zhf#z|Dk@!E;{0ajuxVz$B-VZisqdg2~e8!x(Z60vaS_a)f zyWkmeL^H+TBi1~o6Lb3J~#{0v`+8_4plyI{v`R!9owUvSL;lwbxg$qCI zc#mZq@>vRKWrE5V_IUI0Mn3H8dopRC%N`iG*u=|Wg70(9*KcU%QnjA<3isMLGm3~Nn z3-X5S3v<^r5#a#@DO={UAKN@3kg_^F+7LsUsGJoi!pmr)czn}j8oi!x;DVWd8OY~W zKP-2vn`sLm65X%}1kSuw9fj;}Pk!5%3$p9OLdm-+_kiCWvRC99%QayoK9=J&5y`TI zF|j%PQ*ArzS-y;@iz`^MHWMZwkjl zVP!p{0m9Vd;1X2+F%=9JoN@RwN;}XSpm&+0^eO$_1mU3CfrNX7kWjU7{xUArEBbSw2^gk_keY=ZDYnO$( zI=y`MjURky^XATj4|R8QDd^J0T}P)bg<`nm@||Cz*o-du^2z+tllf&QQ$Q&>4W4hp zqrkuLcfR*c`P!kcT^66#^kTs@ufSsFc-LUg6yQGXRbit!tsQ7hBU@9|Hqh-VK(lrJ z4VNEs`A=N_UoL;m#gm2~gh&SMAoZB^FXG#%q1yrJm2MNHzO`6^?{s|p=gQsrFSxwQ z#RKFoF^ew|JLCJ|?p0>+)+r%>4g59L(hmB?rg?MQWD zL}WJ=(-{9P+0tCmF0I`NHVs}=EsH48PUJMz;=ADa(hGqaklw}RUM@3SoUqgBgvw-) zmAlg^cQV}f>aIN?TRX9N%Z_b#-Z?%rvUAJ!p{?6@j1N7% z?ZmdBr?>Asv3309(|2s!GNBWfGUaZ~X+8PKeV0z3nHb)4{_NRH=T1*w9zHrg_3YH} z(aEW4t@3s;c&?~o{JNWQT^!Bd_}Inq3kS}h8-3x#H;iO^bYn$~J(PCtEd{GuY1)DmRn+;GlqJT*OiVd}0;o1Qs6ed^NF!z#4t z%=wFxQ|C`kZ#p~mG;sUko1D95%XjA7$e!~TE?+$T%&FAKBDeehWG<1)E@^Y)1o6O%&| zBPS+?Mn)#?777HHH=o$N_2iDp6O-eUzT2_L&e1KqcaPq;b!hL%p1mrv_l}|a zcHc*lJ)5`QzhiXI=w+gNG-dJ3S?Or`hE8N^S4>GL3?#yBut*cO7FmxN zagjOiTXfFdi_W=o(K&Z4I;XxK(u9g1^W!4%w>33Zl6@Mw#llsR5Ev6RrYcxuo7)1F zEVik?xA_){kasKX7g_DzfamwfjeAl<{O)Mv@9n@X0`Q)k>zX__oftJ8mo94m`qG^1 zxj1>@{M6~`^SoG+bA{(m452F%#_G+vZj7VTvOjzyu+g)p&&hUcJYJ9rOdhf4E+&d7 zmE)pRV(RhIrPGP+HMPTZuSrYX96cIl~#80kO|5NJxDRut+qRxghp>gy7OBz=1Z}vWw z_ow$};w{Dz^8U2fu9m@^>woy^sq<$hrzdZ(Ju)>}J9O#vcoV#p%m6-Jef} zbhuUy%=F~NbFx=Q&z(3w0eIu*CR$S;nHqm)GDS-s ztEY4B#LK(t%YV$^CcXjqMmzR`! zP_;d-|Ht(IqxtSazPps~UX|}&t>;tu?%sTNzuKDpvhvTosk@oqpyUvbf1=CW0BL{j ztJd}q-c733QEom;rf-sI8+UJ0&k}Y!_&pt@k9h$7o(*1ZP}=MWmwUPFQc`odD);rqR;cC zqzA?B?px}Ys#jFDLPdYb<&Si!e?m#Sc>2qHH|U>Rq9T4jhANpK;4-Moj2f!UZ|1U< z%MLDgQ@cj0sE4_EF2Am<^|j=`%H!9#{4p0HwKDgo`R)$=&U`}|=D^_WaSdlzUhzjN zwnVS$8kOq&8(hA?<%@b>;^kj)Sw)U7ai=k;)Hf%Pg2OsUp8F*(zs%)VNfV^ixnJW> zh_BRdXIN~TN1z~Jt<;5$YW;V)yPFrb0i<#FYUA$njl0(xcdtX4^N&+_FPDSV^?feF zO0~X&%P}rwnV-KGF9f!ka~es-BsqNd3?LzM{MRQIA$LREozg8HiGE& z)fHrXL6^GvT&dINd0y1No$u~cy-@s)RjN~^abqcSdRW`8bZtm;6T`OYUPnxguUL{@KDY6fnaMaVhiVcYV z2f*yWK!+clnOt5=_(S;*9?TVUt2=5gr~l>7T233qYA(-BUZFL)F5Of|3$=W8e|cHC z;5u^EL)wz%^86-`WuNJgzdoXpW$MY7yK4>sRZtE57Hc_?x&C!2WUi;!?K;XmeML1^ zq_JX22fFypXsR_|9w;wUN_T#hHc722sl=*sUf2Apd{Llmt z9dxSopswoBKHtLea<^02&(NOI_|dkktHM*RtX~S}%U#N{uUv5C7*OeauIR6l?6nS6 z4uX|QMSS11FV6|k{IbfaPDk6qgK+K}dZVnQToKfiE3;ovp|ZN-)JSiwvsf$-R6ed^ z#af5%msQS`NL;n7`hwca6_@qf3uWr7br(zJQfXOr_KVAU)VpG7S&wQhE~`$LdR#}b z^0?&kGL23yx2!Uu(z)u8`dyj*itn3#K^His!*8ln0)(X5Hww!@=i9=C&@}sYPtExu zsLp;vnI*V=wwXV%l7B*(xdHz$H_%(lGf@0bQ2G1zW}`*~)QN=3MPW)hr#dPUup1Ci zEcvI8O8`9Pt2^2VsH_6XtE3*)nA|FVUhRrJtFvo$?z@7KmbQP|vZJyUq@iZbQ(vqaRTpX%II z{VUfx{7v#GOeo9Cmh-BJC&}qKr>vYyYn|!*`B0v^R1h@Hy{vn{nqNk{Ge5A}#`SJM zn=2VFfn4bY+hLm}kXLL;Pop|R`rUf z3wAwLOm6tip4$H@zEC?ARaaDN9!@Cl^ORGw^qUU&O4J#>tB+8JkXjMgSE}{hK2`R| zm?s!jQXf$%0V5gd28z@(`z7G&uhxIw*H<5xJS>)rn{)Y_U3s8iQ1n+T1CqIFtj-vV zDqf~Ywf+jyL63h*J@wD|>l^<1g@RtdPd!c<=PsKZ1l%hU24dQ3{p&vCcSLG^NZ>v_ zR_fnX{rPJBJEAXT>T*s7kG~H3_UHA3D)6#xzn4w>{U}xEHz3MXMj6VI-_|GXT&;C# zH)y52te@uQPs61Bt9UE8kN1h%=C5#9oxj}g`#*oBM?^rPQXmZ8F@&;n%7{DOg_laR zmpO68ulo+I^|jR3(w(_q=RrS0qV}zpixP4CvL{vj;kMU_-hE0>kAM4GeEV9tr`9Fu zTr8@9&-hth_VoC&pA|6O;ceNM{XDNF@zvPp1uIqiq@JE7H2gZRy?PMWT=f(&f3-6I zl2E3>qWePqS`9`X=Ff}Yl_zgn(k03h9*l2F)cN9)I1*{v11zYe6WIn_ z&i5sWG+f_N=7q8(Pf}3j>I$_k0u$67{75&6N@kCX*NWN=ES~9LS1`QO(^Hv$rPJkl z7zhM-LdJ^_>NZI4d zgtab8&?fDa5_MWe`N<--G2~a7;Gr(jkSCJ*k{%h$UigV*>Pz&{EzGDQRU=Eej9Ac* zIxm{(Oi;{SLp5T*i z_wVbs`-d;t!oG}yz;7lUx3Jh zN9p8xdPD)*U`|HMfK{aoj7P$*f&dw#gk@-LkIh}Wv2iu%G-1gL2znsOgVYLYlN{X| zHBWjnVi-JGYZ}CS0+9$Y*8`xH0{zqke$@kb&16%si%`1iuYn?Pq#aaUUvu+4^|;e-3j<)s`)CGO?`)Y+3Qq_8|Ayf=i@$=(}-w?eoA^% zfZS$}=X*t3LDYGKYZ{s${-|&*r4P{xW13*7FZ$8ULb6`JBFhf_LL8w6%_kayQ;cQ> z#3qJnf2;x=JG;>**dYlwYWgBbLKP*=q}A7t7S*3!`Yo58=J^t-^}CCf-pOOJB*=A| zfen%;fP+Q={zrpPFX$m-#V@=kV%dNk^E$S^16b-7QnztAA$93ig_!Zu|^au3V zn;zGtH{#)9aS69w14aKrRV#@GI(&`H0z1mCKxY1G7NdDdEsiyG^R(=&hu-l`_UqYWL?Xy7mTuev5^ZLNuzrYwbwc*OtG9Y&tDNCC5E(UThAaD z#L7X2jGdoR&uMuz$d{omxbkKBkh(GBD}8{uTpMgK1ErsKkyW!z(bgk?sbhwC>u<;X ztuidn^@(}&VqOj&y4gVi)kBimXVnp9TDCmbrKYcxH6;BqDsoEGkSte2^mpb{j9B&R z=NP8y%qu?a)AsPPz|wR)U(lve{uESCrE>MR0aGQs*;2Nmxysewp`yxFKO>s?veocK zB!KK(U+Y&b?F~_4UtgcU7FDJ2`;N?6fuebkEWPgu&~j0)g*hb*ELWFo)POiAxPALG zSD~DN@<2!B>K9bAJSh^+vSm7ub>$+muEPr*nOK@tb_}Qom8)L_odX@6E>}c$t)ij) zs#RXBb`@7aam7^{oyzR~KK<}(?hASN4HAyL;Vvg7HL1H^jETzCugL!K7Q?J&UD8fx z>CEhY&ycfU5ko=%vzL@fz={+s*Q{5I{Ze`2+pAx#Tz#{0^(}!^E{oEHx|w%`B5Agl z6Ti;c0t6DeEfde6jwgGdHfA6xKtWVU>%`U3Au9v0wx7nzfj?^HEI;Ua66)SoEmXCUOf`aR1z>T@C$s&#q!eHQchsepLD ze%z2T`?AMy{j`QYaUDooA@lnC<+##V^6RU)49RNT=fB%m>+07uXtsLw2YOj`-Rh7% z>S=xusbrWrq!Re4UZR58Pf0AQw|Fik;D0CxwNU4;pR#;O+Nb5h!*1W@wUPHh>Z~=C z6!)CB#=Y<0D<%5!#d7sUo-6e)t8TgBzQ`!lzoHvGtK~_N00K*5L?bGKddnzD10%n^ zV0v2_1XTU)>WeZGl;9nZ^*5FDEpPX|SdzD`T7T11w_iO`zjo<~F%~)Wm-`_mdEX?S z)tlX30Qp`gkCSM{T-r?Jx#5x$H>AL>UeD1hW_HQeI%L(oY4%F@`VP7at4bUq=c zAIgJYogb;ruN$c1BdN}BlZ+Nk2)OF}F8!#){HRF6)3JD0qpr%d+|0BKgCyar`7w`U zdIg<%Y#cA=mt#@*NjX-9&ceIdN%-MQQWlw|`lHd5Q2c@ZX#j;tgbLuErjFSW&rCw) zfX7$;O|mPC{!!1&H{_`h&uN(fCn5DhW}Z38BVzM9UtTqDtW@W;(BX1bA#SN=rP4B$ z7Y8dZuB*&{I+j6E7p!wh_mqj|D=%(TVHr0PI^w@BSOo>BCtWRv$86=r5r}R6i`cTl zn%*+0s-7^LSN~K-I~JBHm($EX~kMp$SEXuEA84VwFwJe9lj@r8Yc6 z>Zrg5L+Y?V?mIP7SC*@2gLSpUi}p~P5*qez$+BF>&2AaGQQA?N`(=L#cpS3%g2B#g zp{GHNUO&tuEvoaoeJ4u(T2L|R87;sSg@t_e>S~d;$X-vfRfSqTX(|gcg;y1qRxgVL zu@OXtW3p<600})$*k-3+9bw?ASGVy~c~Sh$&u;rcnCfY)V}mQ}f4+MI>ikXJ7P~fR zs9w^^8A6$E4|Wu_D!4(c@fF@^4TI~Sa8(F~it(~K^LA;2Ad#3W_xEsTt4_SqA`{oW z{`#Q5?o&HGx?|k9R(dHCq9``78loas$UFA~f4xFW2HKZMsK22q`nrn3qY$j?pif<^ zD~@;N5H0IE_f4+y%&K^oigb}d4pUv_Ddm2%QoD<4-#n!x-R@t|j$D%d`Zb@jRag9< zy6*A`^o%(+*Skp`1(ul~7zT(0SlhjUYteqnv}2yHv+} zLUq3;L66wUto}$-NqXmlV3H^POda1IC3$trX6VYCsA){Wb&wfQ@ojp+h=wq;R9&t8L0xw+bXlk?QPe zB$pN+`X%Rl)yTWq{lW(UQ_6We5G$riqfVcyvscS2G_*4^`)A&%&VE9zeo{ZxgTQpK zpd1IQvoCiSaEBlCy}jBeKxfa=id;ws1*zn9|3F7ip@Y1ikt`6Fz@-qTAJtnafLQYT zG=&os3tciDRAo;=QomfJg0+Qe^+DlP!18FrkSEDf`ng>yV0|W%^}i_MQgDJCED4HL zw;#@ZfT?clfAzuYgUbuOs+4iH9}!hv!=?euSEq~WY~@ssr{(ElzH&-rU7fC+shpvN z&?M;JP|nIk0mNwtnAU-zArY(e7<{62uHS%9B*2B9Kzo96_H2)+qf1CwrO?_7tSQL~XhqEc8jvCQqu-cu_3x=Sl+T8|w-( z|0~8qdK6yitDhu(Bji&wNI|Ar#l~I89Oy4;#BC&2LU>~DuT^LMlm`5NF<)@`J?L0udXZfDf?Th_HF))65rOd74&>6 zfMhgX6NqS8PM9P9NP3P41Ps%OHi5+y!78L9^~auv>* z8}j_~?{z8{_ld=4u0lLB|4A_-v>jkr zCUIwYuoOuU$r4R)L_XftbfY*<4TM9f?M&)&d8uHgulld5H6Jc?X>8Y))twb;XLfCM z*0(~h>D&!oek}9Xmv1g`>aq_VQCnJW#1A;DppqNNdN+c9ZUll5A1+5~Y%W)Kr*esB zRc_S&(U%*k4d|iO+5%!ek7Vn|qnkUVtbvg=St>4k3pZ$arqA7JN>zFpXSRf^nz zulk94b?!6OxzAPSe!Ds+9jY3&L?g&)alASPb*hgW`>SKi3MGH%MX;8d$5s_|of{W% z(8;-breWyD2 zT~tv&l&rj9_DXg3cS}e#e+s(YVfFVGkfXS)HHQ;p*5xK{aDfVH92A$!Ki_5}uNof^ zTWg9fkHcWKzR}(&C`SK?oavWwQ`aD0sn)mQl;EMh(+iCHu4Q6CdQjhAtsffDD2rS) z!wyfrQ`Oy7lz=F`ejMqCxO-E;`RFowHqifpLN8W@XI8st^y_DpG6u>ds)8mgdnsF; z`GoJ|WxW;TS43O#R&WPR!}5~Q#xpo^>wyM;J9#A&0{!%Y_~UnEv$0 zwvZsx%>UCpLaLnxUxth8U#Qk!ftu@Ilw(5f2!NOy5vzsDAkBLVt6(d2%nKl6x%yZ0 z)xqk_OL90=>%!O9MbhQ!m@*|d^}kT3s`YoO^{=j$nJ5-|3z;EcF*r=spx8y}s|r0v zP@xUzT)obV88sTRm4+k~lrU6~vw0-sy529gMg2Qqhq~a~`gimXOw8_I3GdavgEyr9 zUBRF@Y@g4+{YyX2p7|+`1u4U_SP{zUh@_DJhmzt&^;%fhIb6zw%XR-NBp7THS$ z$yOGvQ%){v2~Jpyx}_qkV_pKwDY&E{VYUOsHz|I1m40; z$}u})Id1l5BQhu9H!&L({JgzhTUZ9k3-E-?5H4#yxXF*&SyAYbEMq2=vMf6UnL^9_ z^)-L}tk+PVS5TOL{JejBU2$Ou2JHoqDWz=zbR+!py(9%Iq$ppqFrdIApZ$+-mDvNm zwEZgsTeyFquo{pwE!U<433C6<2>+`jz1OJUTb=tWYp*)@=RL~$XM#jO-_}oc?k}rz z|6UnnGbJ5Nh@O#M;SCEv>o!U57awmdbo#8qQeuVpWDLxVO9skXUxoH&!lhfeF$%eY zAQLz$rxNmJDOLkOMfd(@Ii{XmUgnBZ&+%husOAo>)*BufQZ14sBP#)b?Ifz|Ep%(z zIVk5}cQ?qO8U>9ML}iNYQ2#I+^$$MsB^0jRsjhzw3KZE9{G=6gU8Op|PoUz2!0jfh zl04$Pp@mE-aS`xc-rLuGLt&}Gt8FOr-Q7KTp}L3-X5luw1h=Xx!P{C+7h<$_+1*zt z`H_(Z6Ha^|=Y@`Lfi&MO!=O5UP~@c#i!4<|^GE%?x(((!)P{yn2-67CU+Lkl;;n_M zCl`-LMnN^B74={LlYo7Xxk0vSU9VLYwJ}%|CG1I|u>O55v1={~ z5`>r-BBlz}p`NmcZZ+auOkcB{MR4w=_30-NRJ^p(YbVqgLOM7-qim6i=#pS+)c;Vz&9W6%aMj*e9aInn3VA zxpZPk;cGOjJ!gjs(r@~~-HtT7r&Mm41AEZJ|eB>j}GV zgiX_Uhtf%95s7CdJk}yaLR&Ro^dzT%E6vk1eAr|p1y`Y&L^n`a*lMb~<&9i6E z7w$lv%W}9>oqt@i{BgM3j{j5?&JN8+%OL~RjLQ#$?Sy?Y|3fU( z(ZouT+8y)Q@Pb@YQRalUyL?&lk7{ldmCb!$1E9!e#Zh{oRkx?F?|avabRz?*{ z02T+l_L00QeyFfnp>#exE3c~}>sV#MWqRE|QkNQ1WxJ_xqa=w6QhO}w?*RH_Sn z>JPX2QC^RGvAb-hGiXKsM{;_J&zD?kg_YI$3vrynJJqTB^j|=c!Pl-!$GU>3^n!0A z$?Ln_C`aQ(sbxx8r#-}!kq3faBw>;wBYD+kk9)TLc%RKqRaCnpi>o#1^>?;!QURT! zbwrTdzS3GB?@!V-0TR(+i42lcO+8m5vk*yk(cg%FL+q;N7&qj<9Am=(z% zg`D|2Tt?Dm=vHhqe=D6N2zwSP|ky;J)I$)Uo-q)-=E)vqK<}lC;Q=0#-$g-oWifz^ixQq$H}52-j!_ zI+16=I4Hkaw1$H&g)|oOrIKbD zCH+yGT_wa%Nz73oxKV$KB~g)Pm0EqeI#hkc+aHZ75#*w&AXcj`*zz)Xa-!@RE!2Cj zl~#G>0oLvO%YsibOVjoa0<6$)tN`biEABaoC)ABa6KV~KfHwO$UHCGe{Z^86g5dNa zJs$$6d{O}lh12hTUn_SO3ZgtRV*I-#AVi<4OTs}^P9Na&^~&7vc@$y*RJ{2_@@8N1 z##XQGLw6w$VFAjkUCCM%|Gi&0_UP)ZfA&)G^?RQB#md)v?)rCUM}Ow>#joxCZ~o?< z|N0O9&wtCeEp&Q%&OP?8|Hpeux9H)ob6ebvd~eRVf1=AB`u~3y^q?D8dp)U@YJCXO`KBq}A*^gsr6PXYl`jo~J%%zUzBkYpwmZ_RHC4pP3sF zF97Silu5z|*dEIAd-vz#^)qsvW3Il$XE7)3rSBSep^E8s7np; zk49N@27ZZl4YL!aukfm2jdGSn6o{qvV!yC`xX+V^MO`y^*C9b1O>HU(}R^v8Pdb6|WTYU;NyKSJWKB&!bvO zxh9xD|AL^ubi`cGotaHBeNl4rH!3WZgr!Hr(n~Bw%?^}YFMCk>39l4WdrZ*FXgr&(S&A}^b7UHMdMo)#M=fl#gEY&dYp(NukYPO=} z=Kiy=^c_lWYSC-0sf$;N=^VGF6-rUl4L{vjy9gy$@8;8jUM>k^1t=Bbm13ravD?E^ zSy*}qrA2r}&5MlHw3VYS1HT2v&EFbiU8#bl6!RfUyYPydgJJehl-zo@*9GgjR#<8j zmfD1+cv$Lz(uH`Xn9Es;nsF$}+D$RNB5oaEi4c>ukz)Q6mga}0M^SRycPUEJU(~#U zpKe{PN69U3{nm#2!Y0JrUa<$I@9>J6_qomMnz}`9tfiQiD7l_HhNbgZikgd1a!Wo6 zCAYPvqEv=g)I5WqZW^zmQ8kz$vrI*XH8SJF zQfXM48Tf6ij#Rm zlZKzJ)EXtXPP4<<`6$WuZD>Y>vFpQ9X;_*UmX?L3H^S1#Vd=-Pbi$-yx~;-eJS_DK zOGRPnj<7U8EIk{R)`g{QD7p3bIZCn)8k(P&O)H%*R~I1Q8NT3IjXcarw+AdGOV?^3qPgQ)I5ow zvX`Zp7g1V=SBlvZmUf1veJn-IF_h$}l45G!9JEMhDQeC@$?aR6P;y7JcvowB!J=j` ze#+WSF;}7FuBC25$!(cZl;k*>V(w!$#XJ#~US%n2-b2Z4>5s$G7c4b52T*cj=&!I8 zn}WH*tGQ{#Qq*)nNv3x$#_vlJpWaPllzH zVd;&q^d3sP@QRv0Si<_78XU*#q2$(S21;@~#WfO2uIGzUa%*yUSQ?FzT=hlG)UaMD zO84UxH7mo?y0G+aSo$O^eHWJgLdi|7#w|fFui_6zxmX%vu5=1YZhvbR#&V=2XajFLMe zeuk3k?}xDTM_5X}HOQuhrF4{J*-}hvl-yN#ZdkgErKq`<^`hohl-yZkHcD=7ED5u( zhq2GY(qAaK<)~8}jNSGqxn8=VXnx&3Gx zV^MP$CD(JU5;wJmrUgo_m+Y`~0ZS=nI7)8R-H4J~8?(dM<0!egUyYLMxq`72vl}H@ zYf*CmB{%o>HaFcAlZukdHbco}vryuAVJyY;3`>1ca@VIr!_w7Z=?0YCet9cnhs>QQ z$-k(XkCN;836$K_UPnpBMnm%W-3} z4Vs#PD7hJZ)$mM9Cd3o>ggN-<}lB->K|4J3`eleej+IwN@EUH_+=hKRJpt2(~uCifqV zIS)1xY=ZGs7m-xcH;i<`Umh(+KbJ6)j6ZrlTI-Ao>%{R#V5L>E4f@H&-y)UBl-fb$ zZ2YlUiHr+d^)R=YM={muj2Qgw-ML!l!LZKx<{qu{L|A8lc^++2%}Pd+@%KJM*J{xh51z@ z=Y;(fnm_fn%B6@D;?)3usTaS^9IJI&7#YLJ-C?8%kr>wM1B^UtC&=1Et0%)cw`!d_ zx^<-IXYK8Xl%Ulr)=9>_on*TZe}8a2BgWi~`&AP8uu3Ei)2d^>X5?9Ww>jC$HvKK^ zrwkF@f`hC%0k7(&41YaPB4;pCYRXM}jdThl^G&u!5{UHHRtro*BV)tJLndD%lM#_I zR^2>h3Ux_uW2Aw3#$2y;W;4GWBG$!7SH0=F(tfR+tyH=M+Sm;Z@!2Fkfn&Zj7uopP9oN z=^sXRn?E%&G>m+0YIrjBE5pb>Q%57?8EJy?)D#h8&N$soE7=@3mua1;VXGuN(t8wj z?m*;zyyWjS6=|I&xNjhlWJIp_FnO~YCd<_Tkx5#of{`Zp%RiH_d#9Q$h^RAu}ZV~K}5D`6GX1n$X|%mEReDBtGzKvw#BKiGc{tphqTW5j1<|L z-b#&J9!BbV>oqbF5jR>Ic$+lRWTG)s5v^`c^Zw9CTSkiQ8D6zynbt$dKZ#cpbB0$P z2i8>c79%rlJFl)rHX`EYxt&*ETfHAf+IuHyolhAlvz@#a8u_|Pq!mt&spb$O)rQDe zjeBjAXF0u@Y(^TG9Pb?M=ORXa@y!t_k2h8$^AM4d*~DDrU7sxHxJMaDHiNv|bn5T(+ZVBw~Iy|+Rm|3w|y%E{(-?>&ut#z+%$hqp~5--LDMc;9H` zm$2tK-gg@LBaD=Le`q9mXt3QM_fn&>4r?;fz`W$06P0;x#7GnKDkJjVRp#)p-QZoI zb#f4qQCDOuyh}CGBaHmuEz-zkVdT`v$@n82sb*9d$%x<^ikSM%j5IK(;e%JHQx?`~ z$Hm%oCq{gty$axy6UnP>Kk*17P zH`hmcXyml8&XmaIG1;3sA|g{yHd7)av`&JNCWu_E5&2ZW&0$GoqDH!hb!JEAY2>0X za&P1bja(K+$|K7(QW!=aj=Y9X!;Be)i2O@7izC|+Nj1+R@)BOj=4pu_{RSfq5cymq z**ND)ory`y82N~GB=V)!*;6I*jYbYuiF~h-YQx>M8kl8~13LA_jFcq3%Sa|83%o6n z<65Ujl}HpH)}@+)Dv_ER86QU8kJQ!3onhp|NR#R&9Y;|{rkkyiW*T{#k(J)oNGpxJ zUL|sxMz&OmwAIM&Dv|aY`K?N%V|9adSpD)~Nw-F_HPQkRAFl@H6Gn2w$Y+dn41sYitMt+I(*U0KH z@>^uEMm92%Z2pK`j-^O7+rr4v$T+RD8xa{BL+sJW#OgA_e_$lp9E(iTI)8>ua&)`16_L|Gctzn(}7}*)tc}ODI7H4w5NH&kL&Rt=vFOu*Z+tBj^ zVdQH@7KM@TC4zodF_LV4L`2r%n_=Xqq(bdyQ`qXKq^q^n?y$~bMt)~xi1m_ZXq`Ts zL#);0M>G-}5v=+4$=fy3fRQ1#WAb+z$v~t7UdiU{paQGbz$UbiD2rtGBVvf%gAg-k`d`vTejEy zDv^Ah)-z$`rD!jm!&PDAjc9MJvzCzt=B;QSoz`YXhS+x`f}VGVtv-p4(rJAiM)pN- z)mDeYRzEXx9FgRavc?Wa=WCr4#PAwo|A;O{#F!Q^`G@z6(ekWq{@Feg@%NUFIx ztkVP$FFn=Ba|rTpx@lT%L+uFG&t0q|k&U&JkX{n@)3n-qwLL^uhLProNS%#g`pIlI~}{BEsO6TQ+!l1(>6q@Q|Wq|t(FM3%3onWtE(N; zR!LX6t<%7~hTlUVQ*RJP-esh+=k16{&lzEz?-8koNJmDVwFemK`>)8@e??~fE3)KY zkq!TfeD$wLWRzQq%BeT|SLB?3MTYzGFn2fujPG=iTA%KQZV^kTj*DDxLgr+^-CEZa>agY|eDT>8I@_3tNxc28;jXmq*O z8(91b*tEmI;@8Qy$aj=~uh$&)wjeItuKatxHR7KkF8oG`&#J)FJ4EqYXB<<(w(yg| z_Fy*IAM6Ofn)zGEyT~U&>1UPW-{#-P^xj2W_z}6AJVaVtsY|}0GHNnFnGbn~Bb4_n zojXKRJZgTj5fFFZml8!_~FK>w!|QIVjWj$*$x@ z=*calrV&E(Hy`U$RmupH;YWx0lcvVKbFOF&tkw?LWRXJoJ)e`CHHK2sWi z_HDsS!HYoY?^<#eDD9siD;N)^{{>v8^C$Bs)^YU%59TlEw-fR*pZ&bZ9H$P6-AzH{I*oy$O(j_ikDWLA#z>k%J{aelKB2Je+XZp zzrp-wP|h>Ez!B*G2T=Bp-${$d#t-4VTtkT`3dlNSeX=Qd4cfN?i@-LZ%y$P+*2_8S z{fhgj_bVPoUc>y2-~_b04ZIPY&G-Z4GvsRWP4Yu>7bwg39VqjEMA@(SIP)hqaPx5r z*%p-b)0ut&DD8)kMPvy%8 z7e5`8?J3(!#%DHs1kP(cKso*f^K&U&*4so-`kPIEl3qdo5}b#2hrkEHRNV94TxEZf zd*DxCc{?ck6?X=uKiRLIK>d9BU^2MQS&r)^sW+1Gv7oGcu755+qD>Kctpsc5<;M3rp%1l$Pd23dXvV1cCZ`1#| zTw4&Ad80rEIm<7C%wLr}K&DWLRoCYeiKKwbpO_AUTrey$=XfzM<9 zgY)wo_)7Rv@MZ8_@HOxYP}==Q%Js=xh}UlH;w{J=vOhT*+<!21@&`piF-l<1*f*pWr%YGV}9Ue=R8U@g?imYUMl_HyLp0uNSxj{ai>d z0Oh zwTL%U;yMVF>2)FV$UdOV=VhQQ&(-8q#>?o7L23U2co_4y8ax8N4ITwQ0c}TXc7svy z09X^Wr{aA)SQ9+CW542)K$%`=GFZ+Y^h-cl&a1(6w7ZV+;Qq&Cc&m;LO$jLPEAIx| zcC5qqo7T*SXQACAV3#WEX9+ysabWRtW_@$nixIR0#; zZvo{xf1CQi;vdP#>2Cf{0i}IwdONZUDC4pxDC#+$Jji+#@6CFH$PwgqplqiS z`u(8PUrb*M_P~1HK>wKhg8Y$8KEuslW3nA6{dA@Gp19msRY-sAvKrhheg19>a+!TqvX z@bj^r%D}$hL(D%7O8d2-tmpU1ubBUfOl#}%Ey%XyIiR#JAg3_?AM$Z>75NtVJ}Aqz zhx`?kermRJ(`im7$V)*vE{_JK-89C_L20*`zKX11d>8!>@}x6e|7}2NmkY}E-9W}i zfztm(GB~f@Mqda@yXVOb8en@^#{y`>paQ&V@o<{Z}$B{G0d&tMg7sz$wC*+SLzKgEw z^(3-Ac^)~C97c{KOUe7lr^uJck3iX;`^jM3yY2e-{jopyN1^PG!Xs$czsmk%vRr>L zP{v~evJu&o`IhAApo|xvo*?tdi$Uq7 zH7LioEV2vPUD>br0#H6b8b}UN;{79e9XWxN>qF^J-lxm@z6tR|xDL1jtonXAcprN| z;dW&0LuIfCa)la_rq7i zrJozX>rj6>H~}mJZv-C)rN0&6E%3KNxsKTaO1mB8m!Q=9o$ke5z9w0hJcSIlYioMu z|D|3I^3wl5>j&%MT;ydrgZG((SwDDR7~FRa-rr5&^yaXhTW|Hui|}b3>zQ|zmB#`3 zT(_?QLA}OgCfSv|h#XCpkoS^H$u;DM_3sn4b*F_MZ;QdGdbpSy0-oAvc3k z?<4SmD(hzl{1L35-Q>@t&6RlNdTb0|gnG@$R%C{Ec1hec9Pk^$2F9A1Z2K8`Vt8PsNxB~TE ze>lFvSA`xipTifyzX#V~Js(s?%x`qBi`x#TfwI2O0@vaA*%R#3p^g~@_J9vl;{2d_ z9H&63Kang}M$Fyd+nC;ciizuV#KKxC!lo`I6)1`|#<^&jL4Ny_JI>W4;~&KLrfnUlZ~)B|gsq zzruW;1%6v)9G?q+ALB1yiO-Y3A5rh0>*G?yW&MsKZw6&~?xH_TE+bzDrJs+;Z$Vkl z$H{u zcmn#}3f2erfG2@Jfja#>w>)ltu;xVgBbZJu*c9~&z!u;XP~JB`O>PD=vMP_GavrRa zHL&;>#D%|;w!8DF^6&NAqTUIJ3*`ybw*TnwFYFicoid^PP8y*+*DK5>^GJD`_OB}Y zm7GVKqTgYN3k%87N^5Qg<#WR_P}cWSP}aw4@@?>R^83UK`?#6vEPX0gkE7a?bcEw;@@Lo{*eUj~8rf(#-Gro`h2iOJk zd7R#`r)%GybmP&Q?(}}3jQgRWEZ=0h8)p$y#(Ix|vi)BKGcevZ(?0_bVSoG%JYSX< zl>QpzyLc%tu_>JpsQO{vvoi_y#x?(;I^P?eNjs z51uz7pC$K#vR#sUx#evK9>V+vItP@)$u$h;~^;HB%d5l2G{Sw^2|b9#>K0k zT+i=dy+6nXyAB-qBz&%GIO4MWSA#NtZoVQW7)O&3m;LK@P`0-l zkJbd^Gnjs`KL*pi7xiVn7L%`m3((JIP^Pn;+ylyT{sKzqgC5kc3*Un6+)m^8 zJdV5ol<5`FuOhD}OUe7d&oMtwf>M7q`3Cq6;_s4Mzyt7)Ksg`oCch?s0)IzdmRH7! z>=(jdx&L35_ZZGg$^B(KR-P|v!Xupq7S|`6l5Lcg^|IkH)aycaRpL1lP{#SiO1w`4 zYj?^tlR$Z{Y&!FE$pz$7)QVGVdNO_MAW;1UP8_!?&SP(47B?neHZyH`7`MabnR-84ag?s>7cyd%2dbatf1@{7b+v>O0W&46PzF1 zc`<6nBc9pGnp;7cFITVfI66n$<8xK8^19XXeOE`m-{@$TAm0P;=T~Vz9nBh0j?ePE z>sgrYn;MUpcSv`9h?vb(mw_hXy=aGmE&wD+kZiR&HBN(+0Xbv@;CB0 z*ag=s)h?0a7q&l`*Qujv0KS>kuecc~&l{dfo`HYP6uB{{X5CIptQfA`N!!?)cX~?<3Yb-x8J+<-V5t5*gs!nyS1P! z|3-4V#__o`DAzT=t4GXnun&$m(Lu652HUk?aZR|ar&O{Dc^a8XCddoOOUaSS{>2l? zTgcm$9nCE8&8&{*KJsDm334g9LK!iykgt-jlW&l3k#8%b<{j|uj#2YpQm$v@JQbX; zg7efT$V>bOQqF&MWqn*K@yhd76Zin^Kf(1za2{%bxLhx|_dk{2V`zuCoNt2n+2_G! zKfQn)0?PHmb>z+DOw!%&!1If6xej|0l=IqhP|ka6$hSZ_k8KC#{m(a`oG*U|WqP$P zbM5MZLoxsEdbNLXaQt`2`HrR;^0GZL$@9p*U<4-;+ndxA47^>O)+< zAt?KCQ?flM`LjW}58oY}g7J9)SON|J<@+r|z(eT&8X?-d`5ah$gZjYYQqmm{2l9A` z@uA0yfyM4PII!3q2L~3rz)j#I;BIgccoel%+y{P(ety^bmHo#qcky~4#%;L#^8L#Lm~Z+1rLZaT z!q#M4CGI;bTbms4F#7Kb9;veYa=g9{$IotvUsvTmmc%D8F1!%=3IE97i1+}+h0$f)VfNytfZ9V|s%xZ0R(7z-P zD5EB6gfR{MsA&kEjCQAkQojq>2;03c<0C-n?*>r%pAAaAdE_JHV)8X|6Id7hyZ1d& zvrFUn{$Fd?elPu7P>$mV=!d~yvHt&X499PzQ0}*2e{mXQF@i-K$ zr=f^1L|nLpTu!bfH<14$x0AccugQbt5%L%**LBjrYzJYrD;?{SvR#H&IX*T&+wDK58a7kFBClVg4<8 z%a+pL2J<;Q4|{#C`V8|8eR3e!bCEwnKN+5Y zC!MDCV{PDhel=5{eil3*-k#n|^D!e&JgjHN+i5<< zq|>`8;+LwuV?nr+re}F`sN6|kLK%}T4%WVE7E)eBTrY${FZ9I zfoV@K*L*{hpfA-t{_X*N9X!uJ(G=1@hUdeJ>H9UGX6DfCHIh#=OX&^adHzXeEj>f? zCzp^Vih$q-VnO;f3^z;JNq;)EN5Z@C1CadaysHo0e^5dL^a^`HTE? zv#_1>sqjI5Gb2wCOTJv&w=nC}=bGo?!~K?~ex~H-nz!Mj{8LR|`WNu){L@Ss{V(-4 zrcDP|zvWoTpNUUC#Y@aN@JW6Le1paLaQIZeBc8l)eiM8e9(iq?<$NZ5rr*g7P@fTd z2tM2QO#%HWco}>Y{RPcun<=%Pk6xhp zF6JoxYR#W*GJMzH1kIm~F(4jn?{iFXws?uT75N4JIi`J%_zWI@6Q-N`JTnLRC;Wst zjCb+)eyVv8{*2$%j5)h%{XDbKd2AW-xqhBLbtTLHGJP5I@6cD%Kc=r!59Tk=^gT!W zkL^W%Ir=bT#ZhZ7G zF}fQcJxqPNJ0JEmjp^=u*wbXt8)5ph{d^Nw52l}Q2Apf~?_QIM{2JuVdE#?TSNPk0 zFSFV?&v(6z=_dIZu|CM>`n^qzekD8suTP(<`3p>A`dym8z+})L(foy`J$;$xFEqLI zH#FbJbfbTu`97wvdNBXERfB)UW^TF#H~W>I3g6}rl(gnsg1h`drd->Xm@m;^o`0F? z)?I3pm;>-Qyj(pPZ@9Il|28E5iTszyFZ^4+z^tp{`~3p5#d*vtlJzshw8SQq^_2=w zz$eq2!e{$K^(h(2w}qF%XEC3v`C+Dv-dpp-%tCqrJl7v?7Sl(=6Y!Pv$(q01tff!a z{N<*Cevjs_Fk9%4X#NVblfD$5=Z`RZ>1*Km@Pq2X`Y$vI3>jH}8!D_(j28n#^GIL_El!k`L2FVc=ha2rh=XhpXOg}QZeMD|1;D_n>q9>c*E># zOhzArKqIi4qo;NOPiN0D!lUFpl=8QBwE z8u_>2xqhYZgU74*QPxjr|8D%WL;Jq!Ut8Dz4Q4pq^*_lJ(Ov(O%sRU3f0Efscl}Q? z`_zN+Imxu_C$l&smNH)EZ<1+4Zvs!i2hiKVm-~}V0lgD^6?~L>Y3yuxr|cU|5xoyQ z&%e>kQlA?e0zV7+GPb_~o`5f)mr8y3V!9iDH<@L0H~wxiYw2$M-E7v=-T1rNY*8^sZ?^%mb!ntl*| zBl7a8g^ai81j*lRHqjfw6Y%Zo!SP{^k-NgE&-2?Hv&lKz&oNu+uKgUdo8Aun<@s~X zKJ}o#xu$4X)&Az1$yGdo`lVI8IQt$m$9XJA`ztf$^n7@}UuG83-SxqJW-0wrU7q{Q zYW3j#?>w{4<#l;xX3sO*)#sWkQ2(y%ax-DL%->uyUVXkvy95r6NB@syKZf5;TIq|k7nuy)eW={t zPnq`W_r{K*{`1*SnKJcxMt(E>3iuYf{9gK(vzM6tRs8krXH3zR(%(FD4)PV*&zV{3 z_o{Ese%?&D%H{hYzcu>>Q>H!_e^m}o*=H{|i`4HmW3>GWv$Bfs&R%KuRq=h;&|e>wjkU(ocpb;0NiaYW{O`gx*Q>pPMASkCyi5X@0k< zMenEi-6l=F#0-N^^S>}f>g$cW|FXv{aBjyS-#5O;ETvCT|I%z?eRn@+ugM)P(_3%u z(*F0F-p*qW!3V~_GJWZ9&z>g$c0 z{*R`R?&kl1S-|=apnhTeC$otDG<-CC8SA_4@v}MXocGs$HbnWfITz5X&QnRnakFSDL`w?7>-70kQ+^_W?N_dzn=-S#?$N5ge` z9KXj*%z5mv&d)I;?|Wo^A~(wX95apSb>X@Ganq9CO7q7}dwN{+#>VMAG;eG-`asQF zEANY>|KXaqc943oyq=ZoP#J%2|MzS$^KN-PJBN9{j{|_-sGP?xfFxm%-&aU*>-SyfB_@57L*xN5kd1zjFFf zyA{{1@_uMN@^L?E_v5-ud<%Uc&a>h>=u>dsK>yr6QCsRfwhwvSpSH8;2jRFsZOiFL zG+)gwq}RJi^4088dOLWYAG0gzJ>dE9b@btI+@H1;^actX_Cr};uW0`@Y<>FwwEr45T|L!^-|7nOfkyA_s{J*dQ;TT^;2vG{S0lNVz<)c@I3znyOZ7xo)6zg@2l-o?LqobZJ%n7 z(#L4~IyMR0S*AZ(+t;zF^ipkK*QU|$)%JC5OZsD)uV>rPU(|d(EBmb+AKd;^-^w@= zcjuS-b`aJ}<#=ppWt`x8i`P>Pt&9_yAGdxR+9`Cmej8dDCz7ww>7Qul&_C4apJ*4* zKhu1gT}1y@^J#V&{V*K&&+Te@WQwf6lk9r+VE!A~HmFxQzm05`dN99@>~QsY_*JK1@xE%Ld36I)C_8=ipAqPx#;n%XkD`~0S)7|Gcr`VNr_xVh^T}yYL&!pQ5y8C>lncYHnpU*V2JL&H8ndWvc-F-gO+#aO6 z&tF>DBXsxqOADKXx7@OQ+~+SXZ7sU{{H3K$qr1;vTG@2E`~0PqZ9{jTzqGblbocp7 zYn!0E&tFcpJ?ZZAms9Nky4(LUYysWve;IZZ-JS1Gvqf}wzCX=Qq4&Y|nC+i#OX*j_ z%iwe9Q{jd2Gi*7%96lPph`s`z@3*nb=Jz%y001B5vC|=k;oi&7!;WLynEp8=}8w;yE_K_U`zWV;88; zh_yq$Fn*R@MDGG04PQp@t@&KLnm$DHxpuvJaJ|ySZgS4!X%`#&kIZkdzPs2|=do+i zKG*MJ)9A(U1iU5v9?hR^+t44={Mj~6Uk)#fpJNmB*Wjb!z3Fa$O4tE(w?8H9aQa5n z&-2fm_q zL9Yi-z_aLXdfjX;-A%8X?WrD2FVFUM>dZ1+G5N_jz^^+q>gMo?S|J$BR6>n(nrD zce{@6ws&{CiQW?PH{0)Fx6(Vp%iz1|T{Yj+?xXk9d{29rJ{+Fs=i8(73GjS)>`qxf z!SeL7sm{4Pz3eRYV7&CQW&iNK>eFH~(O<6L%O0UWOs_Rd`kxkCMNg-1qG!?F_~>PO z(%tsyWeezgS-*%b<6ov%N|*5`UQYiF^=HEu(rXvX`s!tu(p$i1`{&!0^c>BfZ`aZL zXuh|tppVdeZ@ZQ5j!zfZopg76y1?#J4~{<<+9Py#{JGG^XB+$ru0Q+O1l?VK_OZR` zlQ6wpzpouYp8-$6OX>H)^Zb5x7X1-;KD?an#>+)^A>ECai|kUm8!!FsO1c{_{p~vX z67)CQA7Cr!tKenut@O9xh4G8+PWpEEX!t(*5Aa-npgl5}S0F!M_)-3Vfcb^=k@Ur7E?*S1zt8yu`mFn% z-$GwH-}&wE^Roxz>kiHzfUk}Zu|;$_@2-yzv+Jt(d-36RKYb=F)G0kB0Z8%l*j@;#b?gbh$sd4PKxg93My9Lgzd_j<%azeID=SZ^FpG4ax5F zo=U$1)0^gB6XqX;pPxO(Zq@c*TUY;DyOZwfUuXByUHu|^@Nd%_Z;v?V{nzof*26OW z88LVNb-W!;e+JY0Ab!0aMSmN<4L*VXIXuswV5iW3gXhC%(&c_hoL=iB9(DQaHGiWmrx$DfMmza2mzV3< z^RsWVxsQtn`^ywNM}3}=rzI=0r`WYsd{=xb{ydtt*YWXn{8pRgTuGGlg=Nd7t)`k@1s8hUxoTJ7rXlMl;$e`He0Sf*DOc=Kzy1#q8>ayREl52B=u*+ z-19@Fwif+W)X(*&+xqm4@B}=az70OxpJ6l9gY`SZu6{=Q-1*Y3i?rCNE@_+EN@&EH}7)AKZchdn~? z56}1Sv`NqD^h}}FpJkJtubls5@maR9^Vl@xm-~0wmh}7RZK~8yAfM~pEJQvj=WaWP z^`F-M=h$-k8hFB=V;9loe%=T1xppaC?&ocTucmL(_V?Iz^gY`C9=nNt1fJ*LYq!#C zPM77o*Y2h_h3ENYb{{=U^JVrhy_e?ivq$MeHGiLtEpyvryyoZGRCeIq+Qn ze%q4%FgyWoPhYC-%Wa&#THBY~ZuIqECGn0Xv3%So06s z33P9UEdPVHn4SvH^B35e^piBdz?RXQYyKg-fZj&)581_ZU-J*!W%P43|FB(4@2&Yq z?0Whont#M@p3OU?GgHu@Lc~f{Q5(;J=Ve#@LKc_ zH2=7*Pv58c$89?O7(CBkWHab>@V+%4o<(m7&-0(Kx%6DkKVf^)2Wb9D+n0W|=AX0$ z^jqK`#GkT-^t<8P;6?O>nqO=u(^qJIu`Q*$`;SZPEV{e@xWtyzH=ur=|Fm65-ww}* zFQtE{`Dg4(`Z3KvW7pB^;(c$PztmRHo5Ss@2 zbM`3ReO~muO2g2jgZK-!4PEZnY=g(ugU?5n z+XDJqS4n@%ZSNOldcpmN<#saNeIBvg&UDWE)hp~Ay1QS!!j`EA=dTrZk@MI*o!=F9 zC4I5Z?+Uw~{)+layOaK3^%w0C`e*Rv{wf<=<<`fK@Kx|M`Z0K(|B_9o*ZhxckC$v4 z_27K4+AeZ=o=;ZWbyYm!ueMvNc+H%b?QZ9KzqfAAD|VlHNlfnd)`TBc-(cjvMy|ic zCcPx{H!s!p^#bMV6WTHBbO10M~~pyz4+Rok8}_c2DtU$wdP5%669HQS9o z9-e^rrMu6^U$=wk?)vO?Tc{q~FJ5Q&(cR~x>umXIncv|0bDdqN9&EpLc5xL?`0MQI zDqb_^4ZGEOOzy{@lJlnBsa_J3`|+p2_c33B`OWj+vIpsMA0;1t)VX;CKFwcm7r!j? zGb8pKe766#T}FQmUIt%F|1Uh(-(c6%zlJB^Tj<9$|Bl^GuZQ~*3I83tmoE2Za{UUs zpPtZsg*`$ar1^L8YvtYiT&MYWZ7ua+JZ-e=)Jx3_cM#Wz?RY7c>2ID zr0+q0dH#oXG5sfaK71uzemz*Oztyg#*MukF74(MiJpUuRh29*V58p{|r|mzsd+FzC z`;YBG`XFt;%^sm&sqMGfq}OEmX2d4Im;2joEqW<@6+DgZu3tW}8FaZX^g;X+8&?m; z^CxyC-QC~(#1_9U{g;@hP(R^+YS*d<$Ge?&E8UI%owjtH)DOo0PCLuFamTxzb^-J5 z__oU~R1fx#U3Q&%@cws~-TIIGKG)v3?Z3-zdc)vfiCK>M>!0(PJ^ZHgb?}0m-FEj| z&bPux=j^d%Z;P)tZoKTZh3|;F=_mZJ?Sv{`Gv^yS(|JtR--Mifc8+>UOs@ZKg)d-! z5BksZzqO0#2jThfWzNkp_?qBGwl}>wJOLj> zmq+_@{U7XbdN>~{KM%^$LR=`X_Ze23jn z-vGz+9rg%)o3{T2zdp&W&+oPUFSZu_xaNPg_33qI%k+P>>2&x0_&1wDckhpXvsv_3 zsGsW}vAOgtcmm#&exBxkw|(iCYW{ayK)+7&f7n9$G|m5Ei|F@j{!crZ{*31Tw59ad zHGkC3qJN_KqqdxWNb`T$h4gB;FP8BCvP$8`$G_** zr@Qg*dFgcb{8_}ypu6YKB3>5VjqfBcm+r=QlGl^&#&@#Um+r=QvR6PKt?MW171D3j z^%M1q=rF3`yB<9VcyZa|Gubl3l->mK}q`T)gt9wi7Wtd*BU&C8T zUj$FU*U?vMzNS|}e@F8*y{&Zj{9P??C*3`NSIgT+-=Xbmdk5*?YWv#WQTh@1Y(K?I z+N|3rmUOpV52Sdh^i(*WH}KNvE#P?Gz-vjzqrb?fdTr?4HJ|FaM;K?s25P>JmtcOh z=IeO9>9=UUt~Y=_L-TdL;dFT(ZnCK3Js>_$= zwV{6pPxxtGoc_D!Px2D<+H)j-lGj^(y>aWSkvE3!mLINk1Z=H2s0Cwp7z?)jsWz1{R2w0|t8vA2)j6TTRJn0^WT`J5)+QTkQz z7vZsw-11L?ugz)drP6PMzX@+lpAE3HTOoTidg?yoqh{%GJTa!zlB#ye_it}y;<}M z&A0T*owNT|-a_Wz*ZQrz#q`g#ek*S!{d>)~_SVvW(R^#Kf_@ynHRn`s3;l$-`0a=A zo%B=Sd47hsmmY`b!w=H?!E^o7yd(6X@B}>RW8EH{-_yNX&STeT{nNer^c%GP>0Ua$ z1im%r3@?Lz2Yd%Si+(RW&u`=9(w~6ic?+*6-EE(?USGP~K5e}M=bWE*Ug1A{v3hVk zXy3+T!BNWPO-NIwCN=P|q@`bnDiy~*@+&HG*{{Zu%f$M9y+GvRn1 z!z-t|^K0B&NO$MgxVMykmbTCFR?>TD`y6i_eH!*xe81JJpwETl`>ozqdVkc<_0RHl z(ucqk@O|{FG@t7oq+hT3T<<8|J+IfrOWNVKPaX7+=PSHax;ww0?WNJ(`TcCKCH+?P zm*=13wV}^~=fmA2`ZHqlHJ|Vj%rDV=!s|_c8J_E(>kXh+z!UJ{^lh5&>W!j*rTMPj z1ohy2cAht7r_6tFy?371W|uneH=XBYIp_7=d0sc>-TSj{UQhMl`vTp(zH~R9yLkn4 z8BeqQJg<=MKCj60is){8b@wLI-S+D4mC_Gk{`34E-Yoi`@O*eVJ#w$)dwL7$b>R8% zrSwxYpYN@tx6ypQw~p?v*Lrysba%bh%iBuN(e~$iJL!4a{(NsAy`ScLdk5)*HQ(Di zN*}5D3%sPyWc|*F6>0tgFO`0?<}dWp=+iZSq1Tdrx90nJZRiU$-^Yv7muSAPm!PlI zd|$6O-5oFbc?0O~c+t-rPIu$^B5xGkjpvKJ3G~;|U#{QZn?kREC*U*b?`wX5H;2AM z^8>sEba#Be*jq$*$M=i9W%Mt#{XlOu{ReG7&|6RcP4ky{o9JGdO#c#ZJH0j>&lh;R z>5Vl%$lFgpL-Ut8Puman8q+V)_Jh3)`qi2* z@Y>U-XuiP9rQfajAznB7qnaP$^`)=S{7`QYeS_wQdWG~)G(XH6L;pte!@SA#Uo=15 zE2byiC+lapH%mSEzRl%cnfiKuzvc>WJ>7l(V}ysV+~8kuJSg<0(B1WFp*PDpuUAKU zbLj4Rb);8Dch{>Uy#;LV?st#$D(LQdd89YxOE*1tJ$aS4MLoElyvmcTeEwb!%kxD} zr8k9tmvgnZUF&~sUH%#`eXrD?YfeS}V9r=?q59X>)xXwTsvh)TuXT;oi zDe}_k9nfE{U*u)b&!xAg_o3q}N7}xCo}gb#?@6Cb?@PapK8ik{UZEabe-wE|-?;vt zM7}UyDSaZmUgxP^tsf+h?~}uucP{n{)l1BS@Y6b%c(drw!`pYh&CB@F_4fum zxAP3INPR}^LvjChZ!+^g!s~Uu-7Br)=XJir8*{+5{~h`Mo$vC@LFYBfjk%)pT(6AY z2%g*d9&ev|Fy8O=4myvumArqicZ8k`Pr#FYlKy7I&ewdISBpMW^JQKdeH?tbf1j65 zFNUvzw^1*R-L38Cd0F&Fwf#IVL0_Ty`@Np@jhesT8$ka;^W|Ov{jlcCy;17;enOGV z|9r2A?(Xl*_omR@@%#a=l{^IhZ;0gaBuko+ip4a;id-%#2{>_Nph&;Zp>m}&-X#I!1 z-t=cQ|A;q${<7vD@rKji(fmSh6n%&07kU%uKfv?+N4+WZzu@`sne^HZ$n+oc=Fm@s zJwpYqbxgXu5!Dvnm2{$g*-KYX|I z*c!B-?JxHB(LaQj!4K2Fhv)fAyrcBv@O*geFE@Yn9{fM--4A>nbJ{ol$@$J-f}k`A zLW8hDteBiOC+SI>w22m>MXWAC5S1k;LW4vi(n?7+y4ebXBB~{#Dky@eRBHu6mLMt! zf}k`Ao@=h_GkThPd*j}H?&tTrpGRLW^Ln4p_uu?Eb7tnunRxr(Wq}xYKk{sH985pu zruz2<65vVXTyipe6z9tWLAZeP<$)~tBFsHP4lKvZ zj|K8zIbMD&Fbj5W@y7pnpb*}RoJ%fY``6P~2g<~_Ubs5Y2g~u&>OgFi_Z6o9d)d{2 zI9QIiRtMtQ)%M}k{sMP(AP8rZ>&esM6FGk(PzayH`4fRsSgr>=8K{Hhdcc!`rSM!T zpY1*sSPm~B=aSpl9nNi>w+B{>F+c5rK{3uJ+XEYs%lx$m)(5=#^FN>31AXic+i$Ob zR6Z!SAE5qT2~vCT3|KtLJpJ1z;AQ@Odt;ag7aqriEMv=@@yc9 zU8P0cN>6K@@NA$>jQv|jpnZ$-9f1)&a(+4jx*_A)gX)LejzBb=Kn{~*;e$DUE)dW5 z`+qJFk{ta#7sx~TG%8;hd@hg=A4#4~E`(1aFL2icis3WJ_2g36yI*C2`+T4r_U>1y zC)=is*ZaP%-t7#GfxYkZ7PxBzaqKGlFVx<(6V?V2#2DY&KoZ*@|JuMxssBs9T5MNR z{lehdKnJ{tJe%AF-^uw4fqr-e=Pv|?;YT=M7l^iGyw7sJE)dW5`(Gc3vm@8X`aqJ{ zeu>I2n6N&OBGx`3|Bajl|3JQN!i#}kIQlkCyJy1R1G>ZQY5R~LnD9~{3!Xr3o6r@g zVpnK+z+iwSQ9n&Bs@{P2X{Krg(Gtfjpj=!d)6?*so#aUklPU24;!vODI1l?b|>hTtWVB{I`K(v9^dDbiWPE z+|jFFp*6uv;Z|~(@^1JE&Nl_7?R{$o)<|jOEc7nTh}4wyFibkKF2RtJ>gp@+xu%yTX2r z{9szN%8B#Fe#&dyXtjPXFQ?}z(*GYx+fEI^Ur=7@?x2$QmgO-_4!JQZ z2yc6*H~%rJSd97IQPq!fG(UN7q2}#Rj&^rcee7y1j~$i1kF+n#V@H($OMg4599a6> zNfp5||2wN%Smu9c)dKHD<9jM?jOv3AVeg`1_T~QY{I6YAJe)~+(A`xf!_&!Y(sot( z>(b_E0I~y!QR?U*psqcC}qkdB}}ZW$X(39&(sm zgZweh_f&PrU*vpG)rh>0^S#tkiU>KTd_&721_lKj@BA&1`@B z8>hOF%lBpD)bRdW)*o{K+m`czaVid$^MP?H5ne>?huj2}4Bt%-lSA;M){vRM)(bK z_xOXnc)5SSaFR-4FSfs=`d_6b zsSx|as2|DSr5&uM!#m#XZ9fOAHaMPK=^mmw#ZD4AMB zIW1X@h@)n6`D7J+s5ib3qb?&)OFc}*v8(NC$P3)VRRVkixt^R1|BX!F&no&!Kh>|c z?#s+s>9AaXJwoNfk8*ui6~NDOeODF3FOnCysj3X_A-9riVefZKA#y#e z?(zChRZVPveFar{vNt|@o&e=WD5%=uJ5#;mxu7aOjK`1l7gV+E#oEDCep@Qv4X2WK z4vkmEe~c`jrsjz4nUpVZ(^MIJD!HCq1J5Rh+>ok+FCmA?jquf+r>mv#O`NBz7Py(5 z?PjPp_z`k0xdUE9rr*n}_3+=x^m}>L1AE6CAvdi0VDES%Ode$W%PUjOabfmEI&sB}^Ddbl7NVOEch`fs20(-xM%XX)! zHu!d~KTUPOkCEwl!)iVJcQQS1SoOg2`?sUiFf6}+J4z*t=jCT_;P#JJN$@w^{?Upa z;lTTgchC0vKSr&eAm#0Q#oh3(mwD|UucpIe$o1rW zIDt&R?^XrycryLITNT4cbNv(59Jqk%pQy^=3pvkMHE?V^`=_fsxQ^RDUCmk_6${)?v2;KpW_Tw6K3Oj+Zn1(tjYJQXQ&QX zo@aH2>Sz1c-_B4QM|nhSH_>=P?iotYl;zVxrr(3B7})!rXvi&8v2YjXg(?B=OU`!BQek-e<>DN8FE|fAh#aEw`S2lgqpQFmBNck1)*{TU%%|2I^O_h8tyI2+F$aw!j4!M6(CGeNzFu4qNR(Q+f zJXHnnMb0JH!H1Ju-SbrgJdM1Hyc9lz^AfcjzMS(C)dnvlhujNPJG`76Ca;H|=lnv| z4S&G-g{lu$_j~k?g>G$g@8NQSAOH~kF!}+Bu3;vi)zfV`w;qS=wd~B5uI}dpIWvT$)pS+4(3{U3# zay17&iSx@%VziEz||-uNq31H31>)vZ!Z@Db!yDV&=DsjhOXRX2PS=hv!Ucp2x{ssZ>Za<*HehTu2Ix#SV}3$8y;>Ce~d=(2H%lUkj0AIxUbt(y-$N6 z0{CY#{oY*_!DAls#&^A%10P7H=XI+xcs!Y&*R5*cT+ZuM9efh!b*ho=@9!3>Hg<(} z4(0TGZZ&W$FAp5gFI0WU$?}o!zbS|1``?9XBYY|MccU7C8@Rt4l|4i1Kg#VdQZev4 zGW|YY#ldfLUau11O`O-OWO&Dw-uP}(K{%0|OU{BPa=utihx0jKtn%RtIRC3Efah`k zS5*w(&Uu5H1GjMApvvK=IRBfffx9^Wo2rN3<9vx~gumi^iE4(`!`}FBRxR+3Wcq!) zS`F{bd86uplR0lxU2rDnx2PWY1kP_!{qVV*->L@TYR+#}!|?5#-$s8{RMy91oZqIR z;a53tQe)uvId4+&@YkH*t`gzxR(a#UU8TSWkh9%8R0y8P`5h_;K9Tb~RUZ5o&hJ#S z;7ZPyszSJt^QEc;ewg#SR4Lra`CY0Cev9+FRW1Aj=Xa|Hc#lWC@!g}E;2=4dyc|BB z^JcXYzL4`~)ehgl`Ms(WUds8ssvCZk^JS_ReueX8Y5@L}^ZV2gY_xg(-={|4y~*_Z zc%>gN>nDx#Cu2SpaY|dAy9{A6kKcf2JV$L5?gYaC= z+tfz*dd}OFc7iOw+c|$!+3-W0KdQ#S&vE{kii6+a{4tdXf6V#gDj7B&_s0LY3c>r3 zv)$Dy3m(t;YLy2c%lQ*3A3mG&CsZMP1?NwyVz`0xCsir@5a&;+a`;8gpHj8(N1V5- zdiY1q+f@_1^J;H=PpfA5Kyog5B|MSyXVhwV8t2cbPWVL5pH*FOG3U>!Ubuqu4%H9; zmGcfY1h3%yIW-JF&G~amKT+1tYn-o9(eNjnuTin^&zwK6;^Cd1@W%hVN`m(zXS=9h!{nLp&78lWX2Huie?b+o{m>@c8k0 zyG|9ICi5fb2kTS`EawO7R2kcEZ@p?@)AORaz4fZ<^vL$st6DMc-&(KgVfp-CubS9? zdoQXMHa&lo+j~*foDtdHi>gj+%lKYY4X}*wMYR-`^Rd6H<*=NO{av-O{r+B3-RuhO z8}9EVReYA$UxnuV&OPM5qVu!)`md=$_yx{iR~zBiIe%SgMZA3M4V=HBZ1_vg-%w*L3W*Q*A$sQ#3J<_@$n}(OguUlQ6$al?Bk%_DY_eU#?ep^n7P#-K7}$H>Ks`B* z?T@EVCC~B7S7`I7{Ubwts_J6#jqDAoh3zk|e$_V0?P7Z=m8bhNR44opneNX}-S9J< zf1rBd*E#<{4Zw2!>q9jJ%k{4h)d(!t&puN6B{F|<{p=$Z!}hOl4X7G+h4u-xA9M#) z!=+w%|9aSfS_#YV6$Vs?7}vu-R_kH;{nE#(3+3f{*vD#!?Jtjym0l|2k^5mjR?)EB z5A(5#g@<|mKT+{;bi22FK2b?SsRz54$4@vZ7%``gF2Y9lP$$G1wK>y`KK$K0giVA(!4sdzE|9@r)o zW>@0#c9Y6sFR~kGJb9r_st#UGK7rf-w~=R&o8S)e1F6HR4gLu3f`5Pq;BB6f@>*FG zjdziq1joWh!^vgP(?*;aA~y_+z*S{vIBJqo0-W+VsdoZ@l}# z@$f|0d!(Y59}nljXTe2qDO?WEha2E~;THH&xD$RJ?t|ZghvCoQm@8#GQ5`b=M0i&? z3@5?)a5`K99|PCG=fF*HIot*>hP&YV-~sq)Si4Hb_Zl1ve*!1N+H*4g9C#-c@is{2CmDKZo;RdyS002#$x#;WW4bo({Lb zr^B7_T(}Rehlk;N;Ft;-Zzr4xzXgZkjc`7^&GRze68HeP2A&Ex!KcG*a5>xs-whAI zYhdka8Q*(wEc_#!4DZp2^$(AS3*eLBQuq?M4!#a_H! zN*V7ya6Fs|(<4p!{eJ?S2Yb)K@!BtfuZPRw6>tOm9NYrG19!rk;6B)TLE0aNmQdKoFetDkzPpTG{=ehr+ z8sO`w{sQ-B)dc^QTu)vO-^uf*=_}zD?oZR(;U_qc(mUaGoJZ;1@IN>Y=)Ld;&I9@Y z{3+*3A7cCCS9(mXH~xj%50rnNs&sn9E_?eIHSLU$uFr|^?2xI~N4O+p>7DEf&3jJJ zr6F6NetqQnaP%g&e?Oh0k4SE(Qu$Wb(e)c-`Q?yTkz?SKINwH(h0o-C8$AKOfb(tj zB=`!>x7CC2JkF!_FuaKKXni`{pa1Rj26lyZ3zy$cUoYixJh+|SJ*vERql{PX2i{J% zVY&ZxJADi+&jZ%jd5JL|L9{`-Ar zy>?VSFe;C&-?Dx&dvpCdG5((9&UynZ&$HTDUkdAg_s%cI=*!`K$hqV;Se_TPi{1{) z^P+aq*TWf9zSZ4T?}m>duOj!s1)Pu72jCLU$Lbs5a&opCtB=4pl5@%SO|t%%a=x1$ z13$?5Zh9R2B!t>W|^ zw!b~c>Dpph{_=bLI6anKq4iMzL3b}b>91byzaRI~!)$;3?4_5m{q?_>-oak1^;7*T zLwo7n5&k?iUbh>(_7-bjQ{I`nkDdr?FL~ZquZMSFkJDrRCgpc0SA`PvLbl)k{(8CC zKA7^5yT4uor<23vdiW^L56~Oo(>OmsZ-(XhA&GhmEYA-~)K|muJir6>4p^QCc%a?| zd(X?HzYnGNvHj(HkUqG?8-Im%G4*#{=pen}X3zfklk|46eJ$m!Zj#;!FC?!bcf&Vx zez4vP-^KaC`T+a@=ZEM+@S~g`qL09`{U569joiO2+y9|@4E!vW54p*DEc_}tOiqB` z=ln1|2!F}>VR{+-Gv|NQ>)`FWy!wCCyWu^^A@^{706viO!}Z`TvV2m>HExQ&96ph} zD0GA#cdLxIkn4}vli`cG{&;;3T+R6ey#}u5e1hJ<_SZ+6o^+e^cL(L!ZkpcEBwhuV z-7a2B4pP1neg__gzlNvZA?59tz4e!-m%<5TuYNBaLLPmmls^ehX4CJ($@DxTJqTBm z>3KwY7QBe_kUky0jq{M658p@5cGL9&_&IVexfqt~u^IXtSgyxr=;g3nkDaL3z;Zoy zqFxWb%l(J-M)(u%Kdd*i{pFjfmo4??uUh+t@>@cgdYc%ZADMdlD0hl&xt}>x?}Fui z=1jd8miv__>HVou!X~cO!?% z@oay7vh|+3xjx39t@nvB-fVpUmhoom8(|sm6nzAi@lMh0dt^K^-l=*FEXRve^*A_z z#zW6D(i7lA$@DxUJsFny%hl--?cVan{N?Idu*~0)`gB<4??^qL?a$vdJ?`Gf`J1Mv zh_OGNriWnJpH95_f1FXg|OUze3V`S&*b?#S}%nQc>a#o zt6+Iv(J^{0EYB-CMsI-SdS#y81k3fxJbgJV+w-ydN?5k%WA%2pnEDI3$LXE$TymJ) z4bSKC&Cq+{J9vCE^a1!W&S&aF@Qa+!)JI_Xyf|Lhm&y8(&x_;r7`DIsPtar86b3!YjW}` zpY>Vr1Tx+4suzi|eV?LJ=bFOj)14vTYod;!^7n=ckgth4f&5_T^j~~B`Ki!Zqw1gi zOYYS#Og-lpd-d0ZX8&TZ{>!1ijH-X$FS+zz@{6Va3xBcnf61u&m;RD__1_F#_KUsx z8$xq`u~+}|&{cW~FAx9vZn<6>;Z31(y-t>w?L7yx)xBD8fUh9a-)qsA!q=034pr*Q z;oHbYdZpe5w~({lD!m_yrsRDQSg>-0kS zO!7YI3-nreW&!=4HN8%6hR>z^@brax4}1wZDg7pWBirw9u|C4~`&+ErE#CYr(!A%2 zPE2pmlVPv@Bhzozr^8-*N2WLGMQmyB*z{ZUh6o4UJM^XqZwlR^FGqcEe6!M*>iw|S zeo=a}u01IId+isc->b*5tL^K^t?n{Cfi3-=pT10=72!>x7QHCKLAO;eVK34ess7yb zR=pX1fc>!E2S3Gb(_>qu|Ch5uCn_|N8W&^aeKl{*2t}KC3su(XV;i z%d>hjTb6H-+=BY?lrKqtRv$q51IYKJuhEBKIX+pVYY*}Eh5f}E-HtH*Jpg@7gv(Q( z*W=mp_mWqnuho+x@}Rp;Pl<4t9FA~#>Uw?p7WMzG=SSp0w@WXGaF|>i;qug%_0lcs zzp7V7EwoU)PsLczCObAe0;qp!sV&G zdjA&n-_eKI&u%NHWIR zx&Qa7KhR^?{_o*F(2Lm52Bf_Y^jda>b`-VuT>6K4@L{ig$)8OBNY7#W=Pv_#9@}qk zK<|hsU*itwS*yJI(%uW{AL}D*zr9cN!bc*vr%&}__OsibNd1T1PxaU~FZbIY)Jxg^ zcn9?=wzOa4ex`RvxBrzM^SD>O(w6qW(xab$BRu+;9H}y)eRI_XoWs!gr*O=nZ1~Vd^jB{-m#j*O9~IA^1IVw)?X_4F5pR zCF@Un{VmjXdEL7npc(nkiVq`yksf7qu8$mV!02aJXtMnV4E;qfuh257{I}`KNMu)Q z$B=(aH;j}BTN$R2^LHul)qgVGHj3EQ+R0RYrwqsFi*S6#HpcpwxIFfM(MC6Wk#-K1 zKQtrSNb2(PMf`l=BQmx#f)Ng7>|pe;WqD?0>||_YuhB}W{okPYa7W$8G9MA z?1kEPZ+Po>ZzCW1zU+9To4rVzNIpMfA7d!Omu8GJv{z;M9!vQZ83{%Vd=|MfV?QGq zzLb1j#sNkVd<{F%(7UC61N%T@411y0M7}ZOAfp`l1LP$cNk++QQvM0@9T|rh8{sbU zeHn)u-93`OM{dnXHuC@Bd9n5t`O%ESj5c`t9`E~*6r+c|NZX&>k&$B5zv-1^}t z{p~Gew865yO*Hz%*xn`?L$GXblMJob>wk?c+uI}~My$#9Hpz(hbKc%28OiKwyNt&( z$p|Bt?QfEi2g~+1$tVzOvi(gqN?_UkvW;@K9KXDgF~wNQUaT#o_TSFPF$Ulz}>_dB}osuA6|b)8(r{j@Ss>5N6wm;~1kp!fN6SW1x@w(^9DXjEobE zxc9^p$uSd8GHT%yI6v9wfX^id-BXM?8>GDV-0f)@rx=Cri|Sale&Us{ z&~_)MOe{58*z*1zKk+i7i|sG(G9&9#UVhl$UuATOu{^IfdSO|fR~vn>EYGWrLDZM` z-_^#DpY!s(+8Bmqd0uU3gWR7j%kyf(7UTU^X~e*?Jg+fg*_HNwG(XvHwUHv$vdKYm z4!hc(L8kLjqv~_7{YBa=@|20yM(sD^bI4x=Ym7#=e>^eIu)p>4YWr%+yT{KnV&O&P zH_0XJ3j6ODdhgHqMmh4OTz|v>YKl${DHySbUYH|@d z7G6g_kDSb|wBIDt^I44`aykE6WQ1Y4KD@~2g9oX6VrsoH0FRKfGU|<(@1+0eUT^$2 z8L{v?b2aVai8mSX@IjQ%B`3mi{p}_r8J6pBHyL3#MCGrVxY)>nkB0N$)49FHMm~H# zxrWLYz}JwkClA9nlW&^%S3~<=#@j-^ncN6JM_xO=!B`6Sz{}y!$SI))Bl-uaA9&l_ zUK)%s@c232{5KeJ@HUitPJ+Gbfp<)7Fj8RedSElT9+v(8-;5!)zyDui46`fkJ-NLl zhCL$v|AG9##3e=yJemA3ISxLVyn5ozMgn{u`5AIDTtTM4cWQ*-TghuDHX2#*gXEXU zd2k1pzr~mXzeCP-Z!wzTVe;z}Z#7!r-QJ<|3G!-q0{Qicw;3I99_P0iUGQ1t_a-(O zJ#Yp2BXU2Ru4j-3Qg1hsfAr>mk+zKd`NTVo5WJfF&BUcf4!n;1&#ur;ywDkMLpPHlq)Imf8!u zj~PP|o|gK!F%seN;U|nS0dKt3npZwE{FIRx;UmM(7%A`@lph!FFiIkvPp)TIY8xoe z4?kxlDz82HKJ(P@8lw>YhR4%stb~m|Z+vSFO_%ax*e@7K?1kF?nyyTT3d zLF6#yWigV6$@9W*867){kKyv~7&SY2UZ|ZyUKD=ESiPHPI-evrhWm^WHa%aQ{6%ns z5xu+Qb>zFk?;CCK5^_<-2gU$

    `knf$)b$;T}@{1@fx!M@9#`QtKl>8UDn`-_y%0 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 F_)iErWg7qh literal 0 HcmV?d00001 diff --git a/BuildTools/BuildTools/MSBuild.Community.Tasks.targets b/BuildTools/BuildTools/MSBuild.Community.Tasks.targets new file mode 100644 index 00000000..a1c91893 --- /dev/null +++ b/BuildTools/BuildTools/MSBuild.Community.Tasks.targets @@ -0,0 +1,148 @@ + + + + + + $(MSBuildExtensionsPath)\MSBuildCommunityTasks + $(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SharpMap.Common.props b/SharpMap.Common.props index a42735bb..fc0b99f2 100644 --- a/SharpMap.Common.props +++ b/SharpMap.Common.props @@ -6,14 +6,50 @@ v4.0 + + true + 0 + + + + + ci.travis.$(TRAVIS_BUILD_NUMBER) + $(TRAVIS_BUILD_NUMBER) + + + + ci.teamcity.$(BUILD_NUMBER) + $(BUILD_NUMBER) + + + + ci.appveyor.$(APPVEYOR_BUILD_NUMBER) + $(APPVEYOR_BUILD_NUMBER) + + + + local + + + - 1 - 2 - 0 - 0 + + 1 + 2 + 0 + + $([System.DateTime]::UtcNow.Ticks) + $([System.DateTime]::op_Subtraction($([System.DateTime]::new($(SmBuildTimestamp)).Date),$([System.DateTime]::new(621355968000000000))).TotalDays.ToString("00000")) + + $([System.DateTime]::new($(SmBuildTimestamp)).TimeOfDay.TotalMinutes.ToString("0000")) + + $(SmMajor).$(SmMinor).$(SmPatch) + pre.$(SmDaysSinceEpoch)$(SmMinutesSinceStartOfUtcDay)+$(SmBuildMetadata) + + SharpMap - Team @@ -33,13 +70,27 @@ SharpMap.Logo.png LGPL-2.1-or-later true - $(Major).$(Minor).$(Build) - $(Major).$(Minor).$(Build).$(Revision) - $(Major).$(Minor).$(Build)-pre1 + $(SmMajor).$(SmMinor).0.0 + $(VersionPrefix).$(SmBuildNo) + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix)-$(VersionSuffix) + $(SolutionDir)SharpMap.Packages + + true + true + true + snupkg + + + + + + + diff --git a/SharpMap.Extensions/SharpMap.Extensions.csproj b/SharpMap.Extensions/SharpMap.Extensions.csproj index 114f44c9..82c21568 100644 --- a/SharpMap.Extensions/SharpMap.Extensions.csproj +++ b/SharpMap.Extensions/SharpMap.Extensions.csproj @@ -176,10 +176,10 @@ - + - + diff --git a/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj b/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj index fc4c6a3d..6968c678 100644 --- a/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj +++ b/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj @@ -1,5 +1,8 @@  + + + Debug AnyCPU @@ -105,11 +108,12 @@ - + {C83777FC-AABB-47D9-911F-D76255D4D541} diff --git a/SharpMap.targets b/SharpMap.targets index 32ff61d3..24db265f 100644 --- a/SharpMap.targets +++ b/SharpMap.targets @@ -22,22 +22,22 @@ + AssemblyInformationalVersion="$(InformationalVersion)" + AssemblyVersion="$(AssemblyVersion)" + AssemblyFileVersion="$(FileVersion)" /> - <_Parameter1>$(Major).$(Minor).$(Build) + <_Parameter1>$(AssemblyVersion) - <_Parameter1>$(Major).$(Minor).$(Build).$(Revision) + <_Parameter1>$(FileVersion) - <_Parameter1>$(Major).$(Minor).$(Build).9999999 + <_Parameter1>$(InformationalVersion) From b4cc831464019a24ddd77df4ca02ddcf89cc421d Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Tue, 14 Jan 2020 10:49:41 +0100 Subject: [PATCH 14/51] Create dotnetcore.yml Add ReleaseDotNet configuration --- .github/workflows/dotnetcore.yml | 39 +++++++++++++++++++++++++++ SharpMap.Common.props | 2 +- SharpMap.UI/SharpMap.UI.csproj | 2 ++ SharpMap.sln | 45 ++++++++++++++++++++++++++++++++ SharpMap.targets | 1 + 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/dotnetcore.yml diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml new file mode 100644 index 00000000..6b426242 --- /dev/null +++ b/.github/workflows/dotnetcore.yml @@ -0,0 +1,39 @@ +name: .NET Core + +on: [push] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.2.401 + - name: Setup Nuget + uses: nuget/setup-nuget@v1 + - name: Setup MSBuild.exe + uses: warrenbuckley/Setup-MSBuild@v1 + + - name: Install packages into packages folder 1/2 + run: nuget restore SharpMap.Extensions/packages.config -OutputDirectory packages + - name: Install packages into packages folder 2/2 + run: nuget restore SharpMap.SqlServerSpatialObjects/packages.config -OutputDirectory packages + - name: MSBuild SharedAssemblyVersion + run: msbuild SharpMap.targets /t:Version + + - name: Build with dotnet + env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + run: dotnet build SharpMap.sln --configuration ReleaseDotNet + + - name: Setup NUnit TestRunner + run: nuget install NUnit.Console -Version 3.10.0 -OutputDirectory testrunner + - name: Perform tests + run: ../../../../testrunner/NUnit.ConsoleRunner.3.10.0/tools/nunit3-console.exe UnitTests.dll + working-directory: + ./UnitTests/bin/Release/net472 + #shell: cmd diff --git a/SharpMap.Common.props b/SharpMap.Common.props index fc0b99f2..722a4664 100644 --- a/SharpMap.Common.props +++ b/SharpMap.Common.props @@ -45,7 +45,7 @@ $([System.DateTime]::new($(SmBuildTimestamp)).TimeOfDay.TotalMinutes.ToString("0000")) $(SmMajor).$(SmMinor).$(SmPatch) - pre.$(SmDaysSinceEpoch)$(SmMinutesSinceStartOfUtcDay)+$(SmBuildMetadata) + pre-$(SmDaysSinceEpoch)$(SmMinutesSinceStartOfUtcDay)+$(SmBuildMetadata) diff --git a/SharpMap.UI/SharpMap.UI.csproj b/SharpMap.UI/SharpMap.UI.csproj index 5dadc0bd..4f9652df 100644 --- a/SharpMap.UI/SharpMap.UI.csproj +++ b/SharpMap.UI/SharpMap.UI.csproj @@ -10,6 +10,7 @@ Copyright 2008-2019 SharpMap - Team sharpmap winforms This package contains UI Compontents for System.Windows.Forms. + true @@ -28,6 +29,7 @@ + diff --git a/SharpMap.sln b/SharpMap.sln index 3095d163..d8761ba7 100644 --- a/SharpMap.sln +++ b/SharpMap.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{A5E400B4-8360-4259-BDDD-292A134D14CC}" ProjectSection(SolutionItems) = preProject .travis.yml = .travis.yml + .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml SharpMap.Common.props = SharpMap.Common.props SharpMap.targets = SharpMap.targets EndProjectSection @@ -77,6 +78,7 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + ReleaseDotNet|Any CPU = ReleaseDotNet|Any CPU ReleaseLinux|Any CPU = ReleaseLinux|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution @@ -84,133 +86,176 @@ Global {C83777FC-AABB-47D9-911F-D76255D4D541}.Debug|Any CPU.Build.0 = Debug|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.Release|Any CPU.ActiveCfg = Release|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.Release|Any CPU.Build.0 = Release|Any CPU + {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Release|Any CPU.Build.0 = Release|Any CPU + {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Release|Any CPU.Build.0 = Release|Any CPU + {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseLinux|Any CPU.ActiveCfg = ReleaseLinux|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseLinux|Any CPU.Build.0 = ReleaseLinux|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Release|Any CPU.Build.0 = Release|Any CPU + {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseLinux|Any CPU.ActiveCfg = ReleaseLinux|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseLinux|Any CPU.Build.0 = ReleaseLinux|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Release|Any CPU.Build.0 = Release|Any CPU + {73A45373-6307-4D39-9FE9-E8803F95FAB6}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Debug|Any CPU.Build.0 = Debug|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Release|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Release|Any CPU.Build.0 = Release|Any CPU + {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Release|Any CPU.Build.0 = Release|Any CPU + {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Release|Any CPU.Build.0 = Release|Any CPU + {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Release|Any CPU.Build.0 = Release|Any CPU + {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Release|Any CPU.Build.0 = Release|Any CPU + {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Debug|Any CPU.Build.0 = Debug|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Release|Any CPU.ActiveCfg = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Release|Any CPU.Build.0 = Release|Any CPU + {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Release|Any CPU.Build.0 = Release|Any CPU + {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Release|Any CPU.Build.0 = Release|Any CPU + {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Release|Any CPU.Build.0 = Release|Any CPU + {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Debug|Any CPU.Build.0 = Debug|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Release|Any CPU.Build.0 = Release|Any CPU + {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Release|Any CPU.Build.0 = Release|Any CPU + {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Debug|Any CPU.Build.0 = Debug|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Release|Any CPU.ActiveCfg = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Release|Any CPU.Build.0 = Release|Any CPU + {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Release|Any CPU.Build.0 = Release|Any CPU + {42AD06D5-ACDF-4991-B052-232235F002BA}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {42AD06D5-ACDF-4991-B052-232235F002BA}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Release|Any CPU.Build.0 = Release|Any CPU + {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Release|Any CPU.Build.0 = Release|Any CPU + {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Debug|Any CPU.Build.0 = Debug|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Release|Any CPU.ActiveCfg = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Release|Any CPU.Build.0 = Release|Any CPU + {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Debug|Any CPU.Build.0 = Debug|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Release|Any CPU.Build.0 = Release|Any CPU + {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Debug|Any CPU.Build.0 = Debug|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Release|Any CPU.ActiveCfg = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Release|Any CPU.Build.0 = Release|Any CPU + {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Release|Any CPU.Build.0 = Release|Any CPU + {D84F31ED-3D81-42F2-9190-3D021D6038A0}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU + {D84F31ED-3D81-42F2-9190-3D021D6038A0}.ReleaseDotNet|Any CPU.Build.0 = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU EndGlobalSection diff --git a/SharpMap.targets b/SharpMap.targets index 24db265f..054a4843 100644 --- a/SharpMap.targets +++ b/SharpMap.targets @@ -4,6 +4,7 @@ true VersionGit VersionNoTools35 + $(MsBuildThisFileDirectory)BuildTools From def570b17f79ddd012d093bfe6cdd49661934077 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Tue, 14 Jan 2020 15:48:41 +0100 Subject: [PATCH 15/51] Add dotnet nuget push to myget stream --- .github/workflows/dotnetcore.yml | 7 ++++++- SharpMap.Common.props | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 6b426242..8fa3ffee 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -36,4 +36,9 @@ jobs: run: ../../../../testrunner/NUnit.ConsoleRunner.3.10.0/tools/nunit3-console.exe UnitTests.dll working-directory: ./UnitTests/bin/Release/net472 - #shell: cmd + + - name: Publish packages + if: success() + env: + MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }} + run: dotnet nuget push SharpMap.Packages/SharpMap.*.nupkg -k $MYGET_API_KEY -s https://www.myget.org/F/sharpmap/api/v2/package --skip-duplicate diff --git a/SharpMap.Common.props b/SharpMap.Common.props index 722a4664..575f820c 100644 --- a/SharpMap.Common.props +++ b/SharpMap.Common.props @@ -11,6 +11,11 @@ 0 + + + ci.github.$(GITHUB_ACTION) + $(GITHUB_ACTION) + ci.travis.$(TRAVIS_BUILD_NUMBER) @@ -45,7 +50,7 @@ $([System.DateTime]::new($(SmBuildTimestamp)).TimeOfDay.TotalMinutes.ToString("0000")) $(SmMajor).$(SmMinor).$(SmPatch) - pre-$(SmDaysSinceEpoch)$(SmMinutesSinceStartOfUtcDay)+$(SmBuildMetadata) + pre.$(SmDaysSinceEpoch)$(SmMinutesSinceStartOfUtcDay)+$(SmBuildMetadata) From f57ad994e936b84c8d218f6d13e311ea42938e41 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Tue, 14 Jan 2020 16:02:53 +0100 Subject: [PATCH 16/51] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 8fa3ffee..7a233c1d 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -41,4 +41,4 @@ jobs: if: success() env: MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }} - run: dotnet nuget push SharpMap.Packages/SharpMap.*.nupkg -k $MYGET_API_KEY -s https://www.myget.org/F/sharpmap/api/v2/package --skip-duplicate + run: dotnet nuget push SharpMap.Packages/SharpMap.*.nupkg --api-key $MYGET_API_KEY --source https://www.myget.org/F/sharpmap/api/v2/package --skip-duplicate From 450944f7913d58de45356776e5c2c93458d8f5b8 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Wed, 22 Jan 2020 12:10:22 +0100 Subject: [PATCH 17/51] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 7a233c1d..6de7c320 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -37,8 +37,14 @@ jobs: working-directory: ./UnitTests/bin/Release/net472 - - name: Publish packages - if: success() - env: - MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }} - run: dotnet nuget push SharpMap.Packages/SharpMap.*.nupkg --api-key $MYGET_API_KEY --source https://www.myget.org/F/sharpmap/api/v2/package --skip-duplicate +# - name: Publish packages +# if: success() +# env: +# MYGET_API_KEY: ${{ secrets.MYGET_API_KEY }} +# run: dotnet nuget push SharpMap.Packages/SharpMap.*.nupkg --api-key $MYGET_API_KEY --source https://www.myget.org/F/sharpmap/api/v2/package --skip-duplicate + + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: NuGet Package Files + path: SharpMap.Packages From 69abea67d2c9f39d640fcf0006aa753089a2076a Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Wed, 22 Jan 2020 14:26:31 +0100 Subject: [PATCH 18/51] Attempt to fix Travis-CI build --- .travis.yml | 2 +- SharpMap.Common.props | 37 ++++++++++++++++++- .../SharpMap.Extensions.csproj | 12 +++--- SharpMap.targets | 4 +- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 271d6163..4fdbc2c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_install: install: - nuget install NUnit.Console -Version 3.10.0 -OutputDirectory testrunner script: -- nuget restore SharpMap.sln +- nuget restore SharpMap.Extensions/packages.config -o packages - dotnet msbuild SharpMap.targets /t:Version /p:UseTools35=false /v:minimal - dotnet msbuild SharpMap.sln /m "/t:Restore;Build" /p:Configuration=ReleaseLinux "/p:Platform=Any CPU" /p:UseTools35=false "/p:FrameworkPathOverride=$(dirname $(which mono))/../lib/mono/4.5/" /v:minimal /p:WarningLevel=3 - pushd UnitTests/bin/ReleaseLinux/net472 diff --git a/SharpMap.Common.props b/SharpMap.Common.props index 575f820c..cf9404da 100644 --- a/SharpMap.Common.props +++ b/SharpMap.Common.props @@ -36,6 +36,33 @@ local + + + + + + + + + + + $([System.DateTime]::op_Addition( + $([System.DateTime]::new($([MSBuild]::Multiply($(BuildTimeLinux), $([System.TimeSpan]::TicksPerSecond))))), + $([System.TimeSpan]::FromTicks(621355968000000000))).Ticks) + + + + + + $(BuildTimeUtcTicks) + + + + + $([System.DateTime]::UtcNow.Ticks) + + + @@ -44,7 +71,6 @@ 2 0 - $([System.DateTime]::UtcNow.Ticks) $([System.DateTime]::op_Subtraction($([System.DateTime]::new($(SmBuildTimestamp)).Date),$([System.DateTime]::new(621355968000000000))).TotalDays.ToString("00000")) $([System.DateTime]::new($(SmBuildTimestamp)).TimeOfDay.TotalMinutes.ToString("0000")) @@ -79,6 +105,13 @@ $(VersionPrefix).$(SmBuildNo) $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix)-$(VersionSuffix) + + + + $(SolutionDir)SharpMap.Debug.Packages + + + $(SolutionDir)SharpMap.Packages @@ -95,7 +128,7 @@ - + diff --git a/SharpMap.Extensions/SharpMap.Extensions.csproj b/SharpMap.Extensions/SharpMap.Extensions.csproj index 82c21568..2f08f980 100644 --- a/SharpMap.Extensions/SharpMap.Extensions.csproj +++ b/SharpMap.Extensions/SharpMap.Extensions.csproj @@ -27,24 +27,24 @@ true full false - bin\Debug\ + bin\$(Configuration)\ TRACE;DEBUG prompt 4 true AnyCPU - bin\Debug\SharpMap.Extensions.XML + $(OutputPath)SharpMap.Extensions.XML false pdbonly true - bin\Release\ + bin\$(Configuration)\ TRACE prompt 4 true - bin\Release\SharpMap.Extensions.XML + $(OutputPath)SharpMap.Extensions.XML AnyCPU false @@ -52,7 +52,7 @@ bin\ReleaseLinux\ TRACE;LINUX true - bin\Release\SharpMap.Extensions.XML + $(OutputPath)SharpMap.Extensions.XML true pdbonly AnyCPU @@ -179,7 +179,7 @@ - + diff --git a/SharpMap.targets b/SharpMap.targets index 054a4843..5bf8d239 100644 --- a/SharpMap.targets +++ b/SharpMap.targets @@ -17,7 +17,7 @@ - + + From 6685afec7d64904b5d86c2ce03f961a8999e7fb3 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 24 Jan 2020 10:24:02 +0100 Subject: [PATCH 19/51] Fix app.config where applicable * Remove altogether where applicable * Remove outdated binding redirects --- Examples/DemoWinForm/app.config | 6 -- Examples/ExampleCodeSnippets/app.config | 1 - Examples/ExampleCodeSnipplets.VB/app.config | 3 - Examples/WinFormSamples/app.config | 19 ------ SharpMap.Extensions/app.config | 66 ++++--------------- SharpMap.SqlServerSpatialObjects/app.config | 19 ------ .../app.config | 3 - SharpMap/App.config | 20 ------ UnitTests/app.config | 20 ------ 9 files changed, 14 insertions(+), 143 deletions(-) delete mode 100644 Examples/DemoWinForm/app.config delete mode 100644 Examples/ExampleCodeSnipplets.VB/app.config delete mode 100644 SharpMap.SqlServerSpatialObjects/app.config delete mode 100644 SharpMap.Utilities.Indexing.SbnTree/app.config delete mode 100644 SharpMap/App.config diff --git a/Examples/DemoWinForm/app.config b/Examples/DemoWinForm/app.config deleted file mode 100644 index 48db5df4..00000000 --- a/Examples/DemoWinForm/app.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Examples/ExampleCodeSnippets/app.config b/Examples/ExampleCodeSnippets/app.config index 2ba4be27..4913f2e5 100644 --- a/Examples/ExampleCodeSnippets/app.config +++ b/Examples/ExampleCodeSnippets/app.config @@ -5,7 +5,6 @@

    - diff --git a/Examples/ExampleCodeSnipplets.VB/app.config b/Examples/ExampleCodeSnipplets.VB/app.config deleted file mode 100644 index 312bb3f2..00000000 --- a/Examples/ExampleCodeSnipplets.VB/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Examples/WinFormSamples/app.config b/Examples/WinFormSamples/app.config index fdbe0461..26f66778 100644 --- a/Examples/WinFormSamples/app.config +++ b/Examples/WinFormSamples/app.config @@ -57,24 +57,5 @@ - - - - - - - - - - - - - - - - - - - diff --git a/SharpMap.Extensions/app.config b/SharpMap.Extensions/app.config index f792eab5..33ce1930 100644 --- a/SharpMap.Extensions/app.config +++ b/SharpMap.Extensions/app.config @@ -2,56 +2,18 @@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/SharpMap.SqlServerSpatialObjects/app.config b/SharpMap.SqlServerSpatialObjects/app.config deleted file mode 100644 index a0e2f076..00000000 --- a/SharpMap.SqlServerSpatialObjects/app.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/SharpMap.Utilities.Indexing.SbnTree/app.config b/SharpMap.Utilities.Indexing.SbnTree/app.config deleted file mode 100644 index 2d624785..00000000 --- a/SharpMap.Utilities.Indexing.SbnTree/app.config +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/SharpMap/App.config b/SharpMap/App.config deleted file mode 100644 index 4e7bc0ef..00000000 --- a/SharpMap/App.config +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/UnitTests/app.config b/UnitTests/app.config index 8ce0b5e0..54d4781b 100644 --- a/UnitTests/app.config +++ b/UnitTests/app.config @@ -32,24 +32,4 @@ --> - - - - - - - - - - - - - - - - - - - - From 484e1480af406704fe667bf01f4e970a519948d0 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 24 Jan 2020 10:28:42 +0100 Subject: [PATCH 20/51] Prevent attempt to render layer with null envelope --- SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs index 96f700a6..bdfad372 100644 --- a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs +++ b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs @@ -480,6 +480,9 @@ private Rectangle RenderLayerImage(object param) if (token.IsCancellationRequested) return Rectangle.Empty; + if (lyr.Envelope == null || lyr.Envelope.Width == 0d) + return Rectangle.Empty; + var updateRect = Rectangle.Empty; var sw = new Stopwatch(); From d603464c4068369fd03cbc3fd534d1a35d449168 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 24 Jan 2020 10:29:48 +0100 Subject: [PATCH 21/51] Reuse FileCache from HttpTileProvider --- SharpMap.Layers.BruTile/Layers/TileLayer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SharpMap.Layers.BruTile/Layers/TileLayer.cs b/SharpMap.Layers.BruTile/Layers/TileLayer.cs index b862702f..5bb0a66f 100644 --- a/SharpMap.Layers.BruTile/Layers/TileLayer.cs +++ b/SharpMap.Layers.BruTile/Layers/TileLayer.cs @@ -9,6 +9,7 @@ using System.Threading; using BruTile; using BruTile.Cache; +using BruTile.Web; using Common.Logging; using GeoAPI.Geometries; @@ -133,8 +134,15 @@ public TileLayer(ITileSource tileSource, string layerName, Color transparentColo if (!string.IsNullOrEmpty(fileCacheDir)) { _fileCache = new FileCache(fileCacheDir, "png"); - _ImageFormat = ImageFormat.Png; } + else + { + _fileCache = (tileSource as HttpTileProvider)?.PersistentCache as FileCache; + } + + if (_fileCache != null) + _ImageFormat = ImageFormat.Png; + } /// From 01964e0c5b2c364a3cadc9e7ea0b6d87621a095f Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Fri, 24 Jan 2020 10:30:17 +0100 Subject: [PATCH 22/51] Fix some csproj files --- .gitignore | 2 +- .../SharpMap.Data.Providers.FileGdb.csproj | 2 +- .../SharpMap.SqlServerSpatialObjects.csproj | 38 +++++++--------- SharpMap.sln | 44 +++++++++++++++++++ SharpMap/SharpMap.csproj | 10 ++--- UnitTests/UnitTests.csproj | 2 +- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 37b8aa57..76e5aea4 100644 --- a/.gitignore +++ b/.gitignore @@ -211,4 +211,4 @@ I[V|v][V|v]*.* testrunner/** # Generated packages -SharpMap.Packages/** \ No newline at end of file +/SharpMap.*Packages/** diff --git a/SharpMap.Data.Providers.FileGdb/SharpMap.Data.Providers.FileGdb.csproj b/SharpMap.Data.Providers.FileGdb/SharpMap.Data.Providers.FileGdb.csproj index 9fb64c47..df22f910 100644 --- a/SharpMap.Data.Providers.FileGdb/SharpMap.Data.Providers.FileGdb.csproj +++ b/SharpMap.Data.Providers.FileGdb/SharpMap.Data.Providers.FileGdb.csproj @@ -16,7 +16,7 @@ - + Libraries\bin\Esri.FileGDBAPI.dll diff --git a/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj b/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj index 6968c678..49cc3beb 100644 --- a/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj +++ b/SharpMap.SqlServerSpatialObjects/SharpMap.SqlServerSpatialObjects.csproj @@ -1,8 +1,6 @@  - - - + Debug AnyCPU @@ -88,32 +86,30 @@ - Designer - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + - {C83777FC-AABB-47D9-911F-D76255D4D541} @@ -121,4 +117,4 @@ - + \ No newline at end of file diff --git a/SharpMap.sln b/SharpMap.sln index d8761ba7..6367283d 100644 --- a/SharpMap.sln +++ b/SharpMap.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{A5E400B4 ProjectSection(SolutionItems) = preProject .travis.yml = .travis.yml .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml + SharedAssemblyVersion.cs = SharedAssemblyVersion.cs SharpMap.Common.props = SharpMap.Common.props SharpMap.targets = SharpMap.targets EndProjectSection @@ -77,6 +78,7 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugDotNet|Any CPU = DebugDotNet|Any CPU Release|Any CPU = Release|Any CPU ReleaseDotNet|Any CPU = ReleaseDotNet|Any CPU ReleaseLinux|Any CPU = ReleaseLinux|Any CPU @@ -84,6 +86,8 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {C83777FC-AABB-47D9-911F-D76255D4D541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C83777FC-AABB-47D9-911F-D76255D4D541}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {C83777FC-AABB-47D9-911F-D76255D4D541}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.Release|Any CPU.ActiveCfg = Release|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.Release|Any CPU.Build.0 = Release|Any CPU {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -92,6 +96,8 @@ Global {C83777FC-AABB-47D9-911F-D76255D4D541}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.Release|Any CPU.Build.0 = Release|Any CPU {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -99,6 +105,8 @@ Global {DD1CC1DB-4BF9-4C88-A100-733D84795F3A}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.Release|Any CPU.Build.0 = Release|Any CPU {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -107,6 +115,8 @@ Global {0C76DD99-AC5D-47C0-B76F-CE70092A9AC7}.ReleaseLinux|Any CPU.Build.0 = ReleaseLinux|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4140C12-53F5-438C-8D24-9E48C504FECF}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {A4140C12-53F5-438C-8D24-9E48C504FECF}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.Release|Any CPU.Build.0 = Release|Any CPU {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -115,18 +125,21 @@ Global {A4140C12-53F5-438C-8D24-9E48C504FECF}.ReleaseLinux|Any CPU.Build.0 = ReleaseLinux|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73A45373-6307-4D39-9FE9-E8803F95FAB6}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.Release|Any CPU.Build.0 = Release|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {73A45373-6307-4D39-9FE9-E8803F95FAB6}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Release|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.Release|Any CPU.Build.0 = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {F81C1843-3B2A-4D72-81AA-A658DB11FA85}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.Release|Any CPU.Build.0 = Release|Any CPU {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -134,6 +147,8 @@ Global {A4184D38-C5A6-4E01-9D1C-8C63791AF825}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.Release|Any CPU.Build.0 = Release|Any CPU {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -141,12 +156,15 @@ Global {6D681045-8EF1-44EA-A19D-C7A63A6D0F76}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.Release|Any CPU.Build.0 = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {7EF67DF8-D868-4177-8817-4CC2E94ABD66}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.Release|Any CPU.Build.0 = Release|Any CPU {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -155,6 +173,8 @@ Global {75E9B9FF-D04E-4F4F-8D06-8CD310F5F2DB}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Debug|Any CPU.Build.0 = Debug|Any CPU + {292EF671-4063-4952-8DE0-423DF72A0950}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {292EF671-4063-4952-8DE0-423DF72A0950}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Release|Any CPU.ActiveCfg = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.Release|Any CPU.Build.0 = Release|Any CPU {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -163,6 +183,8 @@ Global {292EF671-4063-4952-8DE0-423DF72A0950}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.Release|Any CPU.Build.0 = Release|Any CPU {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -171,6 +193,8 @@ Global {47F790C2-6DC0-4A09-9026-FC81A09FD6A3}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.Release|Any CPU.Build.0 = Release|Any CPU {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -178,6 +202,8 @@ Global {9BBED556-40D8-4DDC-AC11-7B03300E2A2E}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9DA6934-413F-4744-84CE-923D901F60E6}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {F9DA6934-413F-4744-84CE-923D901F60E6}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.Release|Any CPU.Build.0 = Release|Any CPU {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -186,6 +212,8 @@ Global {F9DA6934-413F-4744-84CE-923D901F60E6}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.Release|Any CPU.Build.0 = Release|Any CPU {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -193,12 +221,15 @@ Global {6DDA47A4-CE0C-4880-82CE-6EF6FEEC4912}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.Release|Any CPU.Build.0 = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {28AC314E-35C1-484B-9DD3-116BD8AFC4EA}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Release|Any CPU.ActiveCfg = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.Release|Any CPU.Build.0 = Release|Any CPU {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -207,6 +238,8 @@ Global {723F9FD5-8424-4EEE-BA92-57ECEE0F395D}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42AD06D5-ACDF-4991-B052-232235F002BA}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {42AD06D5-ACDF-4991-B052-232235F002BA}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.Release|Any CPU.Build.0 = Release|Any CPU {42AD06D5-ACDF-4991-B052-232235F002BA}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -214,6 +247,8 @@ Global {42AD06D5-ACDF-4991-B052-232235F002BA}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.Release|Any CPU.Build.0 = Release|Any CPU {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -222,12 +257,15 @@ Global {AF0594AB-FC1D-47E4-8CDD-361CDC621A36}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.Release|Any CPU.Build.0 = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU {E9AC999C-5C90-4851-ABFE-8A9CA899FAEF}.ReleaseLinux|Any CPU.ActiveCfg = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Debug|Any CPU.Build.0 = Debug|Any CPU + {721412E1-5589-4A18-A095-F8E0427BB163}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {721412E1-5589-4A18-A095-F8E0427BB163}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Release|Any CPU.ActiveCfg = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.Release|Any CPU.Build.0 = Release|Any CPU {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -236,6 +274,8 @@ Global {721412E1-5589-4A18-A095-F8E0427BB163}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4182B07-6955-434B-904C-15ABEA5F7409}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {E4182B07-6955-434B-904C-15ABEA5F7409}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.Release|Any CPU.Build.0 = Release|Any CPU {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -244,6 +284,8 @@ Global {E4182B07-6955-434B-904C-15ABEA5F7409}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A019B82E-3766-44A3-907A-0487FF9EBA49}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {A019B82E-3766-44A3-907A-0487FF9EBA49}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Release|Any CPU.ActiveCfg = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.Release|Any CPU.Build.0 = Release|Any CPU {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU @@ -252,6 +294,8 @@ Global {A019B82E-3766-44A3-907A-0487FF9EBA49}.ReleaseLinux|Any CPU.Build.0 = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D84F31ED-3D81-42F2-9190-3D021D6038A0}.DebugDotNet|Any CPU.ActiveCfg = Debug|Any CPU + {D84F31ED-3D81-42F2-9190-3D021D6038A0}.DebugDotNet|Any CPU.Build.0 = Debug|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.Release|Any CPU.Build.0 = Release|Any CPU {D84F31ED-3D81-42F2-9190-3D021D6038A0}.ReleaseDotNet|Any CPU.ActiveCfg = Release|Any CPU diff --git a/SharpMap/SharpMap.csproj b/SharpMap/SharpMap.csproj index 6b49704f..f54fc8d1 100644 --- a/SharpMap/SharpMap.csproj +++ b/SharpMap/SharpMap.csproj @@ -5,7 +5,7 @@ netstandard2.0 SharpMap Engine - Copyright 2006 Morten Nielsen, Copyright 2008-2019 SharpMap - Team + Copyright 2006 Morten Nielsen, Copyright 2008-$([System.DateTime]::UtcNow.Year) SharpMap - Team SharpMap GIS Map GeoSpatial This package contains the core components of SharpMap. @@ -33,10 +33,10 @@ - - - - + + + + diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 7ba69506..b6829dc6 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -31,7 +31,7 @@ - + From 672449af60832b37d2e766ef9e1357cb0d24cbca Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Tue, 4 Feb 2020 13:18:04 +0100 Subject: [PATCH 23/51] Add bindingRedirect for NewtonSoft.Json to UnitTests --- UnitTests/app.config | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/UnitTests/app.config b/UnitTests/app.config index 54d4781b..429c0df4 100644 --- a/UnitTests/app.config +++ b/UnitTests/app.config @@ -32,4 +32,12 @@ --> + + + + + + + + From 9206cd8c672d5cff58e403e52132de1bd5bbaec2 Mon Sep 17 00:00:00 2001 From: Tim C Date: Thu, 9 Jan 2020 19:49:14 +1300 Subject: [PATCH 24/51] Add FormInteractiveVectorLayerRendering to WinFormSamples --- .../WinFormSamples/DlgSamplesMenu.Designer.cs | 64 +- Examples/WinFormSamples/DlgSamplesMenu.cs | 129 +-- ...nteractiveVectorLayerRendering.Designer.cs | 186 +++++ .../FormInteractiveVectorLayerRendering.cs | 748 ++++++++++++++++++ .../FormInteractiveVectorLayerRendering.resx | 126 +++ .../Properties/Resources.Designer.cs | 238 +++--- .../WinFormSamples/Properties/Resources.resx | 273 +++---- .../WinFormSamples/Resources/vessel_01.png | Bin 0 -> 469 bytes 8 files changed, 1436 insertions(+), 328 deletions(-) create mode 100644 Examples/WinFormSamples/FormInteractiveVectorLayerRendering.Designer.cs create mode 100644 Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs create mode 100644 Examples/WinFormSamples/FormInteractiveVectorLayerRendering.resx create mode 100644 Examples/WinFormSamples/Resources/vessel_01.png diff --git a/Examples/WinFormSamples/DlgSamplesMenu.Designer.cs b/Examples/WinFormSamples/DlgSamplesMenu.Designer.cs index 3d0bf895..d1bd1ebb 100644 --- a/Examples/WinFormSamples/DlgSamplesMenu.Designer.cs +++ b/Examples/WinFormSamples/DlgSamplesMenu.Designer.cs @@ -29,6 +29,7 @@ protected override void Dispose(bool disposing) private void InitializeComponent() { this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.checkBox1 = new System.Windows.Forms.CheckBox(); this.button2 = new System.Windows.Forms.Button(); this.textBox2 = new System.Windows.Forms.TextBox(); this.textBox1 = new System.Windows.Forms.TextBox(); @@ -41,7 +42,8 @@ private void InitializeComponent() this.button6 = new System.Windows.Forms.Button(); this.textBox6 = new System.Windows.Forms.TextBox(); this.button3 = new System.Windows.Forms.Button(); - this.checkBox1 = new System.Windows.Forms.CheckBox(); + this.textBox7 = new System.Windows.Forms.TextBox(); + this.button7 = new System.Windows.Forms.Button(); this.tableLayoutPanel1.SuspendLayout(); this.SuspendLayout(); // @@ -50,6 +52,7 @@ private void InitializeComponent() this.tableLayoutPanel1.ColumnCount = 2; this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 70F)); this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 30F)); + this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 7); this.tableLayoutPanel1.Controls.Add(this.button2, 1, 0); this.tableLayoutPanel1.Controls.Add(this.textBox2, 0, 0); this.tableLayoutPanel1.Controls.Add(this.textBox1, 0, 4); @@ -62,11 +65,13 @@ private void InitializeComponent() this.tableLayoutPanel1.Controls.Add(this.button6, 1, 3); this.tableLayoutPanel1.Controls.Add(this.textBox6, 0, 5); this.tableLayoutPanel1.Controls.Add(this.button3, 1, 5); - this.tableLayoutPanel1.Controls.Add(this.checkBox1, 0, 6); + this.tableLayoutPanel1.Controls.Add(this.textBox7, 0, 6); + this.tableLayoutPanel1.Controls.Add(this.button7, 1, 6); this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Top; this.tableLayoutPanel1.Location = new System.Drawing.Point(10, 10); this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 7; + this.tableLayoutPanel1.RowCount = 8; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); @@ -74,9 +79,21 @@ private void InitializeComponent() this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 50F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(264, 348); + this.tableLayoutPanel1.Size = new System.Drawing.Size(264, 403); this.tableLayoutPanel1.TabIndex = 0; // + // checkBox1 + // + this.checkBox1.AutoSize = true; + this.checkBox1.Dock = System.Windows.Forms.DockStyle.Fill; + this.checkBox1.Location = new System.Drawing.Point(3, 353); + this.checkBox1.Name = "checkBox1"; + this.checkBox1.Size = new System.Drawing.Size(178, 47); + this.checkBox1.TabIndex = 8; + this.checkBox1.Text = "MapBox rendering using LegacyMapImageRenderer"; + this.checkBox1.UseVisualStyleBackColor = true; + this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged); + // // button2 // this.button2.Location = new System.Drawing.Point(187, 3); @@ -94,7 +111,7 @@ private void InitializeComponent() this.textBox2.Name = "textBox2"; this.textBox2.Size = new System.Drawing.Size(178, 36); this.textBox2.TabIndex = 2; - this.textBox2.Text = "Different kind of layers which [MapBox]"; + this.textBox2.Text = "Different kind of layers supported by [MapBox]"; // // textBox1 // @@ -140,7 +157,7 @@ private void InitializeComponent() this.textBox5.Name = "textBox5"; this.textBox5.Size = new System.Drawing.Size(178, 36); this.textBox5.TabIndex = 2; - this.textBox5.Text = "Shows Shows how to save a set of images [MapBox]"; + this.textBox5.Text = "Shows how to save a set of images [MapBox]"; // // button4 // @@ -179,7 +196,7 @@ private void InitializeComponent() this.textBox6.Name = "textBox6"; this.textBox6.Size = new System.Drawing.Size(178, 36); this.textBox6.TabIndex = 4; - this.textBox6.Text = "Different kind of layers which [MapBox using DotSpatial]"; + this.textBox6.Text = "Different kind of layers supported by [MapBox using DotSpatial]"; // // button3 // @@ -191,23 +208,30 @@ private void InitializeComponent() this.button3.UseVisualStyleBackColor = true; this.button3.Click += new System.EventHandler(this.button2_Click); // - // checkBox1 + // textBox7 // - this.checkBox1.AutoSize = true; - this.checkBox1.Dock = System.Windows.Forms.DockStyle.Fill; - this.checkBox1.Location = new System.Drawing.Point(3, 303); - this.checkBox1.Name = "checkBox1"; - this.checkBox1.Size = new System.Drawing.Size(178, 44); - this.checkBox1.TabIndex = 7; - this.checkBox1.Text = "MapBox rendering using LegacyMapImageRenderer"; - this.checkBox1.UseVisualStyleBackColor = true; - this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged); + this.textBox7.Location = new System.Drawing.Point(3, 303); + this.textBox7.Multiline = true; + this.textBox7.Name = "textBox7"; + this.textBox7.Size = new System.Drawing.Size(178, 36); + this.textBox7.TabIndex = 9; + this.textBox7.Text = "[MapBox] with interactive layers for image generation validation"; + // + // button7 + // + this.button7.Location = new System.Drawing.Point(187, 303); + this.button7.Name = "button7"; + this.button7.Size = new System.Drawing.Size(74, 23); + this.button7.TabIndex = 10; + this.button7.Text = "Start"; + this.button7.UseVisualStyleBackColor = true; + this.button7.Click += new System.EventHandler(this.button7_Click); // // DlgSamplesMenu // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(284, 368); + this.ClientSize = new System.Drawing.Size(284, 419); this.Controls.Add(this.tableLayoutPanel1); this.MaximizeBox = false; this.MinimizeBox = false; @@ -237,5 +261,7 @@ private void InitializeComponent() private System.Windows.Forms.TextBox textBox6; private System.Windows.Forms.Button button3; private System.Windows.Forms.CheckBox checkBox1; + private System.Windows.Forms.TextBox textBox7; + private System.Windows.Forms.Button button7; } -} \ No newline at end of file +} diff --git a/Examples/WinFormSamples/DlgSamplesMenu.cs b/Examples/WinFormSamples/DlgSamplesMenu.cs index 0130eb55..ad43a3ab 100644 --- a/Examples/WinFormSamples/DlgSamplesMenu.cs +++ b/Examples/WinFormSamples/DlgSamplesMenu.cs @@ -1,61 +1,68 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Text; -using System.Windows.Forms; -using SharpMap.Forms; - -namespace WinFormSamples -{ - public partial class DlgSamplesMenu : Form - { - public DlgSamplesMenu() - { - InitializeComponent(); - checkBox1.Checked = true; - button2.Focus(); - } - - private void button2_Click(object sender, EventArgs e) - { - var ds = sender == button3; - FormMapBox.UseDotSpatial = ds; - using (var f = new FormMapBox()) - f.ShowDialog(); - } - - private void button4_Click(object sender, EventArgs e) - { - using(var f = new DockAreaForm()) - f.ShowDialog(); - } - - private void button5_Click(object sender, EventArgs e) - { - using(var f = new FormDemoDrawGeometries()) - f.ShowDialog(); - } - - private void button6_Click(object sender, EventArgs e) - { - using(var f = new FormAnimation()) - f.ShowDialog(); - } - - private void button1_Click(object sender, EventArgs e) - { - using(var f = new FormMovingObjectOverTileLayer()) - f.ShowDialog(); - } - - private void checkBox1_CheckedChanged(object sender, EventArgs e) - { - if (checkBox1.Checked) - MapBox.MapImageGeneratorFunction = null; - else - MapBox.MapImageGeneratorFunction = MapBox.LayerListImageGenerator; - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using SharpMap.Forms; + +namespace WinFormSamples +{ + public partial class DlgSamplesMenu : Form + { + public DlgSamplesMenu() + { + InitializeComponent(); + checkBox1.Checked = true; + button2.Focus(); + } + + private void button2_Click(object sender, EventArgs e) + { + var ds = sender == button3; + FormMapBox.UseDotSpatial = ds; + using (var f = new FormMapBox()) + f.ShowDialog(); + } + + private void button4_Click(object sender, EventArgs e) + { + using(var f = new DockAreaForm()) + f.ShowDialog(); + } + + private void button5_Click(object sender, EventArgs e) + { + using(var f = new FormDemoDrawGeometries()) + f.ShowDialog(); + } + + private void button6_Click(object sender, EventArgs e) + { + using(var f = new FormAnimation()) + f.ShowDialog(); + } + + private void button1_Click(object sender, EventArgs e) + { + using(var f = new FormMovingObjectOverTileLayer()) + f.ShowDialog(); + } + + private void button7_Click(object sender, EventArgs e) + { + using (var f = new FormLayerListImageGenerator()) + f.ShowDialog(); + } + private void checkBox1_CheckedChanged(object sender, EventArgs e) + { + if (checkBox1.Checked) + MapBox.MapImageGeneratorFunction = null; + else + MapBox.MapImageGeneratorFunction = MapBox.LayerListImageGenerator; + } + + + } +} diff --git a/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.Designer.cs b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.Designer.cs new file mode 100644 index 00000000..a6cf8ad3 --- /dev/null +++ b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.Designer.cs @@ -0,0 +1,186 @@ +using System; +using System.ComponentModel; +using System.Windows.Forms; + +namespace WinFormSamples +{ + partial class FormLayerListImageGenerator + { + /// + /// Required designer variable. + /// + private IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.tv = new System.Windows.Forms.TreeView(); + this.mapZoomToolStrip1 = new SharpMap.Forms.ToolBar.MapZoomToolStrip(this.components); + this.mb = new SharpMap.Forms.MapBox(); + this.toolStripContainer1 = new System.Windows.Forms.ToolStripContainer(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.panel1 = new System.Windows.Forms.Panel(); + this.txtImgGeneration = new System.Windows.Forms.TextBox(); + this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); + this.toolStripContainer1.ContentPanel.SuspendLayout(); + this.toolStripContainer1.TopToolStripPanel.SuspendLayout(); + this.toolStripContainer1.SuspendLayout(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // tv + // + this.tv.CheckBoxes = true; + this.tv.Dock = System.Windows.Forms.DockStyle.Fill; + this.tv.Location = new System.Drawing.Point(0, 38); + this.tv.Name = "tv"; + this.tv.Size = new System.Drawing.Size(200, 519); + this.tv.TabIndex = 0; + this.tv.AfterCheck += new System.Windows.Forms.TreeViewEventHandler(this.tv_AfterCheck); + this.tv.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.tv_NodeMouseClick); + // + // mapZoomToolStrip1 + // + this.mapZoomToolStrip1.Dock = System.Windows.Forms.DockStyle.None; + this.mapZoomToolStrip1.Enabled = false; + this.mapZoomToolStrip1.Location = new System.Drawing.Point(3, 0); + this.mapZoomToolStrip1.MapControl = this.mb; + this.mapZoomToolStrip1.Name = "mapZoomToolStrip1"; + this.mapZoomToolStrip1.Size = new System.Drawing.Size(409, 25); + this.mapZoomToolStrip1.TabIndex = 4; + this.mapZoomToolStrip1.Text = "mapZoomToolStrip1"; + // + // mb + // + this.mb.ActiveTool = SharpMap.Forms.MapBox.Tools.Pan; + this.mb.Cursor = System.Windows.Forms.Cursors.Hand; + this.mb.CustomTool = null; + this.mb.Dock = System.Windows.Forms.DockStyle.Fill; + this.mb.FineZoomFactor = 10D; + this.mb.Location = new System.Drawing.Point(0, 0); + this.mb.MapQueryMode = SharpMap.Forms.MapBox.MapQueryType.LayerByIndex; + this.mb.Name = "mb"; + this.mb.QueryGrowFactor = 5F; + this.mb.QueryLayerIndex = 0; + this.mb.SelectionBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(210)))), ((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); + this.mb.SelectionForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(244)))), ((int)(((byte)(244)))), ((int)(((byte)(244))))); + this.mb.ShowProgressUpdate = false; + this.mb.Size = new System.Drawing.Size(676, 580); + this.mb.TabIndex = 6; + this.mb.Text = "mb"; + this.mb.WheelZoomMagnitude = -2D; + // + // toolStripContainer1 + // + // + // toolStripContainer1.ContentPanel + // + this.toolStripContainer1.ContentPanel.Controls.Add(this.mb); + this.toolStripContainer1.ContentPanel.Size = new System.Drawing.Size(676, 580); + this.toolStripContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.toolStripContainer1.Location = new System.Drawing.Point(200, 0); + this.toolStripContainer1.Name = "toolStripContainer1"; + this.toolStripContainer1.Size = new System.Drawing.Size(676, 605); + this.toolStripContainer1.TabIndex = 7; + this.toolStripContainer1.Text = "toolStripContainer1"; + // + // toolStripContainer1.TopToolStripPanel + // + this.toolStripContainer1.TopToolStripPanel.Controls.Add(this.mapZoomToolStrip1); + // + // textBox1 + // + this.textBox1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBox1.Location = new System.Drawing.Point(0, 557); + this.textBox1.Multiline = true; + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(200, 48); + this.textBox1.TabIndex = 8; + this.textBox1.Text = "NB: Tree view has layer-sensitive context menu to interact with Variable Layers a" + + "nd Map Point Layers"; + // + // panel1 + // + this.panel1.Controls.Add(this.tv); + this.panel1.Controls.Add(this.txtImgGeneration); + this.panel1.Controls.Add(this.textBox1); + this.panel1.Dock = System.Windows.Forms.DockStyle.Left; + this.panel1.Location = new System.Drawing.Point(0, 0); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(200, 605); + this.panel1.TabIndex = 9; + // + // txtImgGeneration + // + this.txtImgGeneration.Dock = System.Windows.Forms.DockStyle.Top; + this.txtImgGeneration.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.txtImgGeneration.ForeColor = System.Drawing.SystemColors.MenuHighlight; + this.txtImgGeneration.Location = new System.Drawing.Point(0, 0); + this.txtImgGeneration.Multiline = true; + this.txtImgGeneration.Name = "txtImgGeneration"; + this.txtImgGeneration.Size = new System.Drawing.Size(200, 38); + this.txtImgGeneration.TabIndex = 9; + this.txtImgGeneration.Text = "Image generation mode:"; + // + // contextMenuStrip1 + // + this.contextMenuStrip1.Name = "contextMenuStrip1"; + this.contextMenuStrip1.Size = new System.Drawing.Size(61, 4); + // + // FormLayerListImageGenerator + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(876, 605); + this.Controls.Add(this.toolStripContainer1); + this.Controls.Add(this.panel1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "FormLayerListImageGenerator"; + this.Text = "LayerListImageGenerator Validation"; + this.Closing += new System.ComponentModel.CancelEventHandler(this.FormLayerListImageGenerator_Closing); + this.Load += new System.EventHandler(this.FormLayerListImageGenerator_Load); + this.toolStripContainer1.ContentPanel.ResumeLayout(false); + this.toolStripContainer1.TopToolStripPanel.ResumeLayout(false); + this.toolStripContainer1.TopToolStripPanel.PerformLayout(); + this.toolStripContainer1.ResumeLayout(false); + this.toolStripContainer1.PerformLayout(); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TreeView tv; + private SharpMap.Forms.ToolBar.MapZoomToolStrip mapZoomToolStrip1; + private SharpMap.Forms.MapBox mb; + private System.Windows.Forms.ToolStripContainer toolStripContainer1; + private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.TextBox txtImgGeneration; + } +} + diff --git a/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs new file mode 100644 index 00000000..893b4ba5 --- /dev/null +++ b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs @@ -0,0 +1,748 @@ +using BruTile.Predefined; +using GeoAPI.CoordinateSystems.Transformations; +using GeoAPI.Geometries; +using NetTopologySuite.Geometries; +using SharpMap; +using SharpMap.Data; +using SharpMap.Data.Providers; +using SharpMap.Layers; +using SharpMap.Rendering.Decoration; +using SharpMap.Rendering.Symbolizer; +using SharpMap.Styles; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.Remoting.Channels; +using System.Windows.Forms; +using WinFormSamples.Properties; +using Point = NetTopologySuite.Geometries.Point; + +namespace WinFormSamples +{ + public partial class FormLayerListImageGenerator : Form + { + private MovingObjects _fastBoats; + private MovingObjects _mediumBoats; + private MovingObjects _slowBoats; + private static Image _boat; + + private Layer _contextLayer; + + // Context Menu actions + private enum enumMenuItem + { + Refresh, // MapBox.Refresh + IncrementLabelSize, // Label Layers + DecrementLabelSize, // Label Layers + StartMoving, // Variable Layer moving object + StopMoving, // Variable Layer moving object + RegularSymbolizer, // Variable Layer and Map Point layer + ThematicSymbolizer, // Variable Layer and Map Point layer + IncreaseSymbolSize, // Basic Vector Style Point Size + DecreaseSymbolSize, // Basic Vector Style Point Size + SymbolOffsetTL, // Basic Vector Style Point Size + SymbolOffsetTR, // Basic Vector Style Point Size + SymbolOffsetNone, // Basic Vector Style Point Size + SymbolOffsetBL, // Basic Vector Style Point Size + SymbolOffsetBR, // Basic Vector Style Point Size + AlignHz, // Regenerate Map Point layer data + AlignVt, // Regenerate Map Point layer data + AlignDiagonal // Regenerate Map Point layer data + } + + public FormLayerListImageGenerator() + { + InitializeComponent(); + } + + private void FormLayerListImageGenerator_Load(object sender, System.EventArgs e) + { + this.SizeChanged += Form_SizeChanged; + + _boat = Resources.vessel_01; + + InitMap(); + InitBackground(); + InitLayers(); + InitVariableLayers(); + InitTreeView(); + this.mb.Refresh(); + + using (var renderer = SharpMap.Forms.MapBox.MapImageGeneratorFunction(new SharpMap.Forms.MapBox(), null)) + { + if (renderer is SharpMap.Forms.ImageGenerator.LegacyMapBoxImageGenerator) + this.txtImgGeneration.Text = (this.txtImgGeneration.Text + "\n. LegacyMapImageRenderer"); + else + this.txtImgGeneration.Text = (this.txtImgGeneration.Text + "\n. LayerListImageGenerator"); + }; + + //_slowBoats?.Start(); + _mediumBoats?.Start(); + _fastBoats?.Start(); + } + + private void FormLayerListImageGenerator_Closing(object sender, EventArgs e) + { + this.SizeChanged -= Form_SizeChanged; + _fastBoats?.Dispose(); + _mediumBoats?.Dispose(); + _slowBoats?.Dispose(); + } + + private void Form_SizeChanged(object sender, EventArgs e) + { + this.mb.Refresh(); + } + + private void tv_AfterCheck(object sender, TreeViewEventArgs e) + { + if (e.Node.Tag != null) + { + var lyr = (Layer)e.Node.Tag; + lyr.Enabled = e.Node.Checked; + this.mb.Refresh(); + } + } + + #region Context Menu Stuff + private void tv_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) + { + if (e.Button != MouseButtons.Right) return; + + var cm = new ContextMenu(); + var vlyr = e.Node.Tag as VectorLayer; + _contextLayer = e.Node.Tag as Layer; + + if (vlyr != null && (e.Node.FullPath.StartsWith("Variable Layers", StringComparison.OrdinalIgnoreCase))) + { + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + + if (_contextLayer.LayerName.StartsWith("Fast")) + cm.MenuItems.Add(_fastBoats.IsRunning ? + CreateMenuItem(enumMenuItem.StopMoving, "Stop") : + CreateMenuItem(enumMenuItem.StartMoving, "Start")); + else if (_contextLayer.LayerName.StartsWith("Slow")) + cm.MenuItems.Add(_slowBoats.IsRunning ? + CreateMenuItem(enumMenuItem.StopMoving, "Stop") : + CreateMenuItem(enumMenuItem.StartMoving, "Start")); + else + cm.MenuItems.Add(_mediumBoats.IsRunning ? + CreateMenuItem(enumMenuItem.StopMoving, "Stop") : + CreateMenuItem(enumMenuItem.StartMoving, "Start")); + + //cm.MenuItems.Add(CreateMenuItem(enumMenuItem.StartMoving, "Start")); + //cm.MenuItems.Add(CreateMenuItem(enumMenuItem.StopMoving, "Stop")); + cm.MenuItems.Add(new MenuItem("-")); + + if (vlyr.Theme == null) + { + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.ThematicSymbolizer, "Thematic Symbolizer")); + cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.IncreaseSymbolSize, "Increment symbol size")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.DecreaseSymbolSize, "Decrement symbol size")); + } + else + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.RegularSymbolizer, "Basic Symbolizer")); + } + else if (vlyr != null && (e.Node.FullPath.StartsWith("Map Layers", StringComparison.OrdinalIgnoreCase))) + { + if (vlyr.LayerName.StartsWith("Point", StringComparison.OrdinalIgnoreCase) ) + { + if (vlyr.Theme == null) + { + // default point style + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.IncreaseSymbolSize, "Increment symbol size")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.DecreaseSymbolSize, "Decrement symbol size")); + cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetNone, "Remove Symbol Offset")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTL, "Offset Step Top Left")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTR, "Offset Step Top Right")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBL, "Offset Step Bottom Left")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBR, "Offset Step Bottom Right")); + } + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignHz, "Align Horizontal")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignVt, "Align Vertical")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignDiagonal, "Align Diagonal")); + } + } + + if (e.Node.Tag is LabelLayer) + { + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.IncrementLabelSize, "Increment Label Size")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.DecrementLabelSize, "Decrement Label Size")); + } + + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.Refresh, "Refresh [clear cache]")); + + cm.Show(tv, new System.Drawing.Point(e.X, e.Y)); + } + + private MenuItem CreateMenuItem(enumMenuItem eMenuItem, string text) + { + var mi = new MenuItem(text) + { + Tag = eMenuItem, + }; + mi.Click += MenuItemClick; + return mi; + } + + private void MenuItemClick (Object sender, EventArgs e) + { + var mi = sender as MenuItem; + if (mi == null) return; + + var vectorLyr = _contextLayer as VectorLayer; + var lblLyr = _contextLayer as LabelLayer; + + switch ((enumMenuItem)mi.Tag) + { + //case enumMenuItem.Refresh: + // this.mb.Refresh(); + // break; + case enumMenuItem.StartMoving: + if (_contextLayer.LayerName.StartsWith("Fast")) + _fastBoats?.Start(); + else if (_contextLayer.LayerName.StartsWith("Slow")) + _slowBoats?.Start(); + else + _mediumBoats?.Start(); + break; + + case enumMenuItem.StopMoving: + if (_contextLayer.LayerName.StartsWith("Fast")) + _fastBoats?.Stop(); + else if (_contextLayer.LayerName.StartsWith("Slow")) + _slowBoats?.Stop(); + else + _mediumBoats?.Stop(); + break; + + case enumMenuItem.IncrementLabelSize: + lblLyr.Style.Font = new Font(lblLyr.Style.Font.FontFamily, lblLyr.Style.Font.Size + 2); + break; + case enumMenuItem.DecrementLabelSize: + lblLyr.Style.Font = new Font(lblLyr.Style.Font.FontFamily, lblLyr.Style.Font.Size - 2); + break; + case enumMenuItem.RegularSymbolizer: + InitRasterPointSymbolizer(vectorLyr, 0); + break; + case enumMenuItem.ThematicSymbolizer: + InitRasterPointSymbolizer(vectorLyr, 1); + break; + case enumMenuItem.IncreaseSymbolSize: + vectorLyr.Style.PointSize += 2; + break; + case enumMenuItem.DecreaseSymbolSize: + vectorLyr.Style.PointSize-= 2; + break; + case enumMenuItem.SymbolOffsetTL: + vectorLyr.Style.SymbolOffset = new PointF(vectorLyr.Style.SymbolOffset.X - 10f, vectorLyr.Style.SymbolOffset.Y - 10f); + break; + case enumMenuItem.SymbolOffsetTR: + vectorLyr.Style.SymbolOffset = new PointF(vectorLyr.Style.SymbolOffset.X + 10f, vectorLyr.Style.SymbolOffset.Y - 10f); + break; + case enumMenuItem.SymbolOffsetNone: + vectorLyr.Style.SymbolOffset = new PointF(0, 0); + break; + case enumMenuItem.SymbolOffsetBL: + vectorLyr.Style.SymbolOffset = new PointF(vectorLyr.Style.SymbolOffset.X - 10f, vectorLyr.Style.SymbolOffset.Y + 10f); + break; + case enumMenuItem.SymbolOffsetBR: + vectorLyr.Style.SymbolOffset = new PointF(vectorLyr.Style.SymbolOffset.X + 10f, vectorLyr.Style.SymbolOffset.Y + 10f); + break; + case enumMenuItem.AlignHz: + if (vectorLyr.Theme == null) + PopulateGeomFeatureLayer(vectorLyr, 0); + else + PopulateCharacterPointSymbolizerLayer((GeometryFeatureProvider)vectorLyr.DataSource, 0); + break; + case enumMenuItem.AlignVt: + if (vectorLyr.Theme == null) + PopulateGeomFeatureLayer(vectorLyr, 1); + else + PopulateCharacterPointSymbolizerLayer((GeometryFeatureProvider)vectorLyr.DataSource, 1); + break; + case enumMenuItem.AlignDiagonal: + if (vectorLyr.Theme == null) + PopulateGeomFeatureLayer(vectorLyr, 2); + else + PopulateCharacterPointSymbolizerLayer((GeometryFeatureProvider)vectorLyr.DataSource, 2); + break; + + default: + break; + } + + this.mb.Refresh(); + + } + #endregion + + private void InitTreeView() + { + var font = new System.Drawing.Font(tv.Font.FontFamily, tv.Font.Size, System.Drawing.FontStyle.Bold); + + tv.Nodes.Add(new TreeNode("Variable Layers") {NodeFont = font}); + tv.Nodes.Add(new TreeNode("Map Layers") { NodeFont = font }); + tv.Nodes.Add(new TreeNode("Background Layers") { NodeFont = font }); + + // Populate Tree View + TreeViewAddLayerNode(tv.Nodes[0], this.mb.Map.VariableLayers); + TreeViewAddLayerNode(tv.Nodes[1], this.mb.Map.Layers); + TreeViewAddLayerNode(tv.Nodes[2], this.mb.Map.BackgroundLayer); + + this.tv.CheckBoxes = true; + this.tv.ShowRootLines = false; + this.tv.ExpandAll(); + } + + private void TreeViewAddLayerNode(TreeNode parentNode, LayerCollection collection) + { + foreach (var lyr in collection) + TreeViewAddLayerNode(parentNode, lyr); + } + + private void TreeViewAddLayerNode(TreeNode parentNode, ILayer layer) + { + var node = new TreeNode(layer.LayerName) { Tag = layer, Checked = layer.Enabled }; + parentNode.Nodes.Add(node); + + var lyrGrp = layer as LayerGroup; + if (lyrGrp != null) + foreach (var lyr in lyrGrp.Layers) + TreeViewAddLayerNode(node, lyr); + } + + private void InitMap() + { + this.mb.Map = new SharpMap.Map() + { + SRID = 3857, + BackColor = System.Drawing.Color.AliceBlue, + }; + + //Lisbon... + var mathTransform = LayerTools.Wgs84toGoogleMercator.MathTransform; + var geom = GeometryTransform.TransformBox( + new Envelope(-9.205626, -9.123736, 38.690993, 38.740837), + mathTransform); + //geom.ExpandBy(2500); + this.mb.Map.ZoomToBox(geom); + } + + private void InitVariableLayers() + { + LayerGroup lyrGrp = null; + VectorLayer lyr = null; + + // group layer with single target + labels + lyrGrp = new LayerGroup("Fast Boats Group"); + lyr = CreateGeometryFeatureProviderLayer("Fast Boats", new[] { + new System.Data.DataColumn("Name",typeof(string)), + new System.Data.DataColumn("Heading",typeof(float)), + new System.Data.DataColumn("Scale",typeof(float)), + new System.Data.DataColumn("ARGB",typeof(int)) + }); + lyr.Style.PointColor = new SolidBrush(Color.Green); + _fastBoats = new MovingObjects(100, 50, lyr, this.mb.Map, 0.8f, Color.Green); + _fastBoats.AddObject("Fast 1", GetRectangleCenter(MapDecorationAnchor.LeftTop)); + InitRasterPointSymbolizer(lyr, 0); + lyrGrp.Layers.Add(lyr); + lyrGrp.Layers.Add(CreateLabelLayer(lyr, "Name", false)); + this.mb.Map.VariableLayers.Add(lyrGrp); + + // group layer with multiple targets + labels + lyrGrp = new LayerGroup("Medium Boats Group"); + lyr = CreateGeometryFeatureProviderLayer("Medium Boats", new[] { + new System.Data.DataColumn("Name",typeof(string)), + new System.Data.DataColumn("Heading",typeof(float)), + new System.Data.DataColumn("Scale",typeof(float)), + new System.Data.DataColumn("ARGB",typeof(int)) + }); + lyr.Style.PointColor = new SolidBrush(Color.Yellow); + _mediumBoats = new MovingObjects(1000, 100, lyr, this.mb.Map, 1, Color.Yellow); + _mediumBoats.AddObject("Boat 1", GetRectangleCenter(MapDecorationAnchor.RightTop)); + _mediumBoats.AddObject("Boat 2", GetRectangleCenter(MapDecorationAnchor.RightCenter)); + InitRasterPointSymbolizer(lyr, 1); + lyrGrp.Layers.Add(lyr); + lyrGrp.Layers.Add(CreateLabelLayer(lyr, "Name", false)); + this.mb.Map.VariableLayers.Add(lyrGrp); + + // group layer with multiple targets + labels + lyrGrp = new LayerGroup("Slow Boats Group"); + lyr = CreateGeometryFeatureProviderLayer("Slow Boats", new[] { + new System.Data.DataColumn("Name",typeof(string)), + new System.Data.DataColumn("Heading",typeof(float)), + new System.Data.DataColumn("Scale",typeof(float)), + new System.Data.DataColumn("ARGB",typeof(int)) + }); + // raster point symbolizer + lyr.Style.PointColor = new SolidBrush(Color.Red); + _slowBoats = new MovingObjects(2000, 100, lyr, this.mb.Map, 1.2f, Color.Red); + _slowBoats.AddObject("Slow 1", GetRectangleCenter(MapDecorationAnchor.LeftBottom)); + _slowBoats.AddObject("Slow 2", GetRectangleCenter(MapDecorationAnchor.CenterBottom)); + _slowBoats.AddObject("Slow 3", GetRectangleCenter(MapDecorationAnchor.RightBottom)); + InitRasterPointSymbolizer(lyr, 1); + lyrGrp.Layers.Add(lyr); + lyrGrp.Layers.Add(CreateLabelLayer(lyr, "Name", true)); + this.mb.Map.VariableLayers.Add(lyrGrp); + } + + private ILayer CreateLabelLayer(VectorLayer lyr, string column, bool enabled) + { + var lblLayer = new LabelLayer( lyr.LayerName + "Labels"); + lblLayer.DataSource = lyr.DataSource; + lblLayer.LabelColumn = column; + lblLayer.SRID = lblLayer.SRID; + lblLayer.Enabled = enabled; + return lblLayer; + } + + private void InitLayers() + { + LayerGroup lyrGrp = null; + VectorLayer lyr = null; + Geometry[] geoms = null; + + // group layer with 2 child layers (Blue Rect, Red Rect) + lyrGrp = new LayerGroup("Layer Group 1"); + + geoms = new Geometry[] { new LineString(GetRectanglePoints(MapDecorationAnchor.LeftTop)) }; + lyrGrp.Layers.Add(CreateGeomLayer("Blue Rectangle", geoms, System.Drawing.Color.DodgerBlue)); + + geoms = new Geometry[] { new LineString(GetRectanglePoints(MapDecorationAnchor.CenterTop)) }; + lyrGrp.Layers.Add(CreateGeomLayer("Red Rectangle", geoms, System.Drawing.Color.Red)); + this.mb.Map.Layers.Add(lyrGrp); + + // layer with Green Rect + geoms = new Geometry[] { new LineString(GetRectanglePoints(MapDecorationAnchor.RightTop)) }; + lyr = CreateGeomLayer("Green Rectangle", geoms, System.Drawing.Color.Green); + this.mb.Map.Layers.Add(lyr); + + // Point layer with basic Vector Style + geoms = new Geometry[] { + GetRectangleCenter(MapDecorationAnchor.LeftTop), + GetRectangleCenter(MapDecorationAnchor.Center), + GetRectangleCenter(MapDecorationAnchor.CenterBottom), + }; + lyr = CreateGeomLayer("Points with Vector Style", geoms, System.Drawing.Color.Transparent); + //lyr.Style.SymbolOffset = new System.Drawing.PointF(20,20); + lyr.Enabled = false; + this.mb.Map.Layers.Add(lyr); + + // Char Symbol Layer with Thematic rendering + lyr = CreateGeometryFeatureProviderLayer("Points with thematic CPS", new [] { + new System.Data.DataColumn("CharIndex",typeof(int)), + new System.Data.DataColumn("CharSize",typeof(float)), + new System.Data.DataColumn("OffsetX",typeof(float)), + new System.Data.DataColumn("OffsetY", typeof(float)) + }); + PopulateCharacterPointSymbolizerLayer((GeometryFeatureProvider)lyr.DataSource, 0); + lyr.Theme = new SharpMap.Rendering.Thematics.CustomTheme(GetCharacterPointStyle); + lyr.Enabled = false; + this.mb.Map.Layers.Add(lyr); + } + + private void InitBackground() + { + var lyr = new TileAsyncLayer(KnownTileSources.Create(KnownTileSource.BingRoads), "Async TileLayer [Bing]"); + lyr.SRID = 3857; + this.mb.Map.BackgroundLayer.Add(lyr); + } + + private void InitRasterPointSymbolizer(VectorLayer lyr, int style) + { + if (style == 1) + lyr.Theme = new SharpMap.Rendering.Thematics.CustomTheme(GetRasterPointSymbolizerStyle); + else + lyr.Theme = null; + } + + public static VectorStyle GetRasterPointSymbolizerStyle(FeatureDataRow row) + { + // NB - this is for testing only. + var rps = new RasterPointSymbolizer() + { + Symbol = (Image)_boat.Clone(), + Rotation = (float)row[2], + RemapColor = Color.White, + Scale = (float) row[3], + SymbolColor = Color.FromArgb((int) row[4]) + }; + return new VectorStyle() {PointSymbolizer = rps}; + } + + private void PopulateGeomFeatureLayer(VectorLayer lyr, int direction) + { + var geoms = new Geometry[] { + GetRectangleCenter(MapDecorationAnchor.LeftTop), + GetRectangleCenter(direction == 0 ? MapDecorationAnchor.CenterTop : direction == 1 ? MapDecorationAnchor.LeftCenter : MapDecorationAnchor.Center), + GetRectangleCenter(direction == 0 ? MapDecorationAnchor.RightTop : direction == 1 ? MapDecorationAnchor.LeftBottom : MapDecorationAnchor.RightBottom) + }; + + var gp = (GeometryProvider)lyr.DataSource; + gp.Geometries.Clear(); + foreach (var geom in geoms) + gp.Geometries.Add(geom); + + } + private void PopulateCharacterPointSymbolizerLayer(GeometryFeatureProvider fp, int direction) + { + FeatureDataRow fdr = null; + + fp.Features.Clear(); + + fdr = fp.Features.NewRow(); + fdr["CharIndex"] = 171; + fdr["CharSize"] = 15f; + fdr["OffsetX"] = -10f; + fdr["OffsetY"] = 5f; + fdr.Geometry = GetRectangleCenter(direction == 0 ? MapDecorationAnchor.LeftBottom : direction == 1 ? MapDecorationAnchor.RightBottom : MapDecorationAnchor.LeftBottom); + fp.Features.AddRow(fdr); + + fdr = fp.Features.NewRow(); + fdr["CharIndex"] = 176; + fdr["CharSize"] = 20f; + fdr["OffsetX"] = -10f; + fdr["OffsetY"] = 5f; + fdr.Geometry = GetRectangleCenter(direction == 0 ? MapDecorationAnchor.CenterBottom : direction == 1 ? MapDecorationAnchor.RightCenter : MapDecorationAnchor.Center); + fp.Features.AddRow(fdr); + + fdr = fp.Features.NewRow(); + fdr["CharIndex"] = 181; + fdr["CharSize"] = 25f; + fdr["OffsetX"] = -15f; + fdr["OffsetY"] = -10f; + fdr.Geometry = GetRectangleCenter(direction == 0 ? MapDecorationAnchor.RightBottom : MapDecorationAnchor.RightTop); + fp.Features.AddRow(fdr); + + fp.Features.AcceptChanges(); + } + private VectorLayer CreateGeometryFeatureProviderLayer(string name, System.Data.DataColumn[] columns) + { + var fdt = new FeatureDataTable(); + fdt.Columns.Add("Oid", typeof(uint)); + var con = new System.Data.UniqueConstraint(fdt.Columns[0]); + con.Columns[0].AutoIncrement = true; + con.Columns[0].AutoIncrementSeed = 1000; + fdt.Constraints.Add(con); + fdt.PrimaryKey = new System.Data.DataColumn[]{fdt.Columns[0]}; + + fdt.Columns.AddRange(columns); + + return new VectorLayer(name, new GeometryFeatureProvider(fdt)); + } + + private VectorLayer CreateGeomLayer(string name, IGeometry[] geometries, System.Drawing.Color lineColor) + { + var lyr = new VectorLayer(name) + { + DataSource = new GeometryProvider(geometries), + SRID = 3857 + }; + lyr.Style.Line.Color = lineColor; + lyr.Style.Line.Width = 2f; + + return lyr; + } + + private Coordinate[] GetRectanglePoints(MapDecorationAnchor anchor) + { + var env = this.mb.Map.Envelope; + env.ExpandBy(-env.Width * 0.05); + + var coords = new Coordinate[5]; + + Coordinate tl = null; + switch (anchor) + { + case MapDecorationAnchor.LeftTop: + tl = env.TopLeft(); + break; + case MapDecorationAnchor.LeftCenter: + tl = new Coordinate(env.MinX, env.MaxY - env.Height * 0.375); + break; + case MapDecorationAnchor.LeftBottom: + tl = new Coordinate(env.MinX, env.MinY + env.Height * 0.25); + break; + case MapDecorationAnchor.CenterTop: + tl = new Coordinate(env.Centre.X - env.Width * 0.125, env.MaxY); + break; + case MapDecorationAnchor.CenterBottom: + tl = new Coordinate(env.Centre.X - env.Width * 0.125, env.MinY + env.Height * 0.25); + break; + case MapDecorationAnchor.RightTop: + tl = new Coordinate(env.MaxX - env.Width * 0.25, env.MaxY); + break; + case MapDecorationAnchor.RightCenter: + tl = new Coordinate(env.MaxX - env.Width * 0.25, env.MaxY - env.Height * 0.375); + break; + case MapDecorationAnchor.RightBottom: + tl = new Coordinate(env.MaxX - env.Width * 0.25, env.MinY + env.Height * 0.25); + break; + default: + tl = new Coordinate(env.Centre.X - env.Width * 0.125, env.Centre.Y + env.Height * 0.125); + break; + } + + coords[0] = tl; + coords[1] = new Coordinate(tl.X + env.Width * 0.25, coords[0].Y); + coords[2] = new Coordinate(coords[1].X, tl.Y - env.Height * 0.25); + coords[3] = new Coordinate(coords[0].X, coords[2].Y); + coords[4] = tl; + + return coords; + } + + private Point GetRectangleCenter(MapDecorationAnchor anchor) + { + var coords = GetRectanglePoints(anchor); + return new Point((coords[0].X + coords[1].X) / 2.0, + (coords[0].Y + coords[2].Y) / 2.0); + } + + public static VectorStyle GetCharacterPointStyle(FeatureDataRow row) + { + var cps = new CharacterPointSymbolizer(); + cps.CharacterIndex = (int)row[1]; + cps.Font = new System.Drawing.Font("Wingdings", (float)row[2]); + cps.Offset = new System.Drawing.PointF((float)row[3], (float)row[4]); + return new VectorStyle() {PointSymbolizer = cps}; + } + } + + public class MovingObjects : IDisposable + { + private List _movingObjects = new List(); + + private readonly Timer _timer; + private readonly VectorLayer _lyr; + private readonly Map _map; + public int StepSize { get; set; } + private float _scale; + private Color _color; + + public MovingObjects(int interval, int stepSize, VectorLayer lyr, Map map, float scale, Color color) + { + _timer = new Timer(); + _timer.Tick += new EventHandler(Timer_Tick); + _timer.Interval = interval; + + StepSize = stepSize; + _lyr = lyr; + _map = map; + _scale = scale; + _color = color; + } + + public void Start() => _timer.Enabled = true; + public void Stop() => _timer.Enabled = false; + public bool IsRunning => _timer.Enabled; + + public void AddObject(string name, Point startAt) + { + lock (((ICollection) _movingObjects).SyncRoot) + { + var fp = (GeometryFeatureProvider) _lyr.DataSource; + var fdr = fp.Features.NewRow(); + fdr[1] = name; + fdr[2] = 0; + fdr[3] = _scale; + fdr[4] = _color.ToArgb(); + fdr.Geometry = startAt; + fp.Features.AddRow(fdr); + fp.Features.AcceptChanges(); + + var obj = new MovingObject(Convert.ToUInt32(fdr[0]), startAt); + _movingObjects.Add(obj); + } + } + + public bool DeleteObject(uint oid) + { + lock (((ICollection) _movingObjects).SyncRoot) + { + var obj = _movingObjects.Find(p => p.Oid == oid); + if (obj == null) return false; + + var fp = (GeometryFeatureProvider) _lyr.DataSource; + var fdr = fp.Features.Rows.Find(oid); + fp.Features.Rows.Remove(fdr); + fp.Features.AcceptChanges(); + + _movingObjects.Remove(obj); + return true; + } + } + + private void Timer_Tick(object sender, EventArgs e) + { + lock (((ICollection)_movingObjects).SyncRoot) + { + foreach (var obj in _movingObjects) + { + obj.Step(_map.Envelope, StepSize); + var fp = (GeometryFeatureProvider) _lyr.DataSource; + var fdr = (FeatureDataRow) fp.Features.Rows.Find(obj.Oid); +// //fdr.Geometry = obj.Position; + fdr[2] = obj.Heading; + fdr.AcceptChanges(); + } + } + if (_lyr.Enabled) _map.VariableLayers.TouchTimer(); + } + + public void Dispose() + { + _timer.Tick -= new EventHandler(Timer_Tick); + _timer?.Dispose(); + } + } + + public class MovingObject + { + public uint Oid { get; } + private bool _movingLeft; + private bool _movingUp; + public Point Position { get; private set; } + public float Heading { get; private set; } + + public MovingObject(uint oid, Point startAt) + { + Oid = oid; + Position = startAt; + _movingUp = true; + } + + public void Step(Envelope currentExtent, double stepSize) + { + var dx = _movingLeft ? -stepSize : stepSize; + var dy = _movingUp ? stepSize : -stepSize; + + Position.X += dx; + Position.Y += dy; + Position.GeometryChanged(); + + if (Position.X < currentExtent.MinX) + _movingLeft = false; + else if (Position.X > currentExtent.MaxX) + _movingLeft = true; + + if (Position.Y < currentExtent.MinY) + _movingUp = true; + else if (Position.Y > currentExtent.MaxY) + _movingUp = false; + + var deg = 90 - Math.Atan2(dy, dx) * 180 / Math.PI; + Heading = (float)(deg + 360) % 360; + } + } +} + diff --git a/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.resx b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.resx new file mode 100644 index 00000000..5ab3f9af --- /dev/null +++ b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 181, 17 + + \ No newline at end of file diff --git a/Examples/WinFormSamples/Properties/Resources.Designer.cs b/Examples/WinFormSamples/Properties/Resources.Designer.cs index ef66e5d8..d304440d 100644 --- a/Examples/WinFormSamples/Properties/Resources.Designer.cs +++ b/Examples/WinFormSamples/Properties/Resources.Designer.cs @@ -1,113 +1,125 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace WinFormSamples.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinFormSamples.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap OutfallSmall { - get { - object obj = ResourceManager.GetObject("OutfallSmall", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap PumpSmall { - get { - object obj = ResourceManager.GetObject("PumpSmall", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap view_left_right { - get { - object obj = ResourceManager.GetObject("view_left_right", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap view_top_bottom { - get { - object obj = ResourceManager.GetObject("view_top_bottom", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap ZoomToExtents1 { - get { - object obj = ResourceManager.GetObject("ZoomToExtents1", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WinFormSamples.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinFormSamples.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap OutfallSmall { + get { + object obj = ResourceManager.GetObject("OutfallSmall", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap PumpSmall { + get { + object obj = ResourceManager.GetObject("PumpSmall", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap vessel_01 + { + get + { + object obj = ResourceManager.GetObject("vessel_01", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap view_left_right { + get { + object obj = ResourceManager.GetObject("view_left_right", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap view_top_bottom { + get { + object obj = ResourceManager.GetObject("view_top_bottom", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ZoomToExtents1 { + get { + object obj = ResourceManager.GetObject("ZoomToExtents1", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Examples/WinFormSamples/Properties/Resources.resx b/Examples/WinFormSamples/Properties/Resources.resx index 488a065e..48cf9e6c 100644 --- a/Examples/WinFormSamples/Properties/Resources.resx +++ b/Examples/WinFormSamples/Properties/Resources.resx @@ -1,136 +1,139 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Resources\view_left_right.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\view_top_bottom.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\ZoomToExtents1.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\OutfallSmall.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Resources\PumpSmall.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\view_left_right.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\view_top_bottom.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\ZoomToExtents1.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\OutfallSmall.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\PumpSmall.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\vessel_01.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/Examples/WinFormSamples/Resources/vessel_01.png b/Examples/WinFormSamples/Resources/vessel_01.png new file mode 100644 index 0000000000000000000000000000000000000000..dd4848297527752ebfc4d555198b9183bcaa0035 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJtM=93Yk-Zrn-8%IEGZjy`6Qj?~nlx%g-zSJni?kD>vzF*tK}g+*JnJyz9g!KB#<` zdV8Dm4mG=XHg)#gkzvKr6(<6PQX)H}_C4p|+_dJ=`lr=t-V;(;*X!Ij4sO!fd~)@uYdiuL;vG!NuiXPM|Yq8erAHgX`Ovh4ZCanx3Wa0eYncBY1;dhQxv?# zRTCno#r`>WoTI{Yx!8hf{T570p+YGy8yHxQRP<>pG%>Jj+1&m+1}ehGB+$@s`|^KB z1}3|Z19BmLx(do14i47UJI=dK;{&pKW+^Z*zJ2r1PCU{I$TG}AHDCoNlfZ>@a_@>K zE@fsC5Lm26Ky$-pjsSh{-IixhUvk{mvg#>wWST&|#pL6ki*M-JCo;3nTXN)p3oz6f NJYD@<);T3K0RZ_dvE2Xw literal 0 HcmV?d00001 From 504d539368ef563557c163130f2c8bce51b8f525 Mon Sep 17 00:00:00 2001 From: Tim C Date: Thu, 9 Jan 2020 19:51:48 +1300 Subject: [PATCH 25/51] Resolve LayerGroup visibility hierarchy, and modify CancellationToken --- .../ImageGenerator/LayerListImageGenerator.cs | 125 ++++++++++-------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs index bdfad372..64962a5b 100644 --- a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs +++ b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs @@ -3,9 +3,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; +using System.Data.SqlClient; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -81,11 +83,22 @@ public void Dispose() } } + private class LayerInfo + { + public ILayer Layer { get; private set; } + public bool Render { get; set; } + public LayerInfo(ILayer layer, bool render) + { + Layer = layer; + Render = render; + } + } + private static readonly ILog _logger = LogManager.GetLogger(); private ProgressBar _progressBar; private readonly ConcurrentDictionary _imageLayers; - private readonly List _layers; + //private readonly List _layerInfos; private readonly object _paintImageLock = new object(); private Bitmap _paintImage; @@ -106,7 +119,7 @@ public void Dispose() public LayerListImageGenerator(MapBox mapBox, ProgressBar progressBar) { _progressBar = progressBar; - _layers = new List(); + //_layerInfos = new List(); _imageLayers = new ConcurrentDictionary(); NeedsUpdate = true; MapBox = mapBox; @@ -163,40 +176,40 @@ private void HandleMapLayerCollectionChanged(object sender, NotifyCollectionChan private bool NeedsUpdate { get; set; } - IEnumerable EnumerateLayers(Map map) + private IEnumerable EnumerateLayers(Map map) { - foreach (var lyr in EnumerateLayers(map.BackgroundLayer)) - yield return lyr; - foreach (var lyr in EnumerateLayers(map.Layers)) - yield return lyr; - foreach (var lyr in EnumerateLayers(map.VariableLayers)) - yield return lyr; + foreach (var lyrInfo in EnumerateLayers(map.BackgroundLayer)) + yield return lyrInfo; + foreach (var lyrInfo in EnumerateLayers(map.Layers)) + yield return lyrInfo; + foreach (var lyrInfo in EnumerateLayers(map.VariableLayers)) + yield return lyrInfo; } - IEnumerable EnumerateLayers(LayerCollection collection) + private IEnumerable EnumerateLayers(LayerCollection collection) { var coll = (ICollection) collection; lock (coll.SyncRoot) { foreach (var lyr in collection) { - foreach (var tmpLyr in EnumerateLayers(lyr)) - yield return tmpLyr; + foreach (var lyrInfo in EnumerateLayers(lyr, lyr.Enabled)) + yield return lyrInfo; } } } - IEnumerable EnumerateLayers(ILayer lyr) + private IEnumerable EnumerateLayers(ILayer lyr, bool hierarchyEnabled) { if (lyr is LayerGroup lyrGroup) { foreach (var tmpLyr in lyrGroup.Layers) - foreach (var tmpLyr2 in EnumerateLayers(tmpLyr)) + foreach (var tmpLyr2 in EnumerateLayers(tmpLyr, lyrGroup.Enabled && tmpLyr.Enabled)) yield return tmpLyr2; } else { - yield return lyr; + yield return new LayerInfo(lyr, hierarchyEnabled);; } } @@ -210,9 +223,11 @@ private void HandleVariableLayersRequery(object sender, EventArgs e) return; var mvp = new MapViewport(map); - foreach (var lyr in Map.VariableLayers) + var token = _cts.Token; + foreach (var lyrInfo in EnumerateLayers(Map.VariableLayers)) { - ThreadPool.QueueUserWorkItem(delegate { RenderLayerImage(new object[] { lyr, mvp, _cts.Token, false }); }); + if (!lyrInfo.Render) continue; + ThreadPool.QueueUserWorkItem(delegate { RenderLayerImage(new object[] { lyrInfo.Layer, mvp, token, false }); }); } } @@ -274,9 +289,9 @@ private void UnwireMap() private void ClearCache() { - foreach (var layer in _layers) + foreach (var key in _imageLayers.Keys) { - if (_imageLayers.TryRemove(layer, out var lockImg)) + if (_imageLayers.TryRemove(key, out var lockImg)) { lock (lockImg.Sync) { @@ -297,7 +312,6 @@ private void ClearCache() _pendingDownloadTracker.ProgressChanged += HandleProgressChanged; _imageLayers.Clear(); - _layers.Clear(); } private void HandleProgressChanged(int tilesRemaining) @@ -349,6 +363,7 @@ public Image ImageValue { res = new Bitmap(MapBox.Width, MapBox.Height); + using (var gr = Graphics.FromImage(res)) { gr.Clear(Map.BackColor); @@ -356,26 +371,24 @@ public Image ImageValue { double mapZoom = Map.Zoom; double mapScale = Map.MapScale; - for (int i = 0; i < _layers.Count; i++) + // select layers to be rendered (ie Layers that are visible, + // excluding children of LayerGroup that is not visible) + var layers = EnumerateLayers(Map).Where(li => li.Render).Select(l => l.Layer).ToList();; + + for (int i = 0; i < layers.Count; i++) { - var lyr = _layers[i]; - - // Does the layer need rendering - if (!lyr.Enabled) continue; + var lyr = layers[i]; // Does it fit in the visibility constraints double mapCompare = lyr.VisibilityUnits == VisibilityUnits.Scale ? mapScale : mapZoom; if (mapCompare < lyr.MinVisible || lyr.MaxVisible < mapCompare) continue; - //lock (_imageLayers) - //{ - if (_imageLayers.TryGetValue(lyr, out var lockImg)) - { - lock (lockImg.Sync) - gr.DrawImageUnscaled(lockImg.Bitmap, 0, 0); - } - //} + if (_imageLayers.TryGetValue(lyr, out var lockImg)) + { + lock (lockImg.Sync) + gr.DrawImageUnscaled(lockImg.Bitmap, 0, 0); + } } } @@ -407,53 +420,58 @@ public void Generate() _logger.Debug(t => t("\n{0}> Enter Generate", Thread.CurrentThread.ManagedThreadId)); _cts.Cancel(); + _cts.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + var layerInfos = EnumerateLayers(Map).ToList(); + if (NeedsUpdate) { ClearCache(); - foreach (var lyr in EnumerateLayers(Map)) + foreach (var lyrInfo in layerInfos) { - if (lyr is ITileAsyncLayer asyncLyr) + if (lyrInfo.Layer is ITileAsyncLayer asyncLyr) _pendingDownloadTracker.Add(asyncLyr); - _layers.Add(lyr); } NeedsUpdate = false; } + if (token.IsCancellationRequested) return; + var map = MapBox.Map; double mapZoom = map.Zoom; double mapScale = map.MapScale; var mvp = new MapViewport(map); - for (int i = 0; i < _layers.Count; i++) + // select layers to be rendered (ie Layers that are visible, + // excluding children of LayerGroup that is not visible) + var layers = layerInfos.Where(li => li.Render).Select(l => l.Layer).ToList(); + + for (int i = 0; i < layers.Count; i++) { // Has an cancellation ben requested - if (_cts.IsCancellationRequested) - break; + if (token.IsCancellationRequested) return; // Get the layer - var lyr = _layers[i]; - - // Does the layer need rendering - if (!lyr.Enabled) continue; + var lyr = layers[i]; // Layer has no envelope, it has no data if (lyr.Envelope?.IsNull ?? false) continue; - + // Does it fit in the visibility constraints - double mapCompare = lyr.VisibilityUnits == VisibilityUnits.Scale ? mapScale : mapZoom; + double mapCompare = lyr.VisibilityUnits == VisibilityUnits.Scale ? mapScale : mapZoom; if (mapCompare < lyr.MinVisible || lyr.MaxVisible < mapCompare) continue; // Add task to list - bool invalidateAll = i == _layers.Count - 1; - ThreadPool.QueueUserWorkItem(delegate { RenderLayerImage(new object[] {lyr, mvp, _cts.Token, invalidateAll}); }); + bool invalidateAll = i == layers.Count - 1; + ThreadPool.QueueUserWorkItem(delegate {RenderLayerImage(new object[] {lyr, mvp, token, invalidateAll});}); } - if (_cts.IsCancellationRequested) - return; + if (token.IsCancellationRequested) return; ImageEnvelope = mvp.Envelope; @@ -469,14 +487,12 @@ private Rectangle RenderLayerImage(object param) { if (IsDisposed) return Rectangle.Empty; - object[] parameters = (object[]) param; var lyr = (ILayer)parameters[0]; var mvp = (MapViewport) parameters[1]; var token = (CancellationToken) parameters[2]; - - //if ((bool)parameters[3]) MapBox.Invalidate(); - + var invalidateAll = (bool) parameters[3]; + if (token.IsCancellationRequested) return Rectangle.Empty; @@ -494,7 +510,6 @@ private Rectangle RenderLayerImage(object param) if (lyr is ITileAsyncLayer) { _imageLayers.TryGetValue(lyr, out img); - //return Rectangle.Empty; } // Update size if necessary @@ -580,7 +595,7 @@ private Rectangle RenderLayerImage(object param) { _logger.Debug(t => t("\n{0}> Invalidating rectangle {1}",Thread.CurrentThread.ManagedThreadId, updateRect)); InvalidateCacheImage(); - if ((bool) parameters[3]) + if (invalidateAll) mapBox.Invalidate(); else mapBox.Invalidate(updateRect); From 028dfa342a1f2f20f1a8f8ee829513dc5c4d5e24 Mon Sep 17 00:00:00 2001 From: Tim C Date: Tue, 14 Jan 2020 22:15:46 +1300 Subject: [PATCH 26/51] ILayerEx with partial implementation for LabelLayer --- .../FormInteractiveVectorLayerRendering.cs | 43 +- .../ImageGenerator/LayerListImageGenerator.cs | 222 +- SharpMap/Layers/ILayerEx.cs | 16 + SharpMap/Layers/LabelLayer.cs | 2114 +++++++++-------- SharpMap/Layers/Layer.cs | 1139 ++++----- SharpMap/Layers/VectorLayer.cs | 1347 ++++++----- SharpMap/Rendering/VectorRenderer.cs | 1511 ++++++------ 7 files changed, 3258 insertions(+), 3134 deletions(-) create mode 100644 SharpMap/Layers/ILayerEx.cs diff --git a/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs index 893b4ba5..3f4b42ee 100644 --- a/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs +++ b/Examples/WinFormSamples/FormInteractiveVectorLayerRendering.cs @@ -49,7 +49,9 @@ private enum enumMenuItem SymbolOffsetBR, // Basic Vector Style Point Size AlignHz, // Regenerate Map Point layer data AlignVt, // Regenerate Map Point layer data - AlignDiagonal // Regenerate Map Point layer data + AlignDiagonal , // Regenerate Map Point layer data + IncrementLineWidth, // Rectangle layers + DecrementLineWidth // Rectangle layers } public FormLayerListImageGenerator() @@ -68,7 +70,6 @@ private void FormLayerListImageGenerator_Load(object sender, System.EventArgs e) InitLayers(); InitVariableLayers(); InitTreeView(); - this.mb.Refresh(); using (var renderer = SharpMap.Forms.MapBox.MapImageGeneratorFunction(new SharpMap.Forms.MapBox(), null)) { @@ -77,10 +78,12 @@ private void FormLayerListImageGenerator_Load(object sender, System.EventArgs e) else this.txtImgGeneration.Text = (this.txtImgGeneration.Text + "\n. LayerListImageGenerator"); }; - + + this.mb.Refresh(); + //_slowBoats?.Start(); _mediumBoats?.Start(); - _fastBoats?.Start(); + //_fastBoats?.Start(); } private void FormLayerListImageGenerator_Closing(object sender, EventArgs e) @@ -157,16 +160,23 @@ private void tv_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) cm.MenuItems.Add(CreateMenuItem(enumMenuItem.IncreaseSymbolSize, "Increment symbol size")); cm.MenuItems.Add(CreateMenuItem(enumMenuItem.DecreaseSymbolSize, "Decrement symbol size")); cm.MenuItems.Add(new MenuItem("-")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetNone, "Remove Symbol Offset")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTL, "Offset Step Top Left")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTR, "Offset Step Top Right")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBL, "Offset Step Bottom Left")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBR, "Offset Step Bottom Right")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetNone, "Remove symbol offset")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTL, "Offset step upper left")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetTR, "Offset step upper right")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBL, "Offset Step lower left")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.SymbolOffsetBR, "Offset step lower right")); } if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignHz, "Align Horizontal")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignVt, "Align Vertical")); - cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignDiagonal, "Align Diagonal")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignHz, "Align Pts Horizontal")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignVt, "Align Pts Vertical")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.AlignDiagonal, "Align Pts Diagonal")); + } + + if (vlyr.LayerName.Contains("Rect")) + { + if (cm.MenuItems.Count > 0) cm.MenuItems.Add(new MenuItem("-")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.IncrementLineWidth, "Increment line width")); + cm.MenuItems.Add(CreateMenuItem(enumMenuItem.DecrementLineWidth, "Decrement line width")); } } @@ -275,6 +285,12 @@ private void MenuItemClick (Object sender, EventArgs e) else PopulateCharacterPointSymbolizerLayer((GeometryFeatureProvider)vectorLyr.DataSource, 2); break; + case enumMenuItem.IncrementLineWidth: + vectorLyr.Style.Line.Width += 1; + break; + case enumMenuItem.DecrementLineWidth: + vectorLyr.Style.Line.Width -= vectorLyr.Style.Line.Width <= 1 ? 0 : 1; + break; default: break; @@ -397,9 +413,10 @@ private void InitVariableLayers() private ILayer CreateLabelLayer(VectorLayer lyr, string column, bool enabled) { - var lblLayer = new LabelLayer( lyr.LayerName + "Labels"); + var lblLayer = new LabelLayer( lyr.LayerName + " Labels"); lblLayer.DataSource = lyr.DataSource; lblLayer.LabelColumn = column; + lblLayer.Style.BackColor = Brushes.LightPink; lblLayer.SRID = lblLayer.SRID; lblLayer.Enabled = enabled; return lblLayer; diff --git a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs index 64962a5b..21f27943 100644 --- a/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs +++ b/SharpMap.UI/Forms/ImageGenerator/LayerListImageGenerator.cs @@ -97,7 +97,7 @@ public LayerInfo(ILayer layer, bool render) private static readonly ILog _logger = LogManager.GetLogger(); private ProgressBar _progressBar; - private readonly ConcurrentDictionary _imageLayers; + private readonly ConcurrentDictionary _imageLayers; //private readonly List _layerInfos; private readonly object _paintImageLock = new object(); @@ -120,7 +120,7 @@ public LayerListImageGenerator(MapBox mapBox, ProgressBar progressBar) { _progressBar = progressBar; //_layerInfos = new List(); - _imageLayers = new ConcurrentDictionary(); + _imageLayers = new ConcurrentDictionary(); NeedsUpdate = true; MapBox = mapBox; WireMapBox(); @@ -223,8 +223,10 @@ private void HandleVariableLayersRequery(object sender, EventArgs e) return; var mvp = new MapViewport(map); + var token = _cts.Token; - foreach (var lyrInfo in EnumerateLayers(Map.VariableLayers)) + var lyrInfos = EnumerateLayers(Map.VariableLayers); + foreach (var lyrInfo in lyrInfos) { if (!lyrInfo.Render) continue; ThreadPool.QueueUserWorkItem(delegate { RenderLayerImage(new object[] { lyrInfo.Layer, mvp, token, false }); }); @@ -245,7 +247,7 @@ private void HandleMapNewTileAvaliable(ITileAsyncLayer sender, Envelope box, Bit if (IsDisposed) return; - if (!_imageLayers.TryGetValue((ILayer)sender, out var lockImg)) + if (!_imageLayers.TryGetValue((ILayerEx)sender, out var lockImg)) return; var min = Point.Round(Map.WorldToImage(box.Min())); @@ -377,7 +379,7 @@ public Image ImageValue { for (int i = 0; i < layers.Count; i++) { - var lyr = layers[i]; + var lyr = (ILayerEx)layers[i]; // Does it fit in the visibility constraints double mapCompare = lyr.VisibilityUnits == VisibilityUnits.Scale ? mapScale : mapZoom; @@ -485,137 +487,135 @@ public void Generate() /// private Rectangle RenderLayerImage(object param) { - if (IsDisposed) return Rectangle.Empty; + if (IsDisposed) return Rectangle.Empty; - object[] parameters = (object[]) param; - var lyr = (ILayer)parameters[0]; - var mvp = (MapViewport) parameters[1]; - var token = (CancellationToken) parameters[2]; - var invalidateAll = (bool) parameters[3]; + object[] parameters = (object[]) param; + var lyr = (ILayerEx)parameters[0]; + var mvp = (MapViewport) parameters[1]; + var token = (CancellationToken) parameters[2]; + var invalidateAll = (bool) parameters[3]; - if (token.IsCancellationRequested) - return Rectangle.Empty; + if (token.IsCancellationRequested) + return Rectangle.Empty; - if (lyr.Envelope == null || lyr.Envelope.Width == 0d) - return Rectangle.Empty; + var updateRect = Rectangle.Empty; - var updateRect = Rectangle.Empty; + var sw = new Stopwatch(); + _logger.Debug(t => t("\n{0}> Enter RenderLayerImage {1}\n{0}>\t{2} => {3}", + Thread.CurrentThread.ManagedThreadId, lyr.LayerName, mvp.Envelope, mvp.Size)); + sw.Start(); - var sw = new Stopwatch(); - _logger.Debug(t => t("\n{0}> Enter RenderLayerImage {1}\n{0}>\t{2} => {3}", - Thread.CurrentThread.ManagedThreadId, lyr.LayerName, mvp.Envelope, mvp.Size)); - sw.Start(); + LockedBitmap img = null; +// if (lyr is ITileAsyncLayer) +// { + _imageLayers.TryGetValue(lyr, out img); +// } - LockedBitmap img = null; - if (lyr is ITileAsyncLayer) - { - _imageLayers.TryGetValue(lyr, out img); - } - - // Update size if necessary - if (img != null) - { - lock (img.Sync) + // Update size if necessary + if (img != null) { - var imageObj = img.Bitmap; - if (img.Bitmap != null && img.Bitmap.Size != mvp.Size) + lock (img.Sync) { - object lockObj = img.Sync; - var newLockImg = new LockedBitmap(lockObj) { - Bitmap = new Bitmap(mvp.Size.Width, mvp.Size.Height, PixelFormat.Format32bppArgb) - }; - while (!_imageLayers.TryUpdate(lyr, newLockImg, img)) - Thread.Sleep(25); - img = newLockImg; - //imageObj.Dispose(); + var imageObj = img.Bitmap; + if (img.Bitmap != null && img.Bitmap.Size != mvp.Size) + { + object lockObj = img.Sync; + var newLockImg = new LockedBitmap(lockObj) { + Bitmap = new Bitmap(mvp.Size.Width, mvp.Size.Height, PixelFormat.Format32bppArgb) + }; + while (!_imageLayers.TryUpdate(lyr, newLockImg, img)) + Thread.Sleep(25); + img = newLockImg; + //imageObj.Dispose(); + } } } - } - else //if (img == null) - { - img = new LockedBitmap { - Bitmap = new Bitmap(mvp.Size.Width, mvp.Size.Height, PixelFormat.Format32bppArgb) - }; - img = _imageLayers.AddOrUpdate(lyr, img, (u,v) => img); - } + else //if (img == null) + { + img = new LockedBitmap { + Bitmap = new Bitmap(mvp.Size.Width, mvp.Size.Height, PixelFormat.Format32bppArgb) + }; + img = _imageLayers.AddOrUpdate(lyr, img, (u,v) => img); + } - lock (img.Sync) - { - try + Envelope affectedArea; + + lock (img.Sync) { - using (var gr = Graphics.FromImage(img.Bitmap)) + try + { + using (var gr = Graphics.FromImage(img.Bitmap)) + { + gr.Clear(Color.Transparent); + lyr.Render(gr, mvp, out affectedArea); + } + } + catch (Exception) { - gr.Clear(Color.Transparent); - lyr.Render(gr, mvp); + goto Exit; } } - catch (Exception) - { - goto Exit; - } - } - if (token.IsCancellationRequested) - goto Exit; + if (token.IsCancellationRequested) + goto Exit; - // Envelope of the current layer - var lyrEnvelope = lyr.Envelope; +// // Envelope of the current layer +// var lyrEnvelope = lyr.Envelope; +// +// if (lyrEnvelope.Width == 0) +// { +// if (lyr is VectorLayer vlyr) +// { +// double pointSize = vlyr.Style.PointSize; +// if (vlyr.Style.Symbol != null) +// pointSize = vlyr.Style.Symbol.Size.Width * vlyr.Style.SymbolScale; +// pointSize = 1.05d * Map.PixelWidth * pointSize; +// lyrEnvelope.ExpandBy(pointSize); +// } +// } + + // Envelope of the last rendered image + var imgEnvelope = img.Extent.Copy(); + imgEnvelope.ExpandToInclude(affectedArea); + + var updateArea = mvp.Envelope.Intersection(imgEnvelope); + var lt = Point.Truncate( mvp.WorldToImage(updateArea.TopLeft())); + var rb = Point.Ceiling(mvp.WorldToImage(updateArea.BottomRight())); + + updateRect = Rectangle.FromLTRB(lt.X, lt.Y, rb.X, rb.Y); + + if (token.IsCancellationRequested) + goto Exit; - if (lyrEnvelope.Width == 0) - { - if (lyr is VectorLayer vlyr) + var mapBox = MapBox; + if (mapBox.IsHandleCreated) { - double pointSize = vlyr.Style.PointSize; - if (vlyr.Style.Symbol != null) - pointSize = vlyr.Style.Symbol.Size.Width * vlyr.Style.SymbolScale; - pointSize = 1.05d * Map.PixelWidth * pointSize; - lyrEnvelope.ExpandBy(pointSize); + _logger.Debug(t => t("\n{0}> Invalidating rectangle {1}",Thread.CurrentThread.ManagedThreadId, updateRect)); + InvalidateCacheImage(); + if (invalidateAll) + mapBox.Invalidate(); + else + mapBox.Invalidate(updateRect); + + //MapBox.Invoke(new MethodInvoker(() => MapBox.Invalidate(updateRect))); + if (!IsDisposed) + mapBox.Invoke(new MethodInvoker(() => { if (!mapBox.IsDisposed) mapBox.Update();})); } - } - - // Envelope of the last rendered image - var imgEnvelope = img.Extent.Copy(); - imgEnvelope.ExpandToInclude(lyrEnvelope); - - var updateArea = mvp.Envelope.Intersection(imgEnvelope); - var lt = Point.Truncate( mvp.WorldToImage(updateArea.TopLeft())); - var rb = Point.Ceiling(mvp.WorldToImage(updateArea.BottomRight())); - - if (token.IsCancellationRequested) - goto Exit; - - updateRect = Rectangle.FromLTRB(lt.X, lt.Y, rb.X, rb.Y); - // Set the image envelope - img.Extent = mvp.Envelope.Intersection(lyr.Envelope); - - - var mapBox = MapBox; - if (mapBox.IsHandleCreated) - { - _logger.Debug(t => t("\n{0}> Invalidating rectangle {1}",Thread.CurrentThread.ManagedThreadId, updateRect)); - InvalidateCacheImage(); - if (invalidateAll) - mapBox.Invalidate(); - else - mapBox.Invalidate(updateRect); - - //MapBox.Invoke(new MethodInvoker(() => MapBox.Invalidate(updateRect))); - if (!IsDisposed) - mapBox.Invoke(new MethodInvoker(() => { if (!mapBox.IsDisposed) mapBox.Update();})); - } + // Set the image envelope + img.Extent = affectedArea; // mvp.Envelope.Intersection(lyr.Envelope); - Exit: - sw.Stop(); - _logger.Debug(t => t("\n{0}> Exit RenderLayerImage {1} after {4}ms \n{0}>\t{2} => {3}", - Thread.CurrentThread.ManagedThreadId, lyr.LayerName, mvp.Envelope, mvp.Size, - sw.ElapsedMilliseconds)); - if (updateRect.IsEmpty) - _logger.Debug(t => t("\n{0}> Exit RenderLayerImage because of cancellation", + Exit: + sw.Stop(); + _logger.Debug(t => t("\n{0}> Exit RenderLayerImage {1} after {4}ms \n{0}>\t{2} => {3}", Thread.CurrentThread.ManagedThreadId, lyr.LayerName, mvp.Envelope, mvp.Size, sw.ElapsedMilliseconds)); + if (updateRect.IsEmpty) + _logger.Debug(t => t("\n{0}> Exit RenderLayerImage because of cancellation", + Thread.CurrentThread.ManagedThreadId, lyr.LayerName, mvp.Envelope, mvp.Size, + sw.ElapsedMilliseconds)); - return updateRect; + return updateRect; } private void InvalidateCacheImage() diff --git a/SharpMap/Layers/ILayerEx.cs b/SharpMap/Layers/ILayerEx.cs new file mode 100644 index 00000000..6c39cca2 --- /dev/null +++ b/SharpMap/Layers/ILayerEx.cs @@ -0,0 +1,16 @@ +using System.Drawing; +using GeoAPI.Geometries; + +namespace SharpMap.Layers +{ + public interface ILayerEx : ILayer + { + /// + /// Renders the layer using the current viewport + /// + /// Graphics object reference + /// Map which is rendered + /// The actual extent of rendered data inclusive of any labels or vector symbology + void Render(Graphics g, MapViewport map, out Envelope affectedArea); + } +} diff --git a/SharpMap/Layers/LabelLayer.cs b/SharpMap/Layers/LabelLayer.cs index ac753a11..b8a3f154 100644 --- a/SharpMap/Layers/LabelLayer.cs +++ b/SharpMap/Layers/LabelLayer.cs @@ -1,1040 +1,1074 @@ -// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. - -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Text; -using System.Globalization; -using SharpMap.Data; -using SharpMap.Data.Providers; -using GeoAPI.Geometries; -using SharpMap.Rendering; -using SharpMap.Rendering.Thematics; -using SharpMap.Styles; -using Transform = SharpMap.Utilities.Transform; -using SharpMap.Rendering.Symbolizer; - -namespace SharpMap.Layers -{ - /// - /// Label layer class - /// - /// - /// Creates a new label layer and sets the label text to the "Name" column in the FeatureDataTable of the datasource - /// - /// //Set up a label layer - /// SharpMap.Layers.LabelLayer layLabel = new SharpMap.Layers.LabelLayer("Country labels"); - /// layLabel.DataSource = layCountries.DataSource; - /// layLabel.Enabled = true; - /// layLabel.LabelColumn = "Name"; - /// layLabel.Style = new SharpMap.Styles.LabelStyle(); - /// layLabel.Style.CollisionDetection = true; - /// layLabel.Style.CollisionBuffer = new SizeF(20, 20); - /// layLabel.Style.ForeColor = Color.White; - /// layLabel.Style.Font = new Font(FontFamily.GenericSerif, 8); - /// layLabel.MaxVisible = 90; - /// layLabel.Style.HorizontalAlignment = SharpMap.Styles.LabelStyle.HorizontalAlignmentEnum.Center; - /// - /// - [Serializable] - public class LabelLayer : Layer - { - #region Delegates - - /// - /// Delegate method for creating advanced label texts - /// - /// the to build the label for - /// the label - public delegate string GetLabelMethod(FeatureDataRow fdr); - - /// - /// Delegate method for calculating the priority of label rendering - /// - /// the to compute the priority value from - /// the priority value - public delegate int GetPriorityMethod(FeatureDataRow fdr); - - /// - /// Delegate method for advanced placement of the label position - /// - /// the to compute the label position from - /// the priority value - public delegate Coordinate GetLocationMethod(FeatureDataRow fdr); - #endregion - - #region MultipartGeometryBehaviourEnum enum - - /// - /// Labelling behaviour for Multipart geometry collections - /// - public enum MultipartGeometryBehaviourEnum - { - /// - /// Place label on all parts (default) - /// - All, - /// - /// Place label on object which the greatest length or area. - /// - /// - /// Multipoint geometries will default to - /// - Largest, - /// - /// The center of the combined geometries - /// - CommonCenter, - /// - /// Center of the first geometry in the collection (fastest method) - /// - First - } - - #endregion - - private IBaseProvider _dataSource; - private GetLabelMethod _getLabelMethod; - private GetPriorityMethod _getPriorityMethod; - private GetLocationMethod _getLocationMethod; - private Envelope _envelope; - - /// - /// Name of the column that holds the value for the label. - /// - private string _labelColumn; - - /// - /// Delegate for custom Label Collision Detection - /// - private LabelCollisionDetection.LabelFilterMethod _labelFilter; - - /// - /// A value indication the priority of the label in cases of label-collision detection - /// - private int _priority; - - /// - /// Name of the column that contains the value indicating the priority of the label in case of label-collision detection - /// - private string _priorityColumn = ""; - - /// - /// Name of the column that contains the value indicating the rotation value of the label - /// - private string _rotationColumn; - - - private TextRenderingHint _textRenderingHint; - - private ITheme _theme; - - /// - /// Creates a new instance of a LabelLayer - /// - public LabelLayer(string layername) - :base(new LabelStyle()) - { - //_Style = new LabelStyle(); - LayerName = layername; - SmoothingMode = SmoothingMode.AntiAlias; - TextRenderingHint = TextRenderingHint.AntiAlias; - MultipartGeometryBehaviour = MultipartGeometryBehaviourEnum.All; - _labelFilter = LabelCollisionDetection.SimpleCollisionDetection; - } - - /// - /// Gets or sets labelling behavior on multipart geometries - /// - /// Default value is - public MultipartGeometryBehaviourEnum MultipartGeometryBehaviour { get; set; } - - /// - /// Filtermethod delegate for performing filtering - /// - /// - /// Default method is - /// - public LabelCollisionDetection.LabelFilterMethod LabelFilter - { - get { return _labelFilter; } - set { _labelFilter = value; } - } - - - /// - /// Render whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas - /// - public SmoothingMode SmoothingMode { get; set; } - - /// - /// Specifies the quality of text rendering - /// - public TextRenderingHint TextRenderingHint - { - get { return _textRenderingHint; } - set { _textRenderingHint = value; } - } - - /// - /// Gets or sets the datasource - /// - public IBaseProvider DataSource - { - get { return _dataSource; } - set - { - _dataSource = value; - _envelope = null; - } - } - - /// - /// Gets or sets the rendering style of the label layer. - /// - public new LabelStyle Style - { - get { return base.Style as LabelStyle; } - set { base.Style = value; } - } - - /// - /// Gets or sets thematic settings for the layer. Set to null to ignore thematics - /// - public ITheme Theme - { - get { return _theme; } - set { _theme = value; } - } - - /// - /// Data column or expression where label text is extracted from. - /// - /// - /// This property is overridden by the . - /// - public string LabelColumn - { - get { return _labelColumn; } - set { _labelColumn = value; } - } - - /// - /// Gets or sets the method for creating a custom label string based on a feature. - /// - /// - /// If this method is not null, it will override the value. - /// The label delegate must take a and return a string. - /// - /// Creating a label-text by combining attributes "ROADNAME" and "STATE" into one string, using - /// an anonymous delegate: - /// - /// myLabelLayer.LabelStringDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) - /// { return fdr["ROADNAME"].ToString() + ", " + fdr["STATE"].ToString(); }; - /// - /// - /// - public GetLabelMethod LabelStringDelegate - { - get { return _getLabelMethod; } - set { _getLabelMethod = value; } - } - /// - /// Gets or sets the method for creating a custom position based on a feature. - /// - /// - /// If this method is not null, it will override the position based on the centroid of the boundingbox of the feature - /// The label delegate must take a and return a GeoAPI.Geometries.Coordinate. - /// If the delegate returns a null, the centroid of the feature will be used - /// - /// Creating a custom position by using X and Y values from the FeatureDataRow attributes "LabelX" and "LabelY", using - /// an anonymous delegate: - /// - /// myLabelLayer.LabelPositionDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) - /// { return new GeoAPI.Geometries.Coordinate(Convert.ToDouble(fdr["LabelX"]), Convert.ToDouble(fdr["LabelY"]));}; - /// - /// - /// - public GetLocationMethod LabelPositionDelegate - { - get { return _getLocationMethod; } - set { _getLocationMethod = value; } - } - - - /// - /// Gets or sets the method for calculating the render priority of a label based on a feature. - /// - /// - /// If this method is not null, it will override the value. - /// The label delegate must take a and return an Int32. - /// - /// Creating a priority by combining attributes "capital" and "population" into one value, using - /// an anonymous delegate: - /// - /// myLabelLayer.PriorityDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) - /// { - /// Int32 retVal = 100000000 * (Int32)( (String)fdr["capital"] == "Y" ? 1 : 0 ); - /// return retVal + Convert.ToInt32(fdr["population"]); - /// }; - /// - /// - /// - public GetPriorityMethod PriorityDelegate - { - get { return _getPriorityMethod; } - set { _getPriorityMethod = value; } - } - - /// - /// Data column from where the label rotation is derived. - /// If this is empty, rotation will be zero, or aligned to a linestring. - /// Rotation are in degrees (positive = clockwise). - /// - public string RotationColumn - { - get { return _rotationColumn; } - set { _rotationColumn = value; } - } - - /// - /// A value indication the priority of the label in cases of label-collision detection - /// - public int Priority - { - get { return _priority; } - set { _priority = value; } - } - - /// - /// Name of the column that holds the value indicating the priority of the label in cases of label-collision detection - /// - public string PriorityColumn - { - get { return _priorityColumn; } - set { _priorityColumn = value; } - } - - /// - /// Gets the boundingbox of the entire layer - /// - public override Envelope Envelope - { - get - { - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - - if (_envelope != null && CacheExtent) - return ToTarget(_envelope.Copy()); - - var wasOpen = DataSource.IsOpen; - if (!wasOpen) - DataSource.Open(); - var box = DataSource.GetExtents(); - if (!wasOpen) //Restore state - DataSource.Close(); - - if (CacheExtent) - _envelope = box; - - return ToTarget(box); - } - } - - /// - /// Gets or sets a value indicating whether the layer envelope should be treated as static or not. - /// - /// - /// When CacheExtent is enabled the layer Envelope will be calculated only once from DataSource, this - /// helps to speed up the Envelope calculation with some DataProviders. Default is false for backward - /// compatibility. - /// - public virtual bool CacheExtent { get; set; } - - /// - /// Gets or sets the SRID of this VectorLayer's data source - /// - public override int SRID - { - get - { - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - return DataSource.SRID; - } - set - { - DataSource.SRID = value; - base.SRID = value; - } - } - - #region IDisposable Members - - /// - /// Releases managed resources - /// - protected override void ReleaseManagedResources() - { - if (DataSource != null) - DataSource.Dispose(); - base.ReleaseManagedResources(); - } - - #endregion - - /// - /// Renders the layer - /// - /// Graphics object reference - /// Map which is rendered - public override void Render(Graphics g, MapViewport map) - { - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - g.TextRenderingHint = TextRenderingHint; - g.SmoothingMode = SmoothingMode; - - var mapEnvelope = map.Envelope; - var layerEnvelope = ToSource(mapEnvelope); //View to render - var lineClipping = new CohenSutherlandLineClipping(mapEnvelope.MinX, mapEnvelope.MinY, - mapEnvelope.MaxX, mapEnvelope.MaxY); - - var ds = new FeatureDataSet(); - DataSource.Open(); - DataSource.ExecuteIntersectionQuery(layerEnvelope, ds); - DataSource.Close(); - if (ds.Tables.Count == 0) - { - base.Render(g, map); - return; - } - - var features = ds.Tables[0]; - - //Initialize label collection - var labels = new List(); - - //List LabelBoxes; //Used for collision detection - //Render labels - - for (int i = 0; i < features.Count; i++) - { - var feature = features[i]; - feature.Geometry = ToTarget(feature.Geometry); - - LabelStyle style; - if (Theme != null) //If thematics is enabled, lets override the style - style = Theme.GetStyle(feature) as LabelStyle; - else - style = Style; - - // Do we need to render at all? - if (!style.Enabled) continue; - - // Rotation - float rotationStyle = style != null ? style.Rotation : 0f; - float rotationColumn = 0f; - if (!String.IsNullOrEmpty(RotationColumn)) - Single.TryParse(feature[RotationColumn].ToString(), NumberStyles.Any, Map.NumberFormatEnUs, - out rotationColumn); - float rotation = rotationStyle + rotationColumn; - - // Priority - int priority = Priority; - if (_getPriorityMethod != null) - priority = _getPriorityMethod(feature); - else if (!String.IsNullOrEmpty(PriorityColumn)) - Int32.TryParse(feature[PriorityColumn].ToString(), NumberStyles.Any, Map.NumberFormatEnUs, - out priority); - - // Text - string text; - if (_getLabelMethod != null) - text = _getLabelMethod(feature); - else - text = feature[LabelColumn].ToString(); - - if (!String.IsNullOrEmpty(text)) - { - // for lineal geometries, try clipping to ensure proper labeling - if (feature.Geometry is ILineal) - { - if (feature.Geometry is ILineString) - feature.Geometry = lineClipping.ClipLineString(feature.Geometry as ILineString); - else if (feature.Geometry is IMultiLineString) - feature.Geometry = lineClipping.ClipLineString(feature.Geometry as IMultiLineString); - } - - if (feature.Geometry is IGeometryCollection) - { - if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.All) - { - foreach (var geom in (feature.Geometry as IGeometryCollection)) - { - BaseLabel lbl = CreateLabel(feature, geom, text, rotation, priority, style, map, g, _getLocationMethod); - if (lbl != null) - labels.Add(lbl); - } - } - else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.CommonCenter) - { - BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); - if (lbl != null) - labels.Add(lbl); - } - else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.First) - { - if ((feature.Geometry as IGeometryCollection).NumGeometries > 0) - { - BaseLabel lbl = CreateLabel(feature, (feature.Geometry as IGeometryCollection).GetGeometryN(0), text, - rotation, style, map, g); - if (lbl != null) - labels.Add(lbl); - } - } - else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.Largest) - { - var coll = (feature.Geometry as IGeometryCollection); - if (coll.NumGeometries > 0) - { - var largestVal = 0d; - var idxOfLargest = 0; - for (var j = 0; j < coll.NumGeometries; j++) - { - var geom = coll.GetGeometryN(j); - if (geom is ILineString && ((ILineString) geom).Length > largestVal) - { - largestVal = ((ILineString) geom).Length; - idxOfLargest = j; - } - if (geom is IMultiLineString && ((IMultiLineString) geom).Length > largestVal) - { - largestVal = ((IMultiLineString) geom).Length; - idxOfLargest = j; - } - if (geom is IPolygon && ((IPolygon) geom).Area > largestVal) - { - largestVal = ((IPolygon) geom).Area; - idxOfLargest = j; - } - if (geom is IMultiPolygon && ((IMultiPolygon) geom).Area > largestVal) - { - largestVal = ((IMultiPolygon) geom).Area; - idxOfLargest = j; - } - } - - BaseLabel lbl = CreateLabel(feature, coll.GetGeometryN(idxOfLargest), text, rotation, priority, style, - map, g, _getLocationMethod); - if (lbl != null) - labels.Add(lbl); - } - } - } - else - { - BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); - if (lbl != null) - labels.Add(lbl); - } - } - } - if (labels.Count > 0) //We have labels to render... - { - if (Style.CollisionDetection && _labelFilter != null) - _labelFilter(labels); - - for (int i = 0; i < labels.Count; i++) - { - // Don't show the label if not necessary - if (!labels[i].Show) - { - continue; - } - - if (labels[i] is Label) - { - var label = labels[i] as Label; - if (label.Style.IsTextOnPath == false || label.TextOnPathLabel == null) - { - VectorRenderer.DrawLabel(g, label.Location, label.Style.Offset, - label.Style.GetFontForGraphics(g), label.Style.ForeColor, - label.Style.BackColor, label.Style.Halo, label.Rotation, - label.Text, map, label.Style.HorizontalAlignment, - label.LabelPoint); - } - else - { - if (label.Style.BackColor != null && label.Style.BackColor != System.Drawing.Brushes.Transparent) - { - //draw background - if (label.TextOnPathLabel.RegionList.Count > 0) - { - g.FillRectangles(labels[i].Style.BackColor, labels[i].TextOnPathLabel.RegionList.ToArray()); - //g.FillPolygon(labels[i].Style.BackColor, labels[i].TextOnPathLabel.PointsText.ToArray()); - } - } - label.TextOnPathLabel.DrawTextOnPath(); - } - } - else if (labels[i] is PathLabel) - { - var plbl = labels[i] as PathLabel; - var lblStyle = plbl.Style; - g.DrawString(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), plbl.Text, - lblStyle.Font.FontFamily, (int) lblStyle.Font.Style, lblStyle.Font.Size, - lblStyle.GetStringFormat(), lblStyle.IgnoreLength, plbl.Location); - } - } - } - base.Render(g, map); - } - - - private BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, LabelStyle style, MapViewport map, Graphics g) - { - return CreateLabel(fdr, feature, text, rotation, Priority, style, map, g, _getLocationMethod); - } - - private static BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, int priority, LabelStyle style, MapViewport map, Graphics g, GetLocationMethod _getLocationMethod) - { - if (feature == null) return null; - - BaseLabel lbl = null; - var font = style.GetFontForGraphics(g); - - SizeF size = VectorRenderer.SizeOfString(g, text, font); - - if (feature is ILineal) - { - var line = feature as ILineString; - if (line != null) - { - if (style.IsTextOnPath == false) - { - if (size.Width < 0.95 * line.Length / map.PixelWidth || style.IgnoreLength) - { - var positiveLineString = PositiveLineString(line, false); - var lineStringPath = LineStringToPath(positiveLineString, map /*, false*/); - var rect = lineStringPath.GetBounds(); - - if (style.CollisionDetection && !style.CollisionBuffer.IsEmpty) - { - var cbx = style.CollisionBuffer.Width; - var cby = style.CollisionBuffer.Height; - rect.Inflate(2 * cbx, 2 * cby); - rect.Offset(-cbx, -cby); - } - var labelBox = new LabelBox(rect); - - lbl = new PathLabel(text, lineStringPath, 0, priority, labelBox, style); - } - } - else - { - //get centriod - System.Drawing.PointF position2 = map.WorldToImage(feature.EnvelopeInternal.Centre); - lbl = new Label(text, position2, rotation, priority, style); - if (size.Width < 0.95 * line.Length / map.PixelWidth || !style.IgnoreLength) - { - CalculateLabelAroundOnLineString(line, ref lbl, map, g, size); - } - } - } - return lbl; - } - - var worldPosition = _getLocationMethod == null - ? feature.EnvelopeInternal.Centre - : _getLocationMethod(fdr); - - if (worldPosition == null) return null; - - var position = map.WorldToImage(worldPosition); - - var location = new PointF( - position.X - size.Width*(short) style.HorizontalAlignment*0.5f, - position.Y - size.Height*(short) (2 - (int) style.VerticalAlignment)*0.5f); - - if (location.X - size.Width > map.Size.Width || location.X + size.Width < 0 || - location.Y - size.Height > map.Size.Height || location.Y + size.Height < 0) - return null; - - if (!style.CollisionDetection) - lbl = new Label(text, location, rotation, priority, null, style) - {LabelPoint = position}; - else - { - //Collision detection is enabled so we need to measure the size of the string - lbl = new Label(text, location, rotation, priority, - new LabelBox(location.X - style.CollisionBuffer.Width, - location.Y - style.CollisionBuffer.Height, - size.Width + 2f*style.CollisionBuffer.Width, - size.Height + 2f*style.CollisionBuffer.Height), style) - { LabelPoint = position }; - } - - /* - if (feature is LineString) - { - var line = feature as LineString; - - //Only label feature if it is long enough, or it is definately wanted - if (line.Length / map.PixelSize > size.Width || style.IgnoreLength) - { - CalculateLabelOnLinestring(line, ref lbl, map); - } - else - return null; - } - */ - return lbl; - } - - /// - /// Very basic test to check for positive direction of Linestring - /// - /// The linestring to test - /// Value indicating whether labels are to be printed right to left - /// The positively directed linestring - private static ILineString PositiveLineString(ILineString line, bool isRightToLeft) - { - var s = line.StartPoint; - var e = line.EndPoint; - - var dx = e.X - s.X; - if (isRightToLeft && dx < 0) - return line; - - if (!isRightToLeft && dx >= 0) - return line; - - var revCoord = new Stack(line.Coordinates); - - return line.Factory.CreateLineString(revCoord.ToArray()); - } - - //private static void WarpedLabel(MultiLineString line, ref BaseLabel baseLabel, Map map) - //{ - // var path = MultiLineStringToPath(line, map, true); - - // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); - // baseLabel = pathLabel; - //} - - //private static void WarpedLabel(LineString line, ref BaseLabel baseLabel, Map map) - //{ - - // var path = LineStringToPath(line, map, false); - - // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); - // baseLabel = pathLabel; - //} - - - /// - /// Function to transform a linestring to a graphics path for further processing - /// - /// The Linestring - /// The map - /// - /// A GraphicsPath - public static GraphicsPath LineStringToPath(ILineString lineString, MapViewport map/*, bool useClipping*/) - { - var gp = new GraphicsPath(FillMode.Alternate); - //if (!useClipping) - gp.AddLines(lineString.TransformToImage(map)); - //else - //{ - // var bb = map.Envelope; - // var cohenSutherlandLineClipping = new CohenSutherlandLineClipping(bb.Left, bb.Bottom, bb.Right, bb.Top); - // var clippedLineStrings = cohenSutherlandLineClipping.ClipLineString(lineString); - // foreach (var clippedLineString in clippedLineStrings.LineStrings) - // { - // var s = clippedLineString.StartPoint; - // var e = clippedLineString.EndPoint; - - // var dx = e.X - s.X; - // //var dy = e.Y - s.Y; - - // LineString revcls = null; - // if (dx < 0) - // revcls = ReverseLineString(clippedLineString); - - // gp.StartFigure(); - // gp.AddLines(revcls == null ? clippedLineString.TransformToImage(map) : revcls.TransformToImage(map)); - // } - //} - return gp; - } - - //private static LineString ReverseLineString(LineString clippedLineString) - //{ - // var coords = new Stack(clippedLineString.Vertices); - // return new LineString(coords.ToArray()); - //} - - ///// - ///// Function to transform a linestring to a graphics path for further processing - ///// - ///// The Linestring - ///// The map - ///// A value indicating whether clipping should be applied or not - ///// A GraphicsPath - //public static GraphicsPath MultiLineStringToPath(MultiLineString multiLineString, Map map, bool useClipping) - //{ - // var gp = new GraphicsPath(FillMode.Alternate); - // foreach (var lineString in multiLineString.LineStrings) - // gp.AddPath(LineStringToPath(lineString, map, useClipping), false); - - // return gp; - //} - - //private static GraphicsPath LineToGraphicsPath(LineString line, Map map) - //{ - // GraphicsPath path = new GraphicsPath(); - // path.AddLines(line.TransformToImage(map)); - // return path; - //} - - private static void CalculateLabelOnLinestring(ILineString line, ref BaseLabel baseLabel, Map map) - { - double dx, dy; - var label = baseLabel as Label; - - // first find the middle segment of the line - var vertices = line.Coordinates; - int midPoint = (vertices.Length - 1)/2; - if (vertices.Length > 2) - { - dx = vertices[midPoint + 1].X - vertices[midPoint].X; - dy = vertices[midPoint + 1].Y - vertices[midPoint].Y; - } - else - { - midPoint = 0; - dx = vertices[1].X - vertices[0].X; - dy = vertices[1].Y - vertices[0].Y; - } - if (dy == 0) - label.Rotation = 0; - else if (dx == 0) - label.Rotation = 90; - else - { - // calculate angle of line - double angle = -Math.Atan(dy/dx) + Math.PI*0.5; - angle *= (180d/Math.PI); // convert radians to degrees - label.Rotation = (float) angle - 90; // -90 text orientation - } - var tmpx = vertices[midPoint].X + (dx*0.5); - var tmpy = vertices[midPoint].Y + (dy*0.5); - label.Location = map.WorldToImage(new Coordinate(tmpx, tmpy)); - } - - private static void CalculateLabelAroundOnLineString(ILineString line, ref BaseLabel label, MapViewport map, System.Drawing.Graphics g, System.Drawing.SizeF textSize) - { - var sPoints = line.Coordinates; - - // only get point in enverlop of map - var colPoint = new Collection(); - var bCheckStarted = false; - //var testEnvelope = map.Envelope.Grow(map.PixelSize*10); - for (var j = 0; j < sPoints.Length; j++) - { - if (map.Envelope.Contains(sPoints[j])) - { - //points[j] = map.WorldToImage(sPoints[j]); - colPoint.Add(map.WorldToImage(sPoints[j])); - bCheckStarted = true; - } - else if (bCheckStarted) - { - // fix bug curved line out of map in center segment of line - break; - } - } - - if (colPoint.Count > 1) - { - label.TextOnPathLabel = new TextOnPath(); - switch (label.Style.HorizontalAlignment) - { - case LabelStyle.HorizontalAlignmentEnum.Left: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Left; - break; - case LabelStyle.HorizontalAlignmentEnum.Right: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Right; - break; - case LabelStyle.HorizontalAlignmentEnum.Center: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; - break; - default: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; - break; - } - switch (label.Style.VerticalAlignment) - { - case LabelStyle.VerticalAlignmentEnum.Bottom: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.UnderPath; - break; - case LabelStyle.VerticalAlignmentEnum.Top: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.OverPath; - break; - case LabelStyle.VerticalAlignmentEnum.Middle: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; - break; - default: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; - break; - } - - var idxStartPath = 0; - var numberPoint = colPoint.Count; - // start Optimzes Path points - - var step = 100; - if (colPoint.Count >= step * 2) - { - numberPoint = step * 2; ; - switch (label.Style.HorizontalAlignment) - { - case LabelStyle.HorizontalAlignmentEnum.Left: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Left; - idxStartPath = 0; - break; - case LabelStyle.HorizontalAlignmentEnum.Right: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Right; - idxStartPath = colPoint.Count - step; - break; - case LabelStyle.HorizontalAlignmentEnum.Center: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; - idxStartPath = (int)colPoint.Count / 2 - step; - break; - default: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; - idxStartPath = (int)colPoint.Count / 2 - step; - break; - } - } - // end optimize path point - var points = new PointF[numberPoint]; - var count = 0; - if (colPoint[0].X <= colPoint[colPoint.Count - 1].X) - { - for (var l = idxStartPath; l < numberPoint + idxStartPath; l++) - { - points[count] = colPoint[l]; - count++; - } - } - else - { - //reverse the path - for (var k = numberPoint - 1 + idxStartPath; k >= idxStartPath; k--) - { - points[count] = colPoint[k]; - count++; - } - } - /* - //get text size in page units ie pixels - float textheight = label.Style.Font.Size; - switch (label.Style.Font.Unit) - { - case GraphicsUnit.Display: - textheight = textheight * g.DpiY / 75; - break; - case GraphicsUnit.Document: - textheight = textheight * g.DpiY / 300; - break; - case GraphicsUnit.Inch: - textheight = textheight * g.DpiY; - break; - case GraphicsUnit.Millimeter: - textheight = (float)(textheight / 25.4 * g.DpiY); - break; - case GraphicsUnit.Pixel: - //do nothing - break; - case GraphicsUnit.Point: - textheight = textheight * g.DpiY / 72; - break; - } - var topFont = new Font(label.Style.Font.FontFamily, textheight, label.Style.Font.Style, GraphicsUnit.Pixel); - */ - var topFont = label.Style.GetFontForGraphics(g); - // - var path = new GraphicsPath(); - path.AddLines(points); - - label.TextOnPathLabel.PathColorTop = System.Drawing.Color.Transparent; - label.TextOnPathLabel.Text = label.Text; - label.TextOnPathLabel.LetterSpacePercentage = 90; - label.TextOnPathLabel.FillColorTop = new System.Drawing.SolidBrush(label.Style.ForeColor); - label.TextOnPathLabel.Font = topFont; - label.TextOnPathLabel.PathDataTop = path.PathData; - label.TextOnPathLabel.Graphics = g; - //label.TextOnPathLabel.ShowPath=true; - //label.TextOnPathLabel.PathColorTop = System.Drawing.Color.YellowGreen; - if (label.Style.Halo != null) - { - label.TextOnPathLabel.ColorHalo = label.Style.Halo; - } - else - { - label.TextOnPathLabel.ColorHalo = null;// new System.Drawing.Pen(label.Style.ForeColor, (float)0.5); - } - path.Dispose(); - - // MeasureString to get region - label.TextOnPathLabel.MeasureString = true; - label.TextOnPathLabel.DrawTextOnPath(); - label.TextOnPathLabel.MeasureString = false; - // Get Region label for CollissionDetection here. - var pathRegion = new GraphicsPath(); - - if (label.TextOnPathLabel.RegionList.Count > 0) - { - //int idxCenter = (int)label.TextOnPathLabel.PointsText.Count / 2; - //System.Drawing.Drawing2D.Matrix rotationMatrix = g.Transform.Clone();// new Matrix(); - //rotationMatrix.RotateAt(label.TextOnPathLabel.Angles[idxCenter], label.TextOnPathLabel.PointsText[idxCenter]); - //if (label.TextOnPathLabel.PointsTextUp.Count > 0) - //{ - // for (int up = label.TextOnPathLabel.PointsTextUp.Count - 1; up >= 0; up--) - // { - // label.TextOnPathLabel.PointsText.Add(label.TextOnPathLabel.PointsTextUp[up]); - // } - - //} - pathRegion.AddRectangles(label.TextOnPathLabel.RegionList.ToArray()); - - // get box for detect colission here - label.Box = new LabelBox(pathRegion.GetBounds()); - //g.FillRectangle(System.Drawing.Brushes.YellowGreen, label.Box); - } - pathRegion.Dispose(); - } - - } - } -} +// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) +// +// This file is part of SharpMap. +// SharpMap is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// SharpMap is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public License +// along with SharpMap; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Globalization; +using SharpMap.Data; +using SharpMap.Data.Providers; +using GeoAPI.Geometries; +using SharpMap.Rendering; +using SharpMap.Rendering.Thematics; +using SharpMap.Styles; +using Transform = SharpMap.Utilities.Transform; +using SharpMap.Rendering.Symbolizer; + +namespace SharpMap.Layers +{ + /// + /// Label layer class + /// + /// + /// Creates a new label layer and sets the label text to the "Name" column in the FeatureDataTable of the datasource + /// + /// //Set up a label layer + /// SharpMap.Layers.LabelLayer layLabel = new SharpMap.Layers.LabelLayer("Country labels"); + /// layLabel.DataSource = layCountries.DataSource; + /// layLabel.Enabled = true; + /// layLabel.LabelColumn = "Name"; + /// layLabel.Style = new SharpMap.Styles.LabelStyle(); + /// layLabel.Style.CollisionDetection = true; + /// layLabel.Style.CollisionBuffer = new SizeF(20, 20); + /// layLabel.Style.ForeColor = Color.White; + /// layLabel.Style.Font = new Font(FontFamily.GenericSerif, 8); + /// layLabel.MaxVisible = 90; + /// layLabel.Style.HorizontalAlignment = SharpMap.Styles.LabelStyle.HorizontalAlignmentEnum.Center; + /// + /// + [Serializable] + public class LabelLayer : Layer + { + #region Delegates + + /// + /// Delegate method for creating advanced label texts + /// + /// the to build the label for + /// the label + public delegate string GetLabelMethod(FeatureDataRow fdr); + + /// + /// Delegate method for calculating the priority of label rendering + /// + /// the to compute the priority value from + /// the priority value + public delegate int GetPriorityMethod(FeatureDataRow fdr); + + /// + /// Delegate method for advanced placement of the label position + /// + /// the to compute the label position from + /// the priority value + public delegate Coordinate GetLocationMethod(FeatureDataRow fdr); + #endregion + + #region MultipartGeometryBehaviourEnum enum + + /// + /// Labelling behaviour for Multipart geometry collections + /// + public enum MultipartGeometryBehaviourEnum + { + /// + /// Place label on all parts (default) + /// + All, + /// + /// Place label on object which the greatest length or area. + /// + /// + /// Multipoint geometries will default to + /// + Largest, + /// + /// The center of the combined geometries + /// + CommonCenter, + /// + /// Center of the first geometry in the collection (fastest method) + /// + First + } + + #endregion + + private IBaseProvider _dataSource; + private GetLabelMethod _getLabelMethod; + private GetPriorityMethod _getPriorityMethod; + private GetLocationMethod _getLocationMethod; + private Envelope _envelope; + + /// + /// Name of the column that holds the value for the label. + /// + private string _labelColumn; + + /// + /// Delegate for custom Label Collision Detection + /// + private LabelCollisionDetection.LabelFilterMethod _labelFilter; + + /// + /// A value indication the priority of the label in cases of label-collision detection + /// + private int _priority; + + /// + /// Name of the column that contains the value indicating the priority of the label in case of label-collision detection + /// + private string _priorityColumn = ""; + + /// + /// Name of the column that contains the value indicating the rotation value of the label + /// + private string _rotationColumn; + + + private TextRenderingHint _textRenderingHint; + + private ITheme _theme; + + /// + /// Creates a new instance of a LabelLayer + /// + public LabelLayer(string layername) + :base(new LabelStyle()) + { + //_Style = new LabelStyle(); + LayerName = layername; + SmoothingMode = SmoothingMode.AntiAlias; + TextRenderingHint = TextRenderingHint.AntiAlias; + MultipartGeometryBehaviour = MultipartGeometryBehaviourEnum.All; + _labelFilter = LabelCollisionDetection.SimpleCollisionDetection; + } + + /// + /// Gets or sets labelling behavior on multipart geometries + /// + /// Default value is + public MultipartGeometryBehaviourEnum MultipartGeometryBehaviour { get; set; } + + /// + /// Filtermethod delegate for performing filtering + /// + /// + /// Default method is + /// + public LabelCollisionDetection.LabelFilterMethod LabelFilter + { + get { return _labelFilter; } + set { _labelFilter = value; } + } + + + /// + /// Render whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas + /// + public SmoothingMode SmoothingMode { get; set; } + + /// + /// Specifies the quality of text rendering + /// + public TextRenderingHint TextRenderingHint + { + get { return _textRenderingHint; } + set { _textRenderingHint = value; } + } + + /// + /// Gets or sets the datasource + /// + public IBaseProvider DataSource + { + get { return _dataSource; } + set + { + _dataSource = value; + _envelope = null; + } + } + + /// + /// Gets or sets the rendering style of the label layer. + /// + public new LabelStyle Style + { + get { return base.Style as LabelStyle; } + set { base.Style = value; } + } + + /// + /// Gets or sets thematic settings for the layer. Set to null to ignore thematics + /// + public ITheme Theme + { + get { return _theme; } + set { _theme = value; } + } + + /// + /// Data column or expression where label text is extracted from. + /// + /// + /// This property is overridden by the . + /// + public string LabelColumn + { + get { return _labelColumn; } + set { _labelColumn = value; } + } + + /// + /// Gets or sets the method for creating a custom label string based on a feature. + /// + /// + /// If this method is not null, it will override the value. + /// The label delegate must take a and return a string. + /// + /// Creating a label-text by combining attributes "ROADNAME" and "STATE" into one string, using + /// an anonymous delegate: + /// + /// myLabelLayer.LabelStringDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) + /// { return fdr["ROADNAME"].ToString() + ", " + fdr["STATE"].ToString(); }; + /// + /// + /// + public GetLabelMethod LabelStringDelegate + { + get { return _getLabelMethod; } + set { _getLabelMethod = value; } + } + /// + /// Gets or sets the method for creating a custom position based on a feature. + /// + /// + /// If this method is not null, it will override the position based on the centroid of the boundingbox of the feature + /// The label delegate must take a and return a GeoAPI.Geometries.Coordinate. + /// If the delegate returns a null, the centroid of the feature will be used + /// + /// Creating a custom position by using X and Y values from the FeatureDataRow attributes "LabelX" and "LabelY", using + /// an anonymous delegate: + /// + /// myLabelLayer.LabelPositionDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) + /// { return new GeoAPI.Geometries.Coordinate(Convert.ToDouble(fdr["LabelX"]), Convert.ToDouble(fdr["LabelY"]));}; + /// + /// + /// + public GetLocationMethod LabelPositionDelegate + { + get { return _getLocationMethod; } + set { _getLocationMethod = value; } + } + + + /// + /// Gets or sets the method for calculating the render priority of a label based on a feature. + /// + /// + /// If this method is not null, it will override the value. + /// The label delegate must take a and return an Int32. + /// + /// Creating a priority by combining attributes "capital" and "population" into one value, using + /// an anonymous delegate: + /// + /// myLabelLayer.PriorityDelegate = delegate(SharpMap.Data.FeatureDataRow fdr) + /// { + /// Int32 retVal = 100000000 * (Int32)( (String)fdr["capital"] == "Y" ? 1 : 0 ); + /// return retVal + Convert.ToInt32(fdr["population"]); + /// }; + /// + /// + /// + public GetPriorityMethod PriorityDelegate + { + get { return _getPriorityMethod; } + set { _getPriorityMethod = value; } + } + + /// + /// Data column from where the label rotation is derived. + /// If this is empty, rotation will be zero, or aligned to a linestring. + /// Rotation are in degrees (positive = clockwise). + /// + public string RotationColumn + { + get { return _rotationColumn; } + set { _rotationColumn = value; } + } + + /// + /// A value indication the priority of the label in cases of label-collision detection + /// + public int Priority + { + get { return _priority; } + set { _priority = value; } + } + + /// + /// Name of the column that holds the value indicating the priority of the label in cases of label-collision detection + /// + public string PriorityColumn + { + get { return _priorityColumn; } + set { _priorityColumn = value; } + } + + /// + /// Gets the boundingbox of the entire layer + /// + public override Envelope Envelope + { + get + { + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + + if (_envelope != null && CacheExtent) + return ToTarget(_envelope.Copy()); + + var wasOpen = DataSource.IsOpen; + if (!wasOpen) + DataSource.Open(); + var box = DataSource.GetExtents(); + if (!wasOpen) //Restore state + DataSource.Close(); + + if (CacheExtent) + _envelope = box; + + return ToTarget(box); + } + } + + /// + /// Gets or sets a value indicating whether the layer envelope should be treated as static or not. + /// + /// + /// When CacheExtent is enabled the layer Envelope will be calculated only once from DataSource, this + /// helps to speed up the Envelope calculation with some DataProviders. Default is false for backward + /// compatibility. + /// + public virtual bool CacheExtent { get; set; } + + /// + /// Gets or sets the SRID of this VectorLayer's data source + /// + public override int SRID + { + get + { + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + return DataSource.SRID; + } + set + { + DataSource.SRID = value; + base.SRID = value; + } + } + + #region IDisposable Members + + /// + /// Releases managed resources + /// + protected override void ReleaseManagedResources() + { + if (DataSource != null) + DataSource.Dispose(); + base.ReleaseManagedResources(); + } + + #endregion + + /// + /// Renders the layer + /// + /// Graphics object reference + /// Map which is rendered + public override void Render(Graphics g, MapViewport map) + { + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + g.TextRenderingHint = TextRenderingHint; + g.SmoothingMode = SmoothingMode; + + var mapEnvelope = map.Envelope; + var layerEnvelope = ToSource(mapEnvelope); //View to render + var lineClipping = new CohenSutherlandLineClipping(mapEnvelope.MinX, mapEnvelope.MinY, + mapEnvelope.MaxX, mapEnvelope.MaxY); + + var ds = new FeatureDataSet(); + DataSource.Open(); + DataSource.ExecuteIntersectionQuery(layerEnvelope, ds); + DataSource.Close(); + if (ds.Tables.Count == 0) + { + base.Render(g, map); + return; + } + + var features = ds.Tables[0]; + + //Initialize label collection + var labels = new List(); + + //List LabelBoxes; //Used for collision detection + //Render labels + + for (int i = 0; i < features.Count; i++) + { + var feature = features[i]; + feature.Geometry = ToTarget(feature.Geometry); + + LabelStyle style; + if (Theme != null) //If thematics is enabled, lets override the style + style = Theme.GetStyle(feature) as LabelStyle; + else + style = Style; + + // Do we need to render at all? + if (!style.Enabled) continue; + + // Rotation + float rotationStyle = style != null ? style.Rotation : 0f; + float rotationColumn = 0f; + if (!String.IsNullOrEmpty(RotationColumn)) + Single.TryParse(feature[RotationColumn].ToString(), NumberStyles.Any, Map.NumberFormatEnUs, + out rotationColumn); + float rotation = rotationStyle + rotationColumn; + + // Priority + int priority = Priority; + if (_getPriorityMethod != null) + priority = _getPriorityMethod(feature); + else if (!String.IsNullOrEmpty(PriorityColumn)) + Int32.TryParse(feature[PriorityColumn].ToString(), NumberStyles.Any, Map.NumberFormatEnUs, + out priority); + + // Text + string text; + if (_getLabelMethod != null) + text = _getLabelMethod(feature); + else + text = feature[LabelColumn].ToString(); + + if (String.IsNullOrEmpty(text)) continue; + + // for lineal geometries, try clipping to ensure proper labeling + if (feature.Geometry is ILineal) + { + if (feature.Geometry is ILineString) + feature.Geometry = lineClipping.ClipLineString(feature.Geometry as ILineString); + else if (feature.Geometry is IMultiLineString) + feature.Geometry = lineClipping.ClipLineString(feature.Geometry as IMultiLineString); + } + + if (feature.Geometry is IGeometryCollection) + { + if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.All) + { + foreach (var geom in (feature.Geometry as IGeometryCollection)) + { + BaseLabel lbl = CreateLabel(feature, geom, text, rotation, priority, style, map, g, _getLocationMethod); + if (lbl != null) + labels.Add(lbl); + } + } + else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.CommonCenter) + { + BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); + if (lbl != null) + labels.Add(lbl); + } + else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.First) + { + if ((feature.Geometry as IGeometryCollection).NumGeometries > 0) + { + BaseLabel lbl = CreateLabel(feature, (feature.Geometry as IGeometryCollection).GetGeometryN(0), text, + rotation, style, map, g); + if (lbl != null) + labels.Add(lbl); + } + } + else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.Largest) + { + var coll = (feature.Geometry as IGeometryCollection); + if (coll.NumGeometries > 0) + { + var largestVal = 0d; + var idxOfLargest = 0; + for (var j = 0; j < coll.NumGeometries; j++) + { + var geom = coll.GetGeometryN(j); + if (geom is ILineString && ((ILineString) geom).Length > largestVal) + { + largestVal = ((ILineString) geom).Length; + idxOfLargest = j; + } + if (geom is IMultiLineString && ((IMultiLineString) geom).Length > largestVal) + { + largestVal = ((IMultiLineString) geom).Length; + idxOfLargest = j; + } + if (geom is IPolygon && ((IPolygon) geom).Area > largestVal) + { + largestVal = ((IPolygon) geom).Area; + idxOfLargest = j; + } + if (geom is IMultiPolygon && ((IMultiPolygon) geom).Area > largestVal) + { + largestVal = ((IMultiPolygon) geom).Area; + idxOfLargest = j; + } + } + + BaseLabel lbl = CreateLabel(feature, coll.GetGeometryN(idxOfLargest), text, rotation, priority, style, + map, g, _getLocationMethod); + if (lbl != null) + labels.Add(lbl); + } + } + } + else + { + BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); + if (lbl != null) + labels.Add(lbl); + } + } + + if (labels.Count == 0) + return; + + if (Style.CollisionDetection && _labelFilter != null) + _labelFilter(labels); + + RectangleF affectedArea = new RectangleF(); + + for (int i = 0; i < labels.Count; i++) + { + // Don't show the label if not necessary + if (!labels[i].Show) + { + continue; + } + + RectangleF rect; + + if (labels[i] is Label) + { + var label = labels[i] as Label; + if (label.Style.IsTextOnPath == false || label.TextOnPathLabel == null) + { + rect = VectorRenderer.DrawLabelEx(g, label.Location, label.Style.Offset, + label.Style.GetFontForGraphics(g), label.Style.ForeColor, + label.Style.BackColor, label.Style.Halo, label.Rotation, + label.Text, map, label.Style.HorizontalAlignment, + label.LabelPoint); + + affectedArea = VectorRenderer.RectExpandToInclude(affectedArea, rect); + } + else + { + if (label.Style.BackColor != null && + label.Style.BackColor != System.Drawing.Brushes.Transparent) + { + //draw background + if (label.TextOnPathLabel.RegionList.Count > 0) + { + g.FillRectangles(labels[i].Style.BackColor, + labels[i].TextOnPathLabel.RegionList.ToArray()); + //g.FillPolygon(labels[i].Style.BackColor, labels[i].TextOnPathLabel.PointsText.ToArray()); + } + } + + label.TextOnPathLabel.DrawTextOnPath(); + } + } + else if (labels[i] is PathLabel) + { + var plbl = labels[i] as PathLabel; + var lblStyle = plbl.Style; + g.DrawString(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), plbl.Text, + lblStyle.Font.FontFamily, (int) lblStyle.Font.Style, lblStyle.Font.Size, + lblStyle.GetStringFormat(), lblStyle.IgnoreLength, plbl.Location); + } + } + + if (!affectedArea.IsEmpty) + { + // TODO optimise for Rotation = 0. + var pts = new PointF[] + { + new PointF(affectedArea.Left - 1, affectedArea.Top - 1), + new PointF(affectedArea.Right + 1, affectedArea.Top - 1), + new PointF(affectedArea.Right + 1, affectedArea.Bottom + 1), + new PointF(affectedArea.Left - 1, affectedArea.Bottom + 1) + }; + + var coords = map.ImageToWorld(pts, true); + + var minX = Math.Min(coords[0].X, Math.Min(coords[1].X, Math.Min(coords[2].X, coords[3].X))); + var maxX = Math.Max(coords[0].X, Math.Max(coords[1].X, Math.Max(coords[2].X, coords[3].X))); + var minY = Math.Min(coords[0].Y, Math.Min(coords[1].Y, Math.Min(coords[2].Y, coords[3].Y))); + var maxY = Math.Max(coords[0].Y, Math.Max(coords[1].Y, Math.Max(coords[2].Y, coords[3].Y))); + + var env = new Envelope(minX, maxX, minY, maxY); + //env.ExpandBy(env.Width * 0.05, env.Height * 0.05); + + base._affectedArea.ExpandToInclude(env); + } + + base.Render(g, map); + } + + + private BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, LabelStyle style, MapViewport map, Graphics g) + { + return CreateLabel(fdr, feature, text, rotation, Priority, style, map, g, _getLocationMethod); + } + + private static BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, int priority, LabelStyle style, MapViewport map, Graphics g, GetLocationMethod _getLocationMethod) + { + if (feature == null) return null; + + BaseLabel lbl = null; + var font = style.GetFontForGraphics(g); + + SizeF size = VectorRenderer.SizeOfString(g, text, font); + + if (feature is ILineal) + { + var line = feature as ILineString; + if (line != null) + { + if (style.IsTextOnPath == false) + { + if (size.Width < 0.95 * line.Length / map.PixelWidth || style.IgnoreLength) + { + var positiveLineString = PositiveLineString(line, false); + var lineStringPath = LineStringToPath(positiveLineString, map /*, false*/); + var rect = lineStringPath.GetBounds(); + + if (style.CollisionDetection && !style.CollisionBuffer.IsEmpty) + { + var cbx = style.CollisionBuffer.Width; + var cby = style.CollisionBuffer.Height; + rect.Inflate(2 * cbx, 2 * cby); + rect.Offset(-cbx, -cby); + } + var labelBox = new LabelBox(rect); + + lbl = new PathLabel(text, lineStringPath, 0, priority, labelBox, style); + } + } + else + { + //get centriod + System.Drawing.PointF position2 = map.WorldToImage(feature.EnvelopeInternal.Centre); + lbl = new Label(text, position2, rotation, priority, style); + if (size.Width < 0.95 * line.Length / map.PixelWidth || !style.IgnoreLength) + { + CalculateLabelAroundOnLineString(line, ref lbl, map, g, size); + } + } + } + return lbl; + } + + var worldPosition = _getLocationMethod == null + ? feature.EnvelopeInternal.Centre + : _getLocationMethod(fdr); + + if (worldPosition == null) return null; + + var position = map.WorldToImage(worldPosition); + + var location = new PointF( + position.X - size.Width*(short) style.HorizontalAlignment*0.5f, + position.Y - size.Height*(short) (2 - (int) style.VerticalAlignment)*0.5f); + + if (location.X - size.Width > map.Size.Width || location.X + size.Width < 0 || + location.Y - size.Height > map.Size.Height || location.Y + size.Height < 0) + return null; + + if (!style.CollisionDetection) + lbl = new Label(text, location, rotation, priority, null, style) + {LabelPoint = position}; + else + { + //Collision detection is enabled so we need to measure the size of the string + lbl = new Label(text, location, rotation, priority, + new LabelBox(location.X - style.CollisionBuffer.Width, + location.Y - style.CollisionBuffer.Height, + size.Width + 2f*style.CollisionBuffer.Width, + size.Height + 2f*style.CollisionBuffer.Height), style) + { LabelPoint = position }; + } + + /* + if (feature is LineString) + { + var line = feature as LineString; + + //Only label feature if it is long enough, or it is definately wanted + if (line.Length / map.PixelSize > size.Width || style.IgnoreLength) + { + CalculateLabelOnLinestring(line, ref lbl, map); + } + else + return null; + } + */ + return lbl; + } + + /// + /// Very basic test to check for positive direction of Linestring + /// + /// The linestring to test + /// Value indicating whether labels are to be printed right to left + /// The positively directed linestring + private static ILineString PositiveLineString(ILineString line, bool isRightToLeft) + { + var s = line.StartPoint; + var e = line.EndPoint; + + var dx = e.X - s.X; + if (isRightToLeft && dx < 0) + return line; + + if (!isRightToLeft && dx >= 0) + return line; + + var revCoord = new Stack(line.Coordinates); + + return line.Factory.CreateLineString(revCoord.ToArray()); + } + + //private static void WarpedLabel(MultiLineString line, ref BaseLabel baseLabel, Map map) + //{ + // var path = MultiLineStringToPath(line, map, true); + + // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); + // baseLabel = pathLabel; + //} + + //private static void WarpedLabel(LineString line, ref BaseLabel baseLabel, Map map) + //{ + + // var path = LineStringToPath(line, map, false); + + // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); + // baseLabel = pathLabel; + //} + + + /// + /// Function to transform a linestring to a graphics path for further processing + /// + /// The Linestring + /// The map + /// + /// A GraphicsPath + public static GraphicsPath LineStringToPath(ILineString lineString, MapViewport map/*, bool useClipping*/) + { + var gp = new GraphicsPath(FillMode.Alternate); + //if (!useClipping) + gp.AddLines(lineString.TransformToImage(map)); + //else + //{ + // var bb = map.Envelope; + // var cohenSutherlandLineClipping = new CohenSutherlandLineClipping(bb.Left, bb.Bottom, bb.Right, bb.Top); + // var clippedLineStrings = cohenSutherlandLineClipping.ClipLineString(lineString); + // foreach (var clippedLineString in clippedLineStrings.LineStrings) + // { + // var s = clippedLineString.StartPoint; + // var e = clippedLineString.EndPoint; + + // var dx = e.X - s.X; + // //var dy = e.Y - s.Y; + + // LineString revcls = null; + // if (dx < 0) + // revcls = ReverseLineString(clippedLineString); + + // gp.StartFigure(); + // gp.AddLines(revcls == null ? clippedLineString.TransformToImage(map) : revcls.TransformToImage(map)); + // } + //} + return gp; + } + + //private static LineString ReverseLineString(LineString clippedLineString) + //{ + // var coords = new Stack(clippedLineString.Vertices); + // return new LineString(coords.ToArray()); + //} + + ///// + ///// Function to transform a linestring to a graphics path for further processing + ///// + ///// The Linestring + ///// The map + ///// A value indicating whether clipping should be applied or not + ///// A GraphicsPath + //public static GraphicsPath MultiLineStringToPath(MultiLineString multiLineString, Map map, bool useClipping) + //{ + // var gp = new GraphicsPath(FillMode.Alternate); + // foreach (var lineString in multiLineString.LineStrings) + // gp.AddPath(LineStringToPath(lineString, map, useClipping), false); + + // return gp; + //} + + //private static GraphicsPath LineToGraphicsPath(LineString line, Map map) + //{ + // GraphicsPath path = new GraphicsPath(); + // path.AddLines(line.TransformToImage(map)); + // return path; + //} + + private static void CalculateLabelOnLinestring(ILineString line, ref BaseLabel baseLabel, Map map) + { + double dx, dy; + var label = baseLabel as Label; + + // first find the middle segment of the line + var vertices = line.Coordinates; + int midPoint = (vertices.Length - 1)/2; + if (vertices.Length > 2) + { + dx = vertices[midPoint + 1].X - vertices[midPoint].X; + dy = vertices[midPoint + 1].Y - vertices[midPoint].Y; + } + else + { + midPoint = 0; + dx = vertices[1].X - vertices[0].X; + dy = vertices[1].Y - vertices[0].Y; + } + if (dy == 0) + label.Rotation = 0; + else if (dx == 0) + label.Rotation = 90; + else + { + // calculate angle of line + double angle = -Math.Atan(dy/dx) + Math.PI*0.5; + angle *= (180d/Math.PI); // convert radians to degrees + label.Rotation = (float) angle - 90; // -90 text orientation + } + var tmpx = vertices[midPoint].X + (dx*0.5); + var tmpy = vertices[midPoint].Y + (dy*0.5); + label.Location = map.WorldToImage(new Coordinate(tmpx, tmpy)); + } + + private static void CalculateLabelAroundOnLineString(ILineString line, ref BaseLabel label, MapViewport map, System.Drawing.Graphics g, System.Drawing.SizeF textSize) + { + var sPoints = line.Coordinates; + + // only get point in enverlop of map + var colPoint = new Collection(); + var bCheckStarted = false; + //var testEnvelope = map.Envelope.Grow(map.PixelSize*10); + for (var j = 0; j < sPoints.Length; j++) + { + if (map.Envelope.Contains(sPoints[j])) + { + //points[j] = map.WorldToImage(sPoints[j]); + colPoint.Add(map.WorldToImage(sPoints[j])); + bCheckStarted = true; + } + else if (bCheckStarted) + { + // fix bug curved line out of map in center segment of line + break; + } + } + + if (colPoint.Count > 1) + { + label.TextOnPathLabel = new TextOnPath(); + switch (label.Style.HorizontalAlignment) + { + case LabelStyle.HorizontalAlignmentEnum.Left: + label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Left; + break; + case LabelStyle.HorizontalAlignmentEnum.Right: + label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Right; + break; + case LabelStyle.HorizontalAlignmentEnum.Center: + label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; + break; + default: + label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; + break; + } + switch (label.Style.VerticalAlignment) + { + case LabelStyle.VerticalAlignmentEnum.Bottom: + label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.UnderPath; + break; + case LabelStyle.VerticalAlignmentEnum.Top: + label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.OverPath; + break; + case LabelStyle.VerticalAlignmentEnum.Middle: + label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; + break; + default: + label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; + break; + } + + var idxStartPath = 0; + var numberPoint = colPoint.Count; + // start Optimzes Path points + + var step = 100; + if (colPoint.Count >= step * 2) + { + numberPoint = step * 2; ; + switch (label.Style.HorizontalAlignment) + { + case LabelStyle.HorizontalAlignmentEnum.Left: + //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Left; + idxStartPath = 0; + break; + case LabelStyle.HorizontalAlignmentEnum.Right: + //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Right; + idxStartPath = colPoint.Count - step; + break; + case LabelStyle.HorizontalAlignmentEnum.Center: + //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; + idxStartPath = (int)colPoint.Count / 2 - step; + break; + default: + //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; + idxStartPath = (int)colPoint.Count / 2 - step; + break; + } + } + // end optimize path point + var points = new PointF[numberPoint]; + var count = 0; + if (colPoint[0].X <= colPoint[colPoint.Count - 1].X) + { + for (var l = idxStartPath; l < numberPoint + idxStartPath; l++) + { + points[count] = colPoint[l]; + count++; + } + } + else + { + //reverse the path + for (var k = numberPoint - 1 + idxStartPath; k >= idxStartPath; k--) + { + points[count] = colPoint[k]; + count++; + } + } + /* + //get text size in page units ie pixels + float textheight = label.Style.Font.Size; + switch (label.Style.Font.Unit) + { + case GraphicsUnit.Display: + textheight = textheight * g.DpiY / 75; + break; + case GraphicsUnit.Document: + textheight = textheight * g.DpiY / 300; + break; + case GraphicsUnit.Inch: + textheight = textheight * g.DpiY; + break; + case GraphicsUnit.Millimeter: + textheight = (float)(textheight / 25.4 * g.DpiY); + break; + case GraphicsUnit.Pixel: + //do nothing + break; + case GraphicsUnit.Point: + textheight = textheight * g.DpiY / 72; + break; + } + var topFont = new Font(label.Style.Font.FontFamily, textheight, label.Style.Font.Style, GraphicsUnit.Pixel); + */ + var topFont = label.Style.GetFontForGraphics(g); + // + var path = new GraphicsPath(); + path.AddLines(points); + + label.TextOnPathLabel.PathColorTop = System.Drawing.Color.Transparent; + label.TextOnPathLabel.Text = label.Text; + label.TextOnPathLabel.LetterSpacePercentage = 90; + label.TextOnPathLabel.FillColorTop = new System.Drawing.SolidBrush(label.Style.ForeColor); + label.TextOnPathLabel.Font = topFont; + label.TextOnPathLabel.PathDataTop = path.PathData; + label.TextOnPathLabel.Graphics = g; + //label.TextOnPathLabel.ShowPath=true; + //label.TextOnPathLabel.PathColorTop = System.Drawing.Color.YellowGreen; + if (label.Style.Halo != null) + { + label.TextOnPathLabel.ColorHalo = label.Style.Halo; + } + else + { + label.TextOnPathLabel.ColorHalo = null;// new System.Drawing.Pen(label.Style.ForeColor, (float)0.5); + } + path.Dispose(); + + // MeasureString to get region + label.TextOnPathLabel.MeasureString = true; + label.TextOnPathLabel.DrawTextOnPath(); + label.TextOnPathLabel.MeasureString = false; + // Get Region label for CollissionDetection here. + var pathRegion = new GraphicsPath(); + + if (label.TextOnPathLabel.RegionList.Count > 0) + { + //int idxCenter = (int)label.TextOnPathLabel.PointsText.Count / 2; + //System.Drawing.Drawing2D.Matrix rotationMatrix = g.Transform.Clone();// new Matrix(); + //rotationMatrix.RotateAt(label.TextOnPathLabel.Angles[idxCenter], label.TextOnPathLabel.PointsText[idxCenter]); + //if (label.TextOnPathLabel.PointsTextUp.Count > 0) + //{ + // for (int up = label.TextOnPathLabel.PointsTextUp.Count - 1; up >= 0; up--) + // { + // label.TextOnPathLabel.PointsText.Add(label.TextOnPathLabel.PointsTextUp[up]); + // } + + //} + pathRegion.AddRectangles(label.TextOnPathLabel.RegionList.ToArray()); + + // get box for detect colission here + label.Box = new LabelBox(pathRegion.GetBounds()); + //g.FillRectangle(System.Drawing.Brushes.YellowGreen, label.Box); + } + pathRegion.Dispose(); + } + + } + } +} diff --git a/SharpMap/Layers/Layer.cs b/SharpMap/Layers/Layer.cs index 844e6a8b..0523a0a8 100644 --- a/SharpMap/Layers/Layer.cs +++ b/SharpMap/Layers/Layer.cs @@ -1,556 +1,583 @@ -// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. - -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Drawing; -using GeoAPI.CoordinateSystems.Transformations; -using GeoAPI.Geometries; -using SharpMap.Base; -using SharpMap.Styles; - -namespace SharpMap.Layers -{ - /// - /// Abstract class for common layer properties - /// Implement this class instead of the ILayer interface to save a lot of common code. - /// - [Serializable] - public abstract partial class Layer : DisposableObject, ILayer - { - #region Events - - #region Delegates - - /// - /// EventHandler for event fired when the layer has been rendered - /// - /// Layer rendered - /// Reference to graphics object used for rendering - public delegate void LayerRenderedEventHandler(Layer layer, Graphics g); - - #endregion - - /// - /// Event fired when the layer has been rendered - /// - public event LayerRenderedEventHandler LayerRendered; - - /// - /// Event raised when the layer's property has changed - /// - public event EventHandler SRIDChanged; - - /// - /// Method called when has changed, to invoke - /// - /// The arguments associated with the event - protected virtual void OnSridChanged(EventArgs eventArgs) - { - var handler = SRIDChanged; - if (handler != null) handler(this, eventArgs); - } - - /// - /// Event raised when the layer's property has changed - /// - public event EventHandler StyleChanged; - - /// - /// Method called when has changed, to invoke - /// - /// The arguments associated with the event - protected virtual void OnStyleChanged(EventArgs eventArgs) - { - var handler = StyleChanged; - if (handler != null) handler(this, eventArgs); - } - - /// - /// Event raised when the layers's property has changed - /// - public event EventHandler LayerNameChanged; - - /// - /// Method called when has changed, to invoke - /// - /// The arguments associated with the event - protected virtual void OnLayerNameChanged(EventArgs eventArgs) - { - var handler = LayerNameChanged; - if (handler != null) handler(this, eventArgs); - } - - #endregion - - private ICoordinateTransformation _coordinateTransform; - private ICoordinateTransformation _reverseCoordinateTransform; - private IGeometryFactory _sourceFactory; - private IGeometryFactory _targetFactory; - - private string _layerName; - private string _layerTitle; - private IStyle _style; - private int _srid = -1; - private int? _targetSrid; - [field: NonSerialized] - private bool _shouldNotResetCt; - // ReSharper disable PublicConstructorInAbstractClass - /// - /// Creates an instance of this class using the given Style - /// - /// - public Layer(Style style) - // ReSharper restore PublicConstructorInAbstractClass - { - _style = style; - } - - /// - /// Creates an instance of this class - /// - protected Layer() //Style style) - { - _style = new Style(); - } - - /// - /// Releases managed resources - /// - protected override void ReleaseManagedResources() - { - _coordinateTransform = null; - _reverseCoordinateTransform = null; - _style = null; - - base.ReleaseManagedResources(); - } - - /// - /// Gets or sets the applied - /// to this vectorlayer prior to rendering - /// - public virtual ICoordinateTransformation CoordinateTransformation - { - get - { - if (_coordinateTransform == null && NeedsTransformation) - { - var css = Session.Instance.CoordinateSystemServices; - _coordinateTransform = css.CreateTransformation( - css.GetCoordinateSystem(SRID), css.GetCoordinateSystem(TargetSRID)); - } - return _coordinateTransform; - } - set - { - if (value == _coordinateTransform && value != null) - return; - - _coordinateTransform = value; - - try - { - // we don't want that by setting SRID we get the CoordinateTransformation resetted - _shouldNotResetCt = true; - - if (_coordinateTransform != null) - { - // causes sourceFactory/targetFactory to reset to new SRID/TargetSRID - SRID = Convert.ToInt32(CoordinateTransformation.SourceCS.AuthorityCode); - TargetSRID = Convert.ToInt32(CoordinateTransformation.TargetCS.AuthorityCode); - } - else - { - _sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(SRID); - // causes targetFactory to be cleared - TargetSRID = 0; - } - } - finally - { - _shouldNotResetCt = false; - } - - // check if ReverseTransform is required - if (_coordinateTransform == null || !NeedsTransformation) - _reverseCoordinateTransform = null; - - // check if existing ReverseTransform is compatible with CoordinateTransform - if (_reverseCoordinateTransform != null) - { - //clear if not compatible with CoordinateTransformation - if (_coordinateTransform.SourceCS.AuthorityCode != _coordinateTransform.TargetCS.AuthorityCode || - _coordinateTransform.TargetCS.AuthorityCode != _coordinateTransform.SourceCS.AuthorityCode) - _reverseCoordinateTransform = null; - } - - OnCoordinateTransformationChanged(EventArgs.Empty); - } - } - - /// - /// Event raised when the has changed - /// - public event EventHandler CoordinateTransformationChanged; - - /// - /// Event invoker for the event - /// - /// The event's arguments - protected virtual void OnCoordinateTransformationChanged(EventArgs e) - { - if (CoordinateTransformationChanged != null) - CoordinateTransformationChanged(this, e); - } - - /// - /// Gets the geometry factory to create source geometries - /// - protected internal IGeometryFactory SourceFactory { get { return _sourceFactory ?? (_sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(SRID)); } } - - /// - /// Gets the geometry factory to create target geometries - /// - protected internal IGeometryFactory TargetFactory { get { return _targetFactory ?? _sourceFactory; } } - - /// - /// Certain Transformations cannot be inverted in ProjNet, in those cases use this property to set the reverse (of CoordinateTransformation) to fetch data from Datasource - /// - /// If your CoordinateTransformation can be inverted you can leave this property to null - /// - public virtual ICoordinateTransformation ReverseCoordinateTransformation - { - get - { - if (_reverseCoordinateTransform == null && NeedsTransformation) - { - var css = Session.Instance.CoordinateSystemServices; - _reverseCoordinateTransform = css.CreateTransformation( - css.GetCoordinateSystem(TargetSRID), css.GetCoordinateSystem(SRID)); - } - return _reverseCoordinateTransform; - } - set - { - if (value == _reverseCoordinateTransform) - return; - _reverseCoordinateTransform = value; - } - } - - protected bool NeedsTransformation - { - get { return SRID != 0 && TargetSRID != 0 && SRID != TargetSRID; } - } - - #region ILayer Members - - /// - /// Gets or sets the name of the layer - /// - public string LayerName - { - get { return _layerName; } - set { _layerName = value; } - } - - /// - /// Gets or sets the title of the layer - /// - public string LayerTitle - { - get { return _layerTitle; } - set { _layerTitle = value; } - } - - /// - /// The spatial reference ID (CRS) - /// - public virtual int SRID - { - get { return _srid; } - set - { - if (value != _srid) - { - _srid = value; - - _sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(value); - if (!_shouldNotResetCt) - _coordinateTransform = _reverseCoordinateTransform = null; - - OnSridChanged(EventArgs.Empty); - } - } - } - - /// - /// Gets or sets a value indicating the target spatial reference id - /// - public virtual int TargetSRID - { - get { return _targetSrid.HasValue ? _targetSrid.Value : SRID; } - set - { - if (value == SRID || value == 0) - { - _targetSrid = null; - _targetFactory = null; - } - else if (_targetSrid != value) - { - _targetSrid = value; - _targetFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(value); - } - if (!_shouldNotResetCt) - _coordinateTransform = _reverseCoordinateTransform = null; - } - } - - //public abstract SharpMap.CoordinateSystems.CoordinateSystem CoordinateSystem { get; set; } - - - /// - /// Renders the layer - /// - /// Graphics object reference - /// Map which is rendered - [Obsolete("Use Render(Graphics, MapViewport)")] - public virtual void Render(Graphics g, Map map) - { - Render(g, (MapViewport)map); - } - - /// - /// Renders the layer - /// - /// Graphics object reference - /// Map which is rendered - public virtual void Render(Graphics g, MapViewport map) - { - OnLayerRendered(g); - } - - /// - /// Event invoker for the event. - /// - /// The graphics object - protected virtual void OnLayerRendered(Graphics g) - { - if (LayerRendered != null) - LayerRendered(this, g); - } - - /// - /// Returns the extent of the layer - /// - /// Bounding box corresponding to the extent of the features in the layer - public abstract Envelope Envelope { get; } - - #endregion - - #region Properties - - /// - /// Proj4 projection definition string - /// - public string Proj4Projection { get; set; } - /* - private bool _Enabled = true; - private double _MaxVisible = double.MaxValue; - private double _MinVisible = 0; - */ - /// - /// Minimum visibility zoom, including this value - /// - public double MinVisible - { - get - { - return _style.MinVisible; // return _MinVisible; - } - set - { - _style.MinVisible = value; // _MinVisible = value; - } - } - - /// - /// Maximum visibility zoom, excluding this value - /// - public double MaxVisible - { - get - { - //return _MaxVisible; - return _style.MaxVisible; - } - set - { - //_MaxVisible = value; - _style.MaxVisible = value; - } - } - - /// - /// Gets or Sets what kind of units the Min/Max visible properties are defined in - /// - public VisibilityUnits VisibilityUnits - { - get - { - return _style.VisibilityUnits; - } - set - { - _style.VisibilityUnits = value; - } - } - - /// - /// Specified whether the layer is rendered or not - /// - public bool Enabled - { - get - { - //return _Enabled; - return _style.Enabled; - } - set - { - //_Enabled = value; - _style.Enabled = value; - } - } - - /// - /// Gets or sets the Style for this Layer - /// - public virtual IStyle Style - { - get { return _style; } - set - { - if (value != _style && !_style.Equals(value)) - { - _style = value; - OnStyleChanged(EventArgs.Empty); - } - } - } - - #endregion - - /// - /// Returns the name of the layer. - /// - /// - public override string ToString() - { - return LayerName; - } - - #region Reprojection utility functions - - /// - /// Utility function to transform given envelope using a specific transformation - /// - /// The source envelope - /// The to use. - /// The target envelope - protected virtual Envelope ToTarget(Envelope envelope, ICoordinateTransformation coordinateTransformation) - { - if (coordinateTransformation == null) - return envelope; - - return GeometryTransform.TransformBox(envelope, coordinateTransformation.MathTransform); - } - - /// - /// Utility function to transform given envelope to the target envelope - /// - /// The source envelope - /// The target envelope - protected Envelope ToTarget(Envelope envelope) - { - return ToTarget(envelope, CoordinateTransformation); - } - - /// - /// Utility function to transform given envelope to the source envelope - /// - /// The target envelope - /// The source envelope - protected virtual Envelope ToSource(Envelope envelope) - { - if (ReverseCoordinateTransformation != null) - { - return GeometryTransform.TransformBox(envelope, ReverseCoordinateTransformation.MathTransform); - } - - if (CoordinateTransformation != null) - { - var mt = CoordinateTransformation.MathTransform; - mt.Invert(); - var res = GeometryTransform.TransformBox(envelope, mt); - mt.Invert(); - return res; - } - - // no transformation - return envelope; - } - - protected virtual IGeometry ToTarget(IGeometry geometry) - { - if (geometry.SRID == TargetSRID) - return geometry; - - if (CoordinateTransformation != null) - { - return GeometryTransform.TransformGeometry(geometry, CoordinateTransformation.MathTransform, TargetFactory); - } - - return geometry; - } - - protected virtual IGeometry ToSource(IGeometry geometry) - { - if (geometry.SRID == SRID) - return geometry; - - if (ReverseCoordinateTransformation != null) - { - return GeometryTransform.TransformGeometry(geometry, - ReverseCoordinateTransformation.MathTransform, SourceFactory); - } - if (CoordinateTransformation != null) - { - var mt = CoordinateTransformation.MathTransform; - mt.Invert(); - var res = GeometryTransform.TransformGeometry(geometry, mt, SourceFactory); - mt.Invert(); - return res; - } - - return geometry; - } - - #endregion - } -} +// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) +// +// This file is part of SharpMap. +// SharpMap is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// SharpMap is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public License +// along with SharpMap; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using System; +using System.Drawing; +using GeoAPI.CoordinateSystems.Transformations; +using GeoAPI.Geometries; +using SharpMap.Base; +using SharpMap.Styles; + +namespace SharpMap.Layers +{ + /// + /// Abstract class for common layer properties + /// Implement this class instead of the ILayer interface to save a lot of common code. + /// + [Serializable] + public abstract partial class Layer : DisposableObject, ILayerEx + { + #region Events + + #region Delegates + + /// + /// EventHandler for event fired when the layer has been rendered + /// + /// Layer rendered + /// Reference to graphics object used for rendering + public delegate void LayerRenderedEventHandler(Layer layer, Graphics g); + + #endregion + + /// + /// Event fired when the layer has been rendered + /// + public event LayerRenderedEventHandler LayerRendered; + + /// + /// Event raised when the layer's property has changed + /// + public event EventHandler SRIDChanged; + + /// + /// Method called when has changed, to invoke + /// + /// The arguments associated with the event + protected virtual void OnSridChanged(EventArgs eventArgs) + { + var handler = SRIDChanged; + if (handler != null) handler(this, eventArgs); + } + + /// + /// Event raised when the layer's property has changed + /// + public event EventHandler StyleChanged; + + /// + /// Method called when has changed, to invoke + /// + /// The arguments associated with the event + protected virtual void OnStyleChanged(EventArgs eventArgs) + { + var handler = StyleChanged; + if (handler != null) handler(this, eventArgs); + } + + /// + /// Event raised when the layers's property has changed + /// + public event EventHandler LayerNameChanged; + + /// + /// Method called when has changed, to invoke + /// + /// The arguments associated with the event + protected virtual void OnLayerNameChanged(EventArgs eventArgs) + { + var handler = LayerNameChanged; + if (handler != null) handler(this, eventArgs); + } + + #endregion + + private ICoordinateTransformation _coordinateTransform; + private ICoordinateTransformation _reverseCoordinateTransform; + private IGeometryFactory _sourceFactory; + private IGeometryFactory _targetFactory; + + private string _layerName; + private string _layerTitle; + private IStyle _style; + private int _srid = -1; + private int? _targetSrid; + [field: NonSerialized] + private bool _shouldNotResetCt; + + [field: NonSerialized] + protected Envelope _affectedArea = new Envelope(); + + // ReSharper disable PublicConstructorInAbstractClass + /// + /// Creates an instance of this class using the given Style + /// + /// + public Layer(Style style) + // ReSharper restore PublicConstructorInAbstractClass + { + _style = style; + } + + /// + /// Creates an instance of this class + /// + protected Layer() //Style style) + { + _style = new Style(); + } + + /// + /// Releases managed resources + /// + protected override void ReleaseManagedResources() + { + _coordinateTransform = null; + _reverseCoordinateTransform = null; + _style = null; + + base.ReleaseManagedResources(); + } + + /// + /// Gets or sets the applied + /// to this vectorlayer prior to rendering + /// + public virtual ICoordinateTransformation CoordinateTransformation + { + get + { + if (_coordinateTransform == null && NeedsTransformation) + { + var css = Session.Instance.CoordinateSystemServices; + _coordinateTransform = css.CreateTransformation( + css.GetCoordinateSystem(SRID), css.GetCoordinateSystem(TargetSRID)); + } + return _coordinateTransform; + } + set + { + if (value == _coordinateTransform && value != null) + return; + + _coordinateTransform = value; + + try + { + // we don't want that by setting SRID we get the CoordinateTransformation resetted + _shouldNotResetCt = true; + + if (_coordinateTransform != null) + { + // causes sourceFactory/targetFactory to reset to new SRID/TargetSRID + SRID = Convert.ToInt32(CoordinateTransformation.SourceCS.AuthorityCode); + TargetSRID = Convert.ToInt32(CoordinateTransformation.TargetCS.AuthorityCode); + } + else + { + _sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(SRID); + // causes targetFactory to be cleared + TargetSRID = 0; + } + } + finally + { + _shouldNotResetCt = false; + } + + // check if ReverseTransform is required + if (_coordinateTransform == null || !NeedsTransformation) + _reverseCoordinateTransform = null; + + // check if existing ReverseTransform is compatible with CoordinateTransform + if (_reverseCoordinateTransform != null) + { + //clear if not compatible with CoordinateTransformation + if (_coordinateTransform.SourceCS.AuthorityCode != _coordinateTransform.TargetCS.AuthorityCode || + _coordinateTransform.TargetCS.AuthorityCode != _coordinateTransform.SourceCS.AuthorityCode) + _reverseCoordinateTransform = null; + } + + OnCoordinateTransformationChanged(EventArgs.Empty); + } + } + + /// + /// Event raised when the has changed + /// + public event EventHandler CoordinateTransformationChanged; + + /// + /// Event invoker for the event + /// + /// The event's arguments + protected virtual void OnCoordinateTransformationChanged(EventArgs e) + { + if (CoordinateTransformationChanged != null) + CoordinateTransformationChanged(this, e); + } + + /// + /// Gets the geometry factory to create source geometries + /// + protected internal IGeometryFactory SourceFactory { get { return _sourceFactory ?? (_sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(SRID)); } } + + /// + /// Gets the geometry factory to create target geometries + /// + protected internal IGeometryFactory TargetFactory { get { return _targetFactory ?? _sourceFactory; } } + + /// + /// Certain Transformations cannot be inverted in ProjNet, in those cases use this property to set the reverse (of CoordinateTransformation) to fetch data from Datasource + /// + /// If your CoordinateTransformation can be inverted you can leave this property to null + /// + public virtual ICoordinateTransformation ReverseCoordinateTransformation + { + get + { + if (_reverseCoordinateTransform == null && NeedsTransformation) + { + var css = Session.Instance.CoordinateSystemServices; + _reverseCoordinateTransform = css.CreateTransformation( + css.GetCoordinateSystem(TargetSRID), css.GetCoordinateSystem(SRID)); + } + return _reverseCoordinateTransform; + } + set + { + if (value == _reverseCoordinateTransform) + return; + _reverseCoordinateTransform = value; + } + } + + protected bool NeedsTransformation + { + get { return SRID != 0 && TargetSRID != 0 && SRID != TargetSRID; } + } + + #region ILayer Members + + /// + /// Gets or sets the name of the layer + /// + public string LayerName + { + get { return _layerName; } + set { _layerName = value; } + } + + /// + /// Gets or sets the title of the layer + /// + public string LayerTitle + { + get { return _layerTitle; } + set { _layerTitle = value; } + } + + /// + /// The spatial reference ID (CRS) + /// + public virtual int SRID + { + get { return _srid; } + set + { + if (value != _srid) + { + _srid = value; + + _sourceFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(value); + if (!_shouldNotResetCt) + _coordinateTransform = _reverseCoordinateTransform = null; + + OnSridChanged(EventArgs.Empty); + } + } + } + + /// + /// Gets or sets a value indicating the target spatial reference id + /// + public virtual int TargetSRID + { + get { return _targetSrid.HasValue ? _targetSrid.Value : SRID; } + set + { + if (value == SRID || value == 0) + { + _targetSrid = null; + _targetFactory = null; + } + else if (_targetSrid != value) + { + _targetSrid = value; + _targetFactory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(value); + } + if (!_shouldNotResetCt) + _coordinateTransform = _reverseCoordinateTransform = null; + } + } + + //public abstract SharpMap.CoordinateSystems.CoordinateSystem CoordinateSystem { get; set; } + + + /// + /// Renders the layer + /// + /// Graphics object reference + /// Map which is rendered + [Obsolete("Use Render(Graphics, MapViewport, out Envelope affectedArea)")] + public virtual void Render(Graphics g, Map map) + { + Render(g, (MapViewport)map); + } + + /// + /// Renders the layer using the current viewport + /// + /// Graphics object reference + /// Map which is rendered + public virtual void Render(Graphics g, MapViewport map) + { + OnLayerRendered(g); + } + + /// + /// Renders the layer using the current viewport + /// + /// Graphics object reference + /// Map which is rendered + /// The actual extent of rendered data inclusive of any labels or vector symbology + public virtual void Render(Graphics g, MapViewport map, out Envelope affectedArea) + { + Render(g, map); + + if (_affectedArea.IsNull) + { + affectedArea = map.Envelope.Intersection(Envelope); + } + else + { + affectedArea = map.Envelope.Intersection(_affectedArea); + _affectedArea.SetToNull(); + } + + // OnLayerRendered already raised by Base.Render(Graphics g,m MapViewport map) + } + + /// + /// Event invoker for the event. + /// + /// The graphics object + protected virtual void OnLayerRendered(Graphics g) + { + if (LayerRendered != null) + LayerRendered(this, g); + } + + /// + /// Returns the extent of the layer + /// + /// Bounding box corresponding to the extent of the features in the layer + public abstract Envelope Envelope { get; } + + #endregion + + #region Properties + + /// + /// Proj4 projection definition string + /// + public string Proj4Projection { get; set; } + /* + private bool _Enabled = true; + private double _MaxVisible = double.MaxValue; + private double _MinVisible = 0; + */ + /// + /// Minimum visibility zoom, including this value + /// + public double MinVisible + { + get + { + return _style.MinVisible; // return _MinVisible; + } + set + { + _style.MinVisible = value; // _MinVisible = value; + } + } + + /// + /// Maximum visibility zoom, excluding this value + /// + public double MaxVisible + { + get + { + //return _MaxVisible; + return _style.MaxVisible; + } + set + { + //_MaxVisible = value; + _style.MaxVisible = value; + } + } + + /// + /// Gets or Sets what kind of units the Min/Max visible properties are defined in + /// + public VisibilityUnits VisibilityUnits + { + get + { + return _style.VisibilityUnits; + } + set + { + _style.VisibilityUnits = value; + } + } + + /// + /// Specified whether the layer is rendered or not + /// + public bool Enabled + { + get + { + //return _Enabled; + return _style.Enabled; + } + set + { + //_Enabled = value; + _style.Enabled = value; + } + } + + /// + /// Gets or sets the Style for this Layer + /// + public virtual IStyle Style + { + get { return _style; } + set + { + if (value != _style && !_style.Equals(value)) + { + _style = value; + OnStyleChanged(EventArgs.Empty); + } + } + } + + #endregion + + /// + /// Returns the name of the layer. + /// + /// + public override string ToString() + { + return LayerName; + } + + #region Reprojection utility functions + + /// + /// Utility function to transform given envelope using a specific transformation + /// + /// The source envelope + /// The to use. + /// The target envelope + protected virtual Envelope ToTarget(Envelope envelope, ICoordinateTransformation coordinateTransformation) + { + if (coordinateTransformation == null) + return envelope; + + return GeometryTransform.TransformBox(envelope, coordinateTransformation.MathTransform); + } + + /// + /// Utility function to transform given envelope to the target envelope + /// + /// The source envelope + /// The target envelope + protected Envelope ToTarget(Envelope envelope) + { + return ToTarget(envelope, CoordinateTransformation); + } + + /// + /// Utility function to transform given envelope to the source envelope + /// + /// The target envelope + /// The source envelope + protected virtual Envelope ToSource(Envelope envelope) + { + if (ReverseCoordinateTransformation != null) + { + return GeometryTransform.TransformBox(envelope, ReverseCoordinateTransformation.MathTransform); + } + + if (CoordinateTransformation != null) + { + var mt = CoordinateTransformation.MathTransform; + mt.Invert(); + var res = GeometryTransform.TransformBox(envelope, mt); + mt.Invert(); + return res; + } + + // no transformation + return envelope; + } + + protected virtual IGeometry ToTarget(IGeometry geometry) + { + if (geometry.SRID == TargetSRID) + return geometry; + + if (CoordinateTransformation != null) + { + return GeometryTransform.TransformGeometry(geometry, CoordinateTransformation.MathTransform, TargetFactory); + } + + return geometry; + } + + protected virtual IGeometry ToSource(IGeometry geometry) + { + if (geometry.SRID == SRID) + return geometry; + + if (ReverseCoordinateTransformation != null) + { + return GeometryTransform.TransformGeometry(geometry, + ReverseCoordinateTransformation.MathTransform, SourceFactory); + } + if (CoordinateTransformation != null) + { + var mt = CoordinateTransformation.MathTransform; + mt.Invert(); + var res = GeometryTransform.TransformGeometry(geometry, mt, SourceFactory); + mt.Invert(); + return res; + } + + return geometry; + } + + #endregion + } +} diff --git a/SharpMap/Layers/VectorLayer.cs b/SharpMap/Layers/VectorLayer.cs index 5e06ff38..f2128456 100644 --- a/SharpMap/Layers/VectorLayer.cs +++ b/SharpMap/Layers/VectorLayer.cs @@ -1,674 +1,673 @@ -// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. - -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.ObjectModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using SharpMap.Data; -using SharpMap.Data.Providers; -using GeoAPI.Geometries; -using SharpMap.Rendering; -using SharpMap.Rendering.Thematics; -using SharpMap.Styles; -using System.Collections.Generic; -using Common.Logging; - -namespace SharpMap.Layers -{ - /// - /// Class for vector layer properties - /// - [Serializable] - public class VectorLayer : Layer, ICanQueryLayer, ICloneable - { - static readonly ILog _logger = LogManager.GetLogger(typeof(VectorLayer)); - - private bool _clippingEnabled; - private bool _isQueryEnabled = true; - private IBaseProvider _dataSource; - private SmoothingMode _smoothingMode; - private ITheme _theme; - private Envelope _envelope; - - /// - /// Initializes a new layer - /// - /// Name of layer - public VectorLayer(string layername) - : base(new VectorStyle()) - { - LayerName = layername; - SmoothingMode = SmoothingMode.AntiAlias; - } - - /// - /// Initializes a new layer with a specified datasource - /// - /// Name of layer - /// Data source - public VectorLayer(string layername, IBaseProvider dataSource) : this(layername) - { - _dataSource = dataSource; - } - /// - /// Gets or sets a Dictionary with themes suitable for this layer. A theme in the dictionary can be used for rendering be setting the Theme Property using a delegate function - /// - public Dictionary Themes - { - get; - set; - } - - - - /// - /// Gets or sets thematic settings for the layer. Set to null to ignore thematics - /// - public ITheme Theme - { - get { return _theme; } - set { _theme = value; } - } - - /// - /// Specifies whether polygons should be clipped prior to rendering - /// - /// - /// Clipping will clip and - /// to the current view prior - /// to rendering the object. - /// Enabling clipping might improve rendering speed if you are rendering - /// only small portions of very large objects. - /// - public bool ClippingEnabled - { - get { return _clippingEnabled; } - set { _clippingEnabled = value; } - } - - /// - /// Render whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas - /// - public SmoothingMode SmoothingMode - { - get { return _smoothingMode; } - set { _smoothingMode = value; } - } - - /// - /// Gets or sets the datasource - /// - public IBaseProvider DataSource - { - get { return _dataSource; } - set - { - _dataSource = value; - _envelope = null; - } - } - - /// - /// Gets or sets the rendering style of the vector layer. - /// - public new VectorStyle Style - { - get { return base.Style as VectorStyle; } - set { base.Style = value; } - } - - /// - /// Returns the extent of the layer - /// - /// Bounding box corresponding to the extent of the features in the layer - public override Envelope Envelope - { - get - { - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - - if (_envelope != null && CacheExtent) - return ToTarget(_envelope.Copy()); - - Envelope box; - lock (_dataSource) - { - // Is datasource already open? - bool wasOpen = DataSource.IsOpen; - if (!wasOpen) { DataSource.Open(); } - - box = DataSource.GetExtents(); - - if (!wasOpen) { DataSource.Close(); } - } - - if (CacheExtent) - _envelope = box; - - return ToTarget(box); - } - } - /// - /// Gets or sets a value indicating whether the layer envelope should be treated as static or not. - /// - /// - /// When CacheExtent is enabled the layer Envelope will be calculated only once from DataSource, this - /// helps to speed up the Envelope calculation with some DataProviders. Default is false for backward - /// compatibility. - /// - public virtual bool CacheExtent { get; set; } - - /// - /// Gets or sets the SRID of this VectorLayer's data source - /// - public override int SRID - { - get - { - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - - return DataSource.SRID; - } - set - { - DataSource.SRID = value; - base.SRID = value; - } - } - - #region IDisposable Members - - /// - /// Disposes the object - /// - protected override void ReleaseManagedResources() - { - if (DataSource != null) - DataSource.Dispose(); - base.ReleaseManagedResources(); - } - - #endregion - - /// - /// Renders the layer to a graphics object, using the given map viewport - /// - /// Graphics object reference - /// Map which is rendered - public override void Render(Graphics g, MapViewport map) - { - if (map.Center == null) - throw (new ApplicationException("Cannot render map. View center not specified")); - - g.SmoothingMode = SmoothingMode; - var envelope = ToSource(map.Envelope); //View to render - - if (DataSource == null) - throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - - //If thematics is enabled, we use a slighty different rendering approach - if (Theme != null) - RenderInternal(g, map, envelope, Theme); - else - RenderInternal(g, map, envelope); - - - base.Render(g, map); - } - - /// - /// Method to render this layer to the map, applying . - /// - /// The graphics object - /// The map object - /// The envelope to render - /// The theme to apply - protected void RenderInternal(Graphics g, MapViewport map, Envelope envelope, ITheme theme) - { - var ds = new FeatureDataSet(); - lock (_dataSource) - { - // Is datasource already open? - bool wasOpen = DataSource.IsOpen; - if (!wasOpen) { DataSource.Open(); } - - DataSource.ExecuteIntersectionQuery(envelope, ds); - - if (!wasOpen) { DataSource.Close(); } - } - - double scale = map.GetMapScale((int)g.DpiX); - double zoom = map.Zoom; - - Func evalStyle; - - if (theme is IThemeEx) - evalStyle = new ThemeExEvaluator((IThemeEx)theme).GetStyle; - else - evalStyle = new ThemeEvaluator(theme).GetStyle; - - foreach (FeatureDataTable features in ds.Tables) - { - // Transform geometries if necessary - if (CoordinateTransformation != null) - { - for (int i = 0; i < features.Count; i++) - { - features[i].Geometry = ToTarget(features[i].Geometry); - } - } - - //Linestring outlines is drawn by drawing the layer once with a thicker line - //before drawing the "inline" on top. - if (Style.EnableOutline) - { - for (int i = 0; i < features.Count; i++) - { - var feature = features[i]; - var outlineStyle = evalStyle(map, feature) as VectorStyle; - if (outlineStyle == null) continue; - if (!(outlineStyle.Enabled && outlineStyle.EnableOutline)) continue; - - var compare = outlineStyle.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale; - - if (!(outlineStyle.MinVisible <= compare && compare <= outlineStyle.MaxVisible)) continue; - - using (outlineStyle = outlineStyle.Clone()) - { - if (outlineStyle != null) - { - //Draw background of all line-outlines first - if (feature.Geometry is ILineString) - { - VectorRenderer.DrawLineString(g, feature.Geometry as ILineString, outlineStyle.Outline, - map, outlineStyle.LineOffset); - } - else if (feature.Geometry is IMultiLineString) - { - VectorRenderer.DrawMultiLineString(g, feature.Geometry as IMultiLineString, - outlineStyle.Outline, map, outlineStyle.LineOffset); - } - } - } - } - } - - - for (int i = 0; i < features.Count; i++) - { - var feature = features[i]; - var style = evalStyle(map, feature); - if (style == null) continue; - if (!style.Enabled) continue; - - double compare = style.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale; - - if (!(style.MinVisible <= compare && compare <= style.MaxVisible)) continue; - - - IEnumerable stylesToRender = GetStylesToRender(style); - - if (stylesToRender == null) - return; - - foreach (var vstyle in stylesToRender) - { - if (!(vstyle is VectorStyle) || !vstyle.Enabled) - continue; - - using (var clone = (vstyle as VectorStyle).Clone()) - { - if (clone != null) - { - RenderGeometry(g, map, feature.Geometry, clone); - } - } - } - } - } - } - - /// - /// Method to render this layer to the map, applying . - /// - /// The graphics object - /// The map object - /// The envelope to render - protected void RenderInternal(Graphics g, MapViewport map, Envelope envelope) - { - //if style is not enabled, we don't need to render anything - if (!Style.Enabled) return; - - IEnumerable stylesToRender = GetStylesToRender(Style); - - if (stylesToRender == null) - return; - - Collection geoms = null; - - foreach (var style in stylesToRender) - { - if (!(style is VectorStyle) || !style.Enabled) - continue; - using (var vStyle = (style as VectorStyle).Clone()) - { - if (vStyle != null) - { - if (geoms == null) - { - lock (_dataSource) - { - // Is datasource already open? - bool wasOpen = DataSource.IsOpen; - if (!wasOpen) { DataSource.Open(); } - - // Read data - geoms = DataSource.GetGeometriesInView(envelope); - - if (!wasOpen) { DataSource.Close(); } - } - - if (_logger.IsDebugEnabled) - { - _logger.DebugFormat("Layer {0}, NumGeometries {1}", LayerName, geoms.Count); - } - - // Transform geometries if necessary - if (CoordinateTransformation != null) - { - for (int i = 0; i < geoms.Count; i++) - { - geoms[i] = ToTarget(geoms[i]); - } - } - } - - if (vStyle.LineSymbolizer != null) - { - vStyle.LineSymbolizer.Begin(g, map, geoms.Count); - } - else - { - //Linestring outlines is drawn by drawing the layer once with a thicker line - //before drawing the "inline" on top. - if (vStyle.EnableOutline) - { - foreach (var geom in geoms) - { - if (geom != null) - { - //Draw background of all line-outlines first - if (geom is ILineString) - VectorRenderer.DrawLineString(g, geom as ILineString, vStyle.Outline, map, vStyle.LineOffset); - else if (geom is IMultiLineString) - VectorRenderer.DrawMultiLineString(g, geom as IMultiLineString, vStyle.Outline, map, vStyle.LineOffset); - } - } - } - } - - foreach (IGeometry geom in geoms) - { - if (geom != null) - RenderGeometry(g, map, geom, vStyle); - } - - if (vStyle.LineSymbolizer != null) - { - vStyle.LineSymbolizer.Symbolize(g, map); - vStyle.LineSymbolizer.End(g, map); - } - } - } - } - } - - /// - /// Unpacks styles to render (can be nested group-styles) - /// - /// - /// - public static IEnumerable GetStylesToRender(IStyle style) - { - IStyle[] stylesToRender = null; - if (style is GroupStyle) - { - var gs = style as GroupStyle; - var styles = new List(); - for (var i = 0; i < gs.Count; i++) - { - styles.AddRange(GetStylesToRender(gs[i])); - } - stylesToRender = styles.ToArray(); - } - else if (style is VectorStyle) - { - stylesToRender = new[] { style }; - } - - return stylesToRender; - } - - /// - /// Method to render using - /// - /// The graphics object - /// The map - /// The feature's geometry - /// The style to apply - protected void RenderGeometry(Graphics g, MapViewport map, IGeometry feature, VectorStyle style) - { - if (feature == null) - return; - - var geometryType = feature.OgcGeometryType; - switch (geometryType) - { - case OgcGeometryType.Polygon: - if (style.EnableOutline) - VectorRenderer.DrawPolygon(g, (IPolygon)feature, style.Fill, style.Outline, _clippingEnabled, - map); - else - VectorRenderer.DrawPolygon(g, (IPolygon)feature, style.Fill, null, _clippingEnabled, map); - break; - case OgcGeometryType.MultiPolygon: - if (style.EnableOutline) - VectorRenderer.DrawMultiPolygon(g, (IMultiPolygon)feature, style.Fill, style.Outline, - _clippingEnabled, map); - else - VectorRenderer.DrawMultiPolygon(g, (IMultiPolygon)feature, style.Fill, null, _clippingEnabled, - map); - break; - case OgcGeometryType.LineString: - if (style.LineSymbolizer != null) - { - style.LineSymbolizer.Render(map, (ILineString)feature, g); - return; - } - VectorRenderer.DrawLineString(g, (ILineString)feature, style.Line, map, style.LineOffset); - return; - case OgcGeometryType.MultiLineString: - if (style.LineSymbolizer != null) - { - style.LineSymbolizer.Render(map, (IMultiLineString)feature, g); - return; - } - VectorRenderer.DrawMultiLineString(g, (IMultiLineString)feature, style.Line, map, style.LineOffset); - break; - case OgcGeometryType.Point: - if (style.PointSymbolizer != null) - { - VectorRenderer.DrawPoint(style.PointSymbolizer, g, (IPoint)feature, map); - return; - } - - if (style.Symbol != null || style.PointColor == null) - { - VectorRenderer.DrawPoint(g, (IPoint)feature, style.Symbol, style.SymbolScale, style.SymbolOffset, - style.SymbolRotation, map); - return; - } - VectorRenderer.DrawPoint(g, (IPoint)feature, style.PointColor, style.PointSize, style.SymbolOffset, map); - - break; - case OgcGeometryType.MultiPoint: - if (style.PointSymbolizer != null) - { - VectorRenderer.DrawMultiPoint(style.PointSymbolizer, g, (IMultiPoint)feature, map); - } - if (style.Symbol != null || style.PointColor == null) - { - VectorRenderer.DrawMultiPoint(g, (IMultiPoint)feature, style.Symbol, style.SymbolScale, - style.SymbolOffset, style.SymbolRotation, map); - } - else - { - VectorRenderer.DrawMultiPoint(g, (IMultiPoint)feature, style.PointColor, style.PointSize, style.SymbolOffset, map); - } - break; - case OgcGeometryType.GeometryCollection: - var coll = (IGeometryCollection)feature; - for (var i = 0; i < coll.NumGeometries; i++) - { - IGeometry geom = coll[i]; - RenderGeometry(g, map, geom, style); - } - break; - default: - break; - } - } - - #region Implementation of ICanQueryLayer - - /// - /// Returns the data associated with all the geometries that are intersected by 'geom' - /// - /// Geometry to intersect with - /// FeatureDataSet to fill data into - public void ExecuteIntersectionQuery(Envelope box, FeatureDataSet ds) - { - box = ToSource(box); - - int tableCount = ds.Tables.Count; - - lock (_dataSource) - { - // Is datasource already open? - bool wasOpen = _dataSource.IsOpen; - if (!wasOpen) { _dataSource.Open(); } - - _dataSource.ExecuteIntersectionQuery(box, ds); - - if (!wasOpen) { DataSource.Close(); } - } - - if (ds.Tables.Count > tableCount) - { - //We added a table, name it according to layer - ds.Tables[ds.Tables.Count - 1].TableName = LayerName; - } - } - - /// - /// Returns the data associated with all the geometries that are intersected by 'geom' - /// - /// Geometry to intersect with - /// FeatureDataSet to fill data into - public void ExecuteIntersectionQuery(IGeometry geometry, FeatureDataSet ds) - { - geometry = ToSource(geometry); - - int tableCount = ds.Tables.Count; - - lock (_dataSource) - { - // Is datasource already open? - bool wasOpen = DataSource.IsOpen; - if (!wasOpen) { DataSource.Open(); } - - _dataSource.ExecuteIntersectionQuery(geometry, ds); - - if (!wasOpen) { DataSource.Close(); } - } - - if (ds.Tables.Count > tableCount) - { - //We added a table, name it according to layer - ds.Tables[ds.Tables.Count - 1].TableName = LayerName; - } - } - - /// - /// Whether the layer is queryable when used in a SharpMap.Web.Wms.WmsServer, ExecuteIntersectionQuery() will be possible in all other situations when set to FALSE - /// - public bool IsQueryEnabled - { - get { return _isQueryEnabled; } - set { _isQueryEnabled = value; } - } - - #endregion - - public object Clone() - { - var res = (VectorLayer)MemberwiseClone(); - res.Style = Style.Clone(); - if (Theme is ICloneable) - res.Theme = (ITheme)((ICloneable)Theme).Clone(); - return res; - } - - #region Theme evaluators - private abstract class ThemeEvaluatorBase - { - public abstract IStyle GetStyle(MapViewport mvp, FeatureDataRow feature); - } - - private class ThemeEvaluator : ThemeEvaluatorBase - { - private readonly ITheme _theme; - - public ThemeEvaluator(ITheme theme) - { - _theme = theme; - } - public sealed override IStyle GetStyle(MapViewport mvp, FeatureDataRow feature) - { - return _theme.GetStyle(feature); - } - } - - private class ThemeExEvaluator : ThemeEvaluatorBase - { - private readonly IThemeEx _theme; - - public ThemeExEvaluator(IThemeEx theme) - { - _theme = theme; - } - public sealed override IStyle GetStyle(MapViewport mvp, FeatureDataRow feature) - { - return _theme.GetStyle(mvp, feature); - } - } - #endregion - } -} +// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) +// +// This file is part of SharpMap. +// SharpMap is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// SharpMap is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public License +// along with SharpMap; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using System; +using System.Collections.ObjectModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using SharpMap.Data; +using SharpMap.Data.Providers; +using GeoAPI.Geometries; +using SharpMap.Rendering; +using SharpMap.Rendering.Thematics; +using SharpMap.Styles; +using System.Collections.Generic; +using Common.Logging; + +namespace SharpMap.Layers +{ + /// + /// Class for vector layer properties + /// + [Serializable] + public class VectorLayer : Layer, ICanQueryLayer, ICloneable + { + static readonly ILog _logger = LogManager.GetLogger(typeof(VectorLayer)); + + private bool _clippingEnabled; + private bool _isQueryEnabled = true; + private IBaseProvider _dataSource; + private SmoothingMode _smoothingMode; + private ITheme _theme; + private Envelope _envelope; + + /// + /// Initializes a new layer + /// + /// Name of layer + public VectorLayer(string layername) + : base(new VectorStyle()) + { + LayerName = layername; + SmoothingMode = SmoothingMode.AntiAlias; + } + + /// + /// Initializes a new layer with a specified datasource + /// + /// Name of layer + /// Data source + public VectorLayer(string layername, IBaseProvider dataSource) : this(layername) + { + _dataSource = dataSource; + } + /// + /// Gets or sets a Dictionary with themes suitable for this layer. A theme in the dictionary can be used for rendering be setting the Theme Property using a delegate function + /// + public Dictionary Themes + { + get; + set; + } + + + + /// + /// Gets or sets thematic settings for the layer. Set to null to ignore thematics + /// + public ITheme Theme + { + get { return _theme; } + set { _theme = value; } + } + + /// + /// Specifies whether polygons should be clipped prior to rendering + /// + /// + /// Clipping will clip and + /// to the current view prior + /// to rendering the object. + /// Enabling clipping might improve rendering speed if you are rendering + /// only small portions of very large objects. + /// + public bool ClippingEnabled + { + get { return _clippingEnabled; } + set { _clippingEnabled = value; } + } + + /// + /// Render whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas + /// + public SmoothingMode SmoothingMode + { + get { return _smoothingMode; } + set { _smoothingMode = value; } + } + + /// + /// Gets or sets the datasource + /// + public IBaseProvider DataSource + { + get { return _dataSource; } + set + { + _dataSource = value; + _envelope = null; + } + } + + /// + /// Gets or sets the rendering style of the vector layer. + /// + public new VectorStyle Style + { + get { return base.Style as VectorStyle; } + set { base.Style = value; } + } + + /// + /// Returns the extent of the layer + /// + /// Bounding box corresponding to the extent of the features in the layer + public override Envelope Envelope + { + get + { + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + + if (_envelope != null && CacheExtent) + return ToTarget(_envelope.Copy()); + + Envelope box; + lock (_dataSource) + { + // Is datasource already open? + bool wasOpen = DataSource.IsOpen; + if (!wasOpen) { DataSource.Open(); } + + box = DataSource.GetExtents(); + + if (!wasOpen) { DataSource.Close(); } + } + + if (CacheExtent) + _envelope = box; + + return ToTarget(box); + } + } + /// + /// Gets or sets a value indicating whether the layer envelope should be treated as static or not. + /// + /// + /// When CacheExtent is enabled the layer Envelope will be calculated only once from DataSource, this + /// helps to speed up the Envelope calculation with some DataProviders. Default is false for backward + /// compatibility. + /// + public virtual bool CacheExtent { get; set; } + + /// + /// Gets or sets the SRID of this VectorLayer's data source + /// + public override int SRID + { + get + { + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + + return DataSource.SRID; + } + set + { + DataSource.SRID = value; + base.SRID = value; + } + } + + #region IDisposable Members + + /// + /// Disposes the object + /// + protected override void ReleaseManagedResources() + { + if (DataSource != null) + DataSource.Dispose(); + base.ReleaseManagedResources(); + } + + #endregion + + /// + /// Renders the layer to a graphics object, using the given map viewport + /// + /// Graphics object reference + /// Map which is rendered + public override void Render(Graphics g, MapViewport map) + { + if (map.Center == null) + throw (new ApplicationException("Cannot render map. View center not specified")); + + g.SmoothingMode = SmoothingMode; + var envelope = ToSource(map.Envelope); //View to render + + if (DataSource == null) + throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); + + //If thematics is enabled, we use a slighty different rendering approach + if (Theme != null) + RenderInternal(g, map, envelope, Theme); + else + RenderInternal(g, map, envelope); + + base.Render(g, map); + } + + /// + /// Method to render this layer to the map, applying . + /// + /// The graphics object + /// The map object + /// The envelope to render + /// The theme to apply + protected void RenderInternal(Graphics g, MapViewport map, Envelope envelope, ITheme theme) + { + var ds = new FeatureDataSet(); + lock (_dataSource) + { + // Is datasource already open? + bool wasOpen = DataSource.IsOpen; + if (!wasOpen) { DataSource.Open(); } + + DataSource.ExecuteIntersectionQuery(envelope, ds); + + if (!wasOpen) { DataSource.Close(); } + } + + double scale = map.GetMapScale((int)g.DpiX); + double zoom = map.Zoom; + + Func evalStyle; + + if (theme is IThemeEx) + evalStyle = new ThemeExEvaluator((IThemeEx)theme).GetStyle; + else + evalStyle = new ThemeEvaluator(theme).GetStyle; + + foreach (FeatureDataTable features in ds.Tables) + { + // Transform geometries if necessary + if (CoordinateTransformation != null) + { + for (int i = 0; i < features.Count; i++) + { + features[i].Geometry = ToTarget(features[i].Geometry); + } + } + + //Linestring outlines is drawn by drawing the layer once with a thicker line + //before drawing the "inline" on top. + if (Style.EnableOutline) + { + for (int i = 0; i < features.Count; i++) + { + var feature = features[i]; + var outlineStyle = evalStyle(map, feature) as VectorStyle; + if (outlineStyle == null) continue; + if (!(outlineStyle.Enabled && outlineStyle.EnableOutline)) continue; + + var compare = outlineStyle.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale; + + if (!(outlineStyle.MinVisible <= compare && compare <= outlineStyle.MaxVisible)) continue; + + using (outlineStyle = outlineStyle.Clone()) + { + if (outlineStyle != null) + { + //Draw background of all line-outlines first + if (feature.Geometry is ILineString) + { + VectorRenderer.DrawLineString(g, feature.Geometry as ILineString, outlineStyle.Outline, + map, outlineStyle.LineOffset); + } + else if (feature.Geometry is IMultiLineString) + { + VectorRenderer.DrawMultiLineString(g, feature.Geometry as IMultiLineString, + outlineStyle.Outline, map, outlineStyle.LineOffset); + } + } + } + } + } + + + for (int i = 0; i < features.Count; i++) + { + var feature = features[i]; + var style = evalStyle(map, feature); + if (style == null) continue; + if (!style.Enabled) continue; + + double compare = style.VisibilityUnits == VisibilityUnits.ZoomLevel ? zoom : scale; + + if (!(style.MinVisible <= compare && compare <= style.MaxVisible)) continue; + + + IEnumerable stylesToRender = GetStylesToRender(style); + + if (stylesToRender == null) + return; + + foreach (var vstyle in stylesToRender) + { + if (!(vstyle is VectorStyle) || !vstyle.Enabled) + continue; + + using (var clone = (vstyle as VectorStyle).Clone()) + { + if (clone != null) + { + RenderGeometry(g, map, feature.Geometry, clone); + } + } + } + } + } + } + + /// + /// Method to render this layer to the map, applying . + /// + /// The graphics object + /// The map object + /// The envelope to render + protected void RenderInternal(Graphics g, MapViewport map, Envelope envelope) + { + //if style is not enabled, we don't need to render anything + if (!Style.Enabled) return; + + IEnumerable stylesToRender = GetStylesToRender(Style); + + if (stylesToRender == null) + return; + + Collection geoms = null; + + foreach (var style in stylesToRender) + { + if (!(style is VectorStyle) || !style.Enabled) + continue; + using (var vStyle = (style as VectorStyle).Clone()) + { + if (vStyle != null) + { + if (geoms == null) + { + lock (_dataSource) + { + // Is datasource already open? + bool wasOpen = DataSource.IsOpen; + if (!wasOpen) { DataSource.Open(); } + + // Read data + geoms = DataSource.GetGeometriesInView(envelope); + + if (!wasOpen) { DataSource.Close(); } + } + + if (_logger.IsDebugEnabled) + { + _logger.DebugFormat("Layer {0}, NumGeometries {1}", LayerName, geoms.Count); + } + + // Transform geometries if necessary + if (CoordinateTransformation != null) + { + for (int i = 0; i < geoms.Count; i++) + { + geoms[i] = ToTarget(geoms[i]); + } + } + } + + if (vStyle.LineSymbolizer != null) + { + vStyle.LineSymbolizer.Begin(g, map, geoms.Count); + } + else + { + //Linestring outlines is drawn by drawing the layer once with a thicker line + //before drawing the "inline" on top. + if (vStyle.EnableOutline) + { + foreach (var geom in geoms) + { + if (geom != null) + { + //Draw background of all line-outlines first + if (geom is ILineString) + VectorRenderer.DrawLineString(g, geom as ILineString, vStyle.Outline, map, vStyle.LineOffset); + else if (geom is IMultiLineString) + VectorRenderer.DrawMultiLineString(g, geom as IMultiLineString, vStyle.Outline, map, vStyle.LineOffset); + } + } + } + } + + foreach (IGeometry geom in geoms) + { + if (geom != null) + RenderGeometry(g, map, geom, vStyle); + } + + if (vStyle.LineSymbolizer != null) + { + vStyle.LineSymbolizer.Symbolize(g, map); + vStyle.LineSymbolizer.End(g, map); + } + } + } + } + } + + /// + /// Unpacks styles to render (can be nested group-styles) + /// + /// + /// + public static IEnumerable GetStylesToRender(IStyle style) + { + IStyle[] stylesToRender = null; + if (style is GroupStyle) + { + var gs = style as GroupStyle; + var styles = new List(); + for (var i = 0; i < gs.Count; i++) + { + styles.AddRange(GetStylesToRender(gs[i])); + } + stylesToRender = styles.ToArray(); + } + else if (style is VectorStyle) + { + stylesToRender = new[] { style }; + } + + return stylesToRender; + } + + /// + /// Method to render using + /// + /// The graphics object + /// The map + /// The feature's geometry + /// The style to apply + protected void RenderGeometry(Graphics g, MapViewport map, IGeometry feature, VectorStyle style) + { + if (feature == null) + return; + + var geometryType = feature.OgcGeometryType; + switch (geometryType) + { + case OgcGeometryType.Polygon: + if (style.EnableOutline) + VectorRenderer.DrawPolygon(g, (IPolygon)feature, style.Fill, style.Outline, _clippingEnabled, + map); + else + VectorRenderer.DrawPolygon(g, (IPolygon)feature, style.Fill, null, _clippingEnabled, map); + break; + case OgcGeometryType.MultiPolygon: + if (style.EnableOutline) + VectorRenderer.DrawMultiPolygon(g, (IMultiPolygon)feature, style.Fill, style.Outline, + _clippingEnabled, map); + else + VectorRenderer.DrawMultiPolygon(g, (IMultiPolygon)feature, style.Fill, null, _clippingEnabled, + map); + break; + case OgcGeometryType.LineString: + if (style.LineSymbolizer != null) + { + style.LineSymbolizer.Render(map, (ILineString)feature, g); + return; + } + VectorRenderer.DrawLineString(g, (ILineString)feature, style.Line, map, style.LineOffset); + return; + case OgcGeometryType.MultiLineString: + if (style.LineSymbolizer != null) + { + style.LineSymbolizer.Render(map, (IMultiLineString)feature, g); + return; + } + VectorRenderer.DrawMultiLineString(g, (IMultiLineString)feature, style.Line, map, style.LineOffset); + break; + case OgcGeometryType.Point: + if (style.PointSymbolizer != null) + { + VectorRenderer.DrawPoint(style.PointSymbolizer, g, (IPoint)feature, map); + return; + } + + if (style.Symbol != null || style.PointColor == null) + { + VectorRenderer.DrawPoint(g, (IPoint)feature, style.Symbol, style.SymbolScale, style.SymbolOffset, + style.SymbolRotation, map); + return; + } + VectorRenderer.DrawPoint(g, (IPoint)feature, style.PointColor, style.PointSize, style.SymbolOffset, map); + + break; + case OgcGeometryType.MultiPoint: + if (style.PointSymbolizer != null) + { + VectorRenderer.DrawMultiPoint(style.PointSymbolizer, g, (IMultiPoint)feature, map); + } + if (style.Symbol != null || style.PointColor == null) + { + VectorRenderer.DrawMultiPoint(g, (IMultiPoint)feature, style.Symbol, style.SymbolScale, + style.SymbolOffset, style.SymbolRotation, map); + } + else + { + VectorRenderer.DrawMultiPoint(g, (IMultiPoint)feature, style.PointColor, style.PointSize, style.SymbolOffset, map); + } + break; + case OgcGeometryType.GeometryCollection: + var coll = (IGeometryCollection)feature; + for (var i = 0; i < coll.NumGeometries; i++) + { + IGeometry geom = coll[i]; + RenderGeometry(g, map, geom, style); + } + break; + default: + break; + } + } + + #region Implementation of ICanQueryLayer + + /// + /// Returns the data associated with all the geometries that are intersected by 'geom' + /// + /// Geometry to intersect with + /// FeatureDataSet to fill data into + public void ExecuteIntersectionQuery(Envelope box, FeatureDataSet ds) + { + box = ToSource(box); + + int tableCount = ds.Tables.Count; + + lock (_dataSource) + { + // Is datasource already open? + bool wasOpen = _dataSource.IsOpen; + if (!wasOpen) { _dataSource.Open(); } + + _dataSource.ExecuteIntersectionQuery(box, ds); + + if (!wasOpen) { DataSource.Close(); } + } + + if (ds.Tables.Count > tableCount) + { + //We added a table, name it according to layer + ds.Tables[ds.Tables.Count - 1].TableName = LayerName; + } + } + + /// + /// Returns the data associated with all the geometries that are intersected by 'geom' + /// + /// Geometry to intersect with + /// FeatureDataSet to fill data into + public void ExecuteIntersectionQuery(IGeometry geometry, FeatureDataSet ds) + { + geometry = ToSource(geometry); + + int tableCount = ds.Tables.Count; + + lock (_dataSource) + { + // Is datasource already open? + bool wasOpen = DataSource.IsOpen; + if (!wasOpen) { DataSource.Open(); } + + _dataSource.ExecuteIntersectionQuery(geometry, ds); + + if (!wasOpen) { DataSource.Close(); } + } + + if (ds.Tables.Count > tableCount) + { + //We added a table, name it according to layer + ds.Tables[ds.Tables.Count - 1].TableName = LayerName; + } + } + + /// + /// Whether the layer is queryable when used in a SharpMap.Web.Wms.WmsServer, ExecuteIntersectionQuery() will be possible in all other situations when set to FALSE + /// + public bool IsQueryEnabled + { + get { return _isQueryEnabled; } + set { _isQueryEnabled = value; } + } + + #endregion + + public object Clone() + { + var res = (VectorLayer)MemberwiseClone(); + res.Style = Style.Clone(); + if (Theme is ICloneable) + res.Theme = (ITheme)((ICloneable)Theme).Clone(); + return res; + } + + #region Theme evaluators + private abstract class ThemeEvaluatorBase + { + public abstract IStyle GetStyle(MapViewport mvp, FeatureDataRow feature); + } + + private class ThemeEvaluator : ThemeEvaluatorBase + { + private readonly ITheme _theme; + + public ThemeEvaluator(ITheme theme) + { + _theme = theme; + } + public sealed override IStyle GetStyle(MapViewport mvp, FeatureDataRow feature) + { + return _theme.GetStyle(feature); + } + } + + private class ThemeExEvaluator : ThemeEvaluatorBase + { + private readonly IThemeEx _theme; + + public ThemeExEvaluator(IThemeEx theme) + { + _theme = theme; + } + public sealed override IStyle GetStyle(MapViewport mvp, FeatureDataRow feature) + { + return _theme.GetStyle(mvp, feature); + } + } + #endregion + } +} diff --git a/SharpMap/Rendering/VectorRenderer.cs b/SharpMap/Rendering/VectorRenderer.cs index 621b1876..09258c65 100644 --- a/SharpMap/Rendering/VectorRenderer.cs +++ b/SharpMap/Rendering/VectorRenderer.cs @@ -1,740 +1,771 @@ -// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. - -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Reflection; -using GeoAPI.Geometries; -using SharpMap.Rendering.Symbolizer; -using SharpMap.Styles; -using SharpMap.Utilities; -using Point=GeoAPI.Geometries.Coordinate; -using System.Runtime.CompilerServices; - -namespace SharpMap.Rendering -{ - /// - /// This class renders individual geometry features to a graphics object using the settings of a map object. - /// - public static class VectorRenderer - { - internal const float ExtremeValueLimit = 1E+8f; - internal const float NearZero = 1E-30f; // 1/Infinity - - static VectorRenderer() - { - SizeOfString = SizeOfStringCeiling; - } - - private static readonly Bitmap Defaultsymbol = - (Bitmap) - Image.FromStream( - Assembly.GetExecutingAssembly().GetManifestResourceStream("SharpMap.Styles.DefaultSymbol.png")); - - /// - /// Renders a MultiLineString to the map. - /// - /// Graphics reference - /// MultiLineString to be rendered - /// Pen style used for rendering - /// Map reference - /// Offset by which line will be moved to right - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawMultiLineString(Graphics g, IMultiLineString lines, Pen pen, MapViewport map, float offset) - { - for(var i = 0; i < lines.NumGeometries; i++) - { - var line = (ILineString) lines[i]; - DrawLineString(g, line, pen, map, offset); - } - } - - /// - /// Offset drawn linestring by given pixel width - /// - /// - /// - /// - internal static PointF[] OffsetRight(PointF[] points, float offset) - { - int length = points.Length; - var newPoints = new PointF[(length - 1) * 2]; - - float space = (offset * offset / 4) + 1; - - //if there are two or more points - if (length >= 2) - { - var counter = 0; - float x = 0, y = 0; - for (var i = 0; i < length - 1; i++) - { - var b = -(points[i + 1].X - points[i].X); - if (b != 0) - { - var a = points[i + 1].Y - points[i].Y; - var c = a / b; - y = 2 * (float)Math.Sqrt(space / (c * c + 1)); - y = b < 0 ? y : -y; - x = c * y; - - if (offset < 0) - { - y = -y; - x = -x; - } - - newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); - newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); - } - else - { - newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); - newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); - } - counter += 2; - } - - return newPoints; - } - return points; - } - - /// - /// Renders a LineString to the map. - /// - /// Graphics reference - /// LineString to render - /// Pen style used for rendering - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map) - { - DrawLineString(g, line, pen, map, 0); - } - /// - /// Renders a LineString to the map. - /// - /// Graphics reference - /// LineString to render - /// Pen style used for rendering - /// Map reference - /// Offset by which line will be moved to right - public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map, float offset) - { - var points = line.TransformToImage(map); - if (points.Length > 1) - { - var gp = new GraphicsPath(); - if (offset != 0d) - points = OffsetRight(points, offset); - gp.AddLines(LimitValues(points, ExtremeValueLimit)); - - g.DrawPath(pen, gp); - } - } - - /// - /// Renders a multipolygon byt rendering each polygon in the collection by calling DrawPolygon. - /// - /// Graphics reference - /// MultiPolygon to render - /// Brush used for filling (null or transparent for no filling) - /// Outline pen style (null if no outline) - /// Specifies whether polygon clipping should be applied - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawMultiPolygon(Graphics g, IMultiPolygon pols, Brush brush, Pen pen, bool clip, MapViewport map) - { - for (var i = 0; i < pols.NumGeometries;i++ ) - { - var p = (IPolygon) pols[i]; - DrawPolygon(g, p, brush, pen, clip, map); - } - } - - /// - /// Renders a polygon to the map. - /// - /// Graphics reference - /// Polygon to render - /// Brush used for filling (null or transparent for no filling) - /// Outline pen style (null if no outline) - /// Specifies whether polygon clipping should be applied - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawPolygon(Graphics g, IPolygon pol, Brush brush, Pen pen, bool clip, MapViewport map) - { - if (pol.ExteriorRing == null) - return; - - var points = pol.ExteriorRing.TransformToImage(map); - if (points.Length > 2) - { - //Use a graphics path instead of DrawPolygon. DrawPolygon has a problem with several interior holes - var gp = new GraphicsPath(); - - //Add the exterior polygon - if (!clip) - gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); - else - DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, map.Size.Height); - - //Add the interior polygons (holes) - if (pol.NumInteriorRings > 0) - { - foreach (ILinearRing ring in pol.InteriorRings) - { - points = ring.TransformToImage(map); - if (!clip) - gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); - else - DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, - map.Size.Height); - } - } - - - // Only render inside of polygon if the brush isn't null or isn't transparent - if (brush != null && brush != Brushes.Transparent) - g.FillPath(brush, gp); - - // Create an outline if a pen style is available - if (pen != null) - g.DrawPath(pen, gp); - } - } - - private static void DrawPolygonClipped(GraphicsPath gp, PointF[] polygon, int width, int height) - { - var clipState = DetermineClipState(polygon, width, height); - if (clipState == ClipState.Within) - { - gp.AddPolygon(polygon); - } - else if (clipState == ClipState.Intersecting) - { - var clippedPolygon = ClipPolygon(polygon, width, height); - if (clippedPolygon.Length > 2) - gp.AddPolygon(clippedPolygon); - } - } - - /// - /// Purpose of this method is to prevent the 'overflow error' exception in the FillPath method. - /// This Exception is thrown when the coordinate values become too big (values over -2E+9f always - /// throw an exception, values under 1E+8f seem to be okay). This method limits the coordinates to - /// the values given by the second parameter (plus an minus). Theoretically the lines to and from - /// these limited points are not correct but GDI+ paints incorrect even before that limit is reached. - /// - /// The vertices that need to be limited - /// The limit at which coordinate values will be cutoff - /// The limited vertices - public static PointF[] LimitValues(PointF[] vertices, float limit) - { - for (var i = 0; i < vertices.Length; i++) - { - vertices[i].X = Math.Max(-limit, Math.Min(limit, vertices[i].X)); - vertices[i].Y = Math.Max(-limit, Math.Min(limit, vertices[i].Y)); - } - return vertices; - } - - /// - /// Signature for a function that evaluates the length of a string when rendered on a Graphics object with a given font - /// - /// object - /// the text to render - /// the font to use - /// the size - public delegate SizeF SizeOfStringDelegate(Graphics g, string text, Font font); - - private static SizeOfStringDelegate _sizeOfString; - - /// - /// Delegate used to determine the of a given string. - /// - public static SizeOfStringDelegate SizeOfString - { - get { return _sizeOfString ?? (_sizeOfString = SizeOfStringCeiling); } - set - { - if (value != null ) - _sizeOfString = value; - } - } - - /// - /// Function to get the of a string when rendered with the given font. - /// - /// object - /// the text to render - /// the font to use - /// the size - public static SizeF SizeOfStringBase(Graphics g, string text, Font font) - { - return g.MeasureString(text, font); - } - - /// - /// Function to get the of a string when rendered with the given font. - /// - /// object - /// the text to render - /// the font to use - /// the size - [Obsolete] - public static SizeF SizeOfString74(Graphics g, string text, Font font) - { - var s = g.MeasureString(text, font); - return new SizeF(s.Width * 0.74f+1f, s.Height * 0.74f); - } - /// - /// Function to get the of a string when rendered with the given font. - /// - /// object - /// the text to render - /// the font to use - /// the size - public static SizeF SizeOfStringCeiling(Graphics g, string text, Font font) - { - SizeF f = g.MeasureString(text, font); - return new SizeF((float)Math.Ceiling(f.Width), (float)Math.Ceiling(f.Height)); - } - - - /// - /// Renders a label to the map. - /// - /// Graphics reference - /// Label placement - /// Offset of label in screen coordinates - /// Font used for rendering - /// Font forecolor - /// Background color - /// Color of halo - /// Text rotation in degrees - /// Text to render - /// Map reference - /// Horizontal alignment for multi line labels. If not set is used - /// Point where the rotation should take place - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawLabel(Graphics g, PointF labelPoint, PointF offset, Font font, Color forecolor, - Brush backcolor, Pen halo, float rotation, string text, MapViewport map, - LabelStyle.HorizontalAlignmentEnum alignment = LabelStyle.HorizontalAlignmentEnum.Left, - PointF? rotationPoint = null) - - { - //Calculate the size of the text - var labelSize = _sizeOfString(g, text, font); - - //Add label offset - labelPoint.X += offset.X; - labelPoint.Y += offset.Y; - - //Translate alignment to stringalignment - StringAlignment salign; - switch (alignment) - { - case LabelStyle.HorizontalAlignmentEnum.Left: - salign = StringAlignment.Near; - break; - case LabelStyle.HorizontalAlignmentEnum.Center: - salign = StringAlignment.Center; - break; - default: - salign = StringAlignment.Far; - break; - } - - if (rotation != 0 && !float.IsNaN(rotation)) - { - rotationPoint = rotationPoint ?? labelPoint; - - g.FillEllipse(Brushes.LawnGreen, rotationPoint.Value.X - 1, rotationPoint.Value.Y - 1, 2, 2); - - var t = g.Transform.Clone(); - g.TranslateTransform(rotationPoint.Value.X, rotationPoint.Value.Y); - g.RotateTransform(rotation); - //g.TranslateTransform(-labelSize.Width/2, -labelSize.Height/2); - - labelPoint = new PointF(labelPoint.X - rotationPoint.Value.X, - labelPoint.Y - rotationPoint.Value.Y); - - //labelSize = new SizeF(labelSize.Width*0.74f + 1f, labelSize.Height*0.74f); - if (backcolor != null && backcolor != Brushes.Transparent) - g.FillRectangle(backcolor, labelPoint.X, labelPoint.Y, labelSize.Width, labelSize.Height); - - var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int) font.Style, font.Size, - new RectangleF(labelPoint, labelSize) /* labelPoint*/, - new StringFormat { Alignment = salign } /*null*/); - if (halo != null) - g.DrawPath(halo, path); - - g.FillPath(new SolidBrush(forecolor), path); - //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), 0, 0); - g.Transform = t; - } - else - { - if (backcolor != null && backcolor != Brushes.Transparent) - g.FillRectangle(backcolor, labelPoint.X, labelPoint.Y, labelSize.Width, - labelSize.Height); - - var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int) font.Style, font.Size, - new RectangleF(labelPoint, labelSize) /* labelPoint*/, - new StringFormat { Alignment = salign } /*null*/); - if (halo != null) - g.DrawPath(halo, path); - g.FillPath(new SolidBrush(forecolor), path); - //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), LabelPoint.X, LabelPoint.Y); - } - } - - private static ClipState DetermineClipState(PointF[] vertices, int width, int height) - { - float minX = float.MaxValue; - float minY = float.MaxValue; - float maxX = float.MinValue; - float maxY = float.MinValue; - - for (int i = 0; i < vertices.Length; i++) - { - minX = Math.Min(minX, vertices[i].X); - minY = Math.Min(minY, vertices[i].Y); - maxX = Math.Max(maxX, vertices[i].X); - maxY = Math.Max(maxY, vertices[i].Y); - } - - if (maxX < 0) return ClipState.Outside; - if (maxY < 0) return ClipState.Outside; - if (minX > width) return ClipState.Outside; - if (minY > height) return ClipState.Outside; - if (minX > 0 && maxX < width && minY > 0 && maxY < height) return ClipState.Within; - return ClipState.Intersecting; - } - - /// - /// Clips a polygon to the view. - /// Based on UMN Mapserver renderer - /// - /// vertices in image coordinates - /// Width of map in image coordinates - /// Height of map in image coordinates - /// Clipped polygon - internal static PointF[] ClipPolygon(PointF[] vertices, int width, int height) - { - var line = new List(); - if (vertices.Length <= 1) /* nothing to clip */ - return vertices; - - for (int i = 0; i < vertices.Length - 1; i++) - { - var x1 = vertices[i].X; - var y1 = vertices[i].Y; - var x2 = vertices[i + 1].X; - var y2 = vertices[i + 1].Y; - - var deltax = x2 - x1; - if (deltax == 0f) - { - // bump off of the vertical - deltax = (x1 > 0) ? -NearZero : NearZero; - } - var deltay = y2 - y1; - if (deltay == 0f) - { - // bump off of the horizontal - deltay = (y1 > 0) ? -NearZero : NearZero; - } - - float xin; - float xout; - if (deltax > 0) - { - // points to right - xin = 0; - xout = width; - } - else - { - xin = width; - xout = 0; - } - - float yin; - float yout; - if (deltay > 0) - { - // points up - yin = 0; - yout = height; - } - else - { - yin = height; - yout = 0; - } - - var tinx = (xin - x1)/deltax; - var tiny = (yin - y1)/deltay; - - float tin1; - float tin2; - if (tinx < tiny) - { - // hits x first - tin1 = tinx; - tin2 = tiny; - } - else - { - // hits y first - tin1 = tiny; - tin2 = tinx; - } - - if (1 >= tin1) - { - if (0 < tin1) - line.Add(new PointF(xin, yin)); - - if (1 >= tin2) - { - var toutx = (xout - x1)/deltax; - var touty = (yout - y1)/deltay; - - var tout = (toutx < touty) ? toutx : touty; - - if (0 < tin2 || 0 < tout) - { - if (tin2 <= tout) - { - if (0 < tin2) - { - line.Add(tinx > tiny - ? new PointF(xin, y1 + tinx*deltay) - : new PointF(x1 + tiny*deltax, yin)); - } - - if (1 > tout) - { - line.Add(toutx < touty - ? new PointF(xout, y1 + toutx*deltay) - : new PointF(x1 + touty*deltax, yout)); - } - else - line.Add(new PointF(x2, y2)); - } - else - { - line.Add(tinx > tiny ? new PointF(xin, yout) : new PointF(xout, yin)); - } - } - } - } - } - if (line.Count > 0) - line.Add(new PointF(line[0].X, line[0].Y)); - - return line.ToArray(); - } - - /// - /// Renders a point to the map. - /// - /// Graphics reference - /// Point to render - /// Brush reference - /// Size of drawn Point - /// Symbol offset af scale=1 - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawPoint(Graphics g, IPoint point, Brush b, float size, PointF offset, MapViewport map) - { - if (point == null) - return; - - var pp = map.WorldToImage(point.Coordinate); - //var startingTransform = g.Transform; - - var width = size; - var height = size; - - g.FillEllipse(b, (int)pp.X - width / 2 + offset.X , - (int)pp.Y - height / 2 + offset.Y , width, height); - } - - /// - /// Renders a point to the map. - /// - /// Graphics reference - /// Point to render - /// Symbolizer to decorate point - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawPoint(IPointSymbolizer symbolizer, Graphics g, IPoint point, MapViewport map) - { - if (point == null) - return; - - symbolizer.Render(map, point, g); - } - - /// - /// Renders a point to the map. - /// - /// Graphics reference - /// Point to render - /// Symbol to place over point - /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling - /// Symbol offset af scale=1 - /// Symbol rotation in degrees - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawPoint(Graphics g, IPoint point, Image symbol, float symbolscale, PointF offset, - float rotation, MapViewport map) - { - if (point == null) - return; - - if (symbol == null) //We have no point style - Use a default symbol - symbol = Defaultsymbol; - - - var pp = map.WorldToImage(point.Coordinate); - - lock (symbol) - { - if (rotation != 0 && !Single.IsNaN(rotation)) - { - var startingTransform = g.Transform.Clone(); - - var transform = g.Transform; - var rotationCenter = pp; - transform.RotateAt(rotation, rotationCenter); - g.Transform = transform; - - //if (symbolscale == 1f) - //{ - // g.DrawImage(symbol, (pp.X - symbol.Width/2f + offset.X), - // (pp.Y - symbol.Height/2f + offset.Y)); - //} - //else - //{ - // var width = symbol.Width*symbolscale; - // var height = symbol.Height*symbolscale; - // g.DrawImage(symbol, (int) pp.X - width/2 + offset.X*symbolscale, - // (int) pp.Y - height/2 + offset.Y*symbolscale, width, height); - //} - var width = symbol.Width * symbolscale; - var height = symbol.Height * symbolscale; - g.DrawImage(symbol, pp.X - width / 2 + offset.X * symbolscale, - pp.Y - height / 2 + offset.Y * symbolscale, width, height); - g.Transform = startingTransform; - } - else - { - //if (symbolscale == 1f) - //{ - // g.DrawImageUnscaled(symbol, (int) (pp.X - symbol.Width/2f + offset.X), - // (int) (pp.Y - symbol.Height/2f + offset.Y)); - //} - //else - //{ - // var width = symbol.Width*symbolscale; - // var height = symbol.Height*symbolscale; - // g.DrawImage(symbol, (int) pp.X - width/2 + offset.X*symbolscale, - // (int) pp.Y - height/2 + offset.Y*symbolscale, width, height); - //} - var width = symbol.Width * symbolscale; - var height = symbol.Height * symbolscale; - g.DrawImage(symbol, pp.X - width / 2 + offset.X * symbolscale, - pp.Y - height / 2 + offset.Y * symbolscale, width, height); - } - } - } - - /// - /// Renders a to the map. - /// - /// Graphics reference - /// MultiPoint to render - /// Symbol to place over point - /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling - /// Symbol offset af scale=1 - /// Symbol rotation in degrees - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawMultiPoint(Graphics g, IMultiPoint points, Image symbol, float symbolscale, PointF offset, - float rotation, MapViewport map) - { - for (var i = 0; i < points.NumGeometries; i++) - { - var point = (IPoint) points[i]; - DrawPoint(g, point, symbol, symbolscale, offset, rotation, map); - } - } - - /// - /// Renders a to the map. - /// - /// Graphics reference - /// MultiPoint to render - /// Symbolizer to decorate point - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawMultiPoint(IPointSymbolizer symbolizer, Graphics g, IMultiPoint points, MapViewport map) - { - symbolizer.Render(map, points, g); - } - - /// - /// Renders a to the map. - /// - /// Graphics reference - /// MultiPoint to render - /// Brush reference - /// Size of drawn Point - /// Symbol offset af scale=1 - /// Map reference - [MethodImpl(MethodImplOptions.Synchronized)] - public static void DrawMultiPoint(Graphics g, IMultiPoint points, Brush brush, float size, PointF offset, MapViewport map) - { - for (var i = 0; i < points.NumGeometries; i++) - { - var point = (IPoint) points[i]; - DrawPoint(g, point, brush, size, offset, map); - } - } - - #region Nested type: ClipState - - private enum ClipState - { - Within, - Outside, - Intersecting - } ; - - #endregion - } -} \ No newline at end of file +// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) +// +// This file is part of SharpMap. +// SharpMap is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// SharpMap is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public License +// along with SharpMap; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Reflection; +using GeoAPI.Geometries; +using SharpMap.Rendering.Symbolizer; +using SharpMap.Styles; +using SharpMap.Utilities; +using Point=GeoAPI.Geometries.Coordinate; +using System.Runtime.CompilerServices; + +namespace SharpMap.Rendering +{ + /// + /// This class renders individual geometry features to a graphics object using the settings of a map object. + /// + public static class VectorRenderer + { + internal const float ExtremeValueLimit = 1E+8f; + internal const float NearZero = 1E-30f; // 1/Infinity + + static VectorRenderer() + { + SizeOfString = SizeOfStringCeiling; + } + + private static readonly Bitmap Defaultsymbol = + (Bitmap) + Image.FromStream( + Assembly.GetExecutingAssembly().GetManifestResourceStream("SharpMap.Styles.DefaultSymbol.png")); + + /// + /// Renders a MultiLineString to the map. + /// + /// Graphics reference + /// MultiLineString to be rendered + /// Pen style used for rendering + /// Map reference + /// Offset by which line will be moved to right + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawMultiLineString(Graphics g, IMultiLineString lines, Pen pen, MapViewport map, float offset) + { + for(var i = 0; i < lines.NumGeometries; i++) + { + var line = (ILineString) lines[i]; + DrawLineString(g, line, pen, map, offset); + } + } + + /// + /// Offset drawn linestring by given pixel width + /// + /// + /// + /// + internal static PointF[] OffsetRight(PointF[] points, float offset) + { + int length = points.Length; + var newPoints = new PointF[(length - 1) * 2]; + + float space = (offset * offset / 4) + 1; + + //if there are two or more points + if (length >= 2) + { + var counter = 0; + float x = 0, y = 0; + for (var i = 0; i < length - 1; i++) + { + var b = -(points[i + 1].X - points[i].X); + if (b != 0) + { + var a = points[i + 1].Y - points[i].Y; + var c = a / b; + y = 2 * (float)Math.Sqrt(space / (c * c + 1)); + y = b < 0 ? y : -y; + x = c * y; + + if (offset < 0) + { + y = -y; + x = -x; + } + + newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); + newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); + } + else + { + newPoints[counter] = new PointF(points[i].X + x, points[i].Y + y); + newPoints[counter + 1] = new PointF(points[i + 1].X + x, points[i + 1].Y + y); + } + counter += 2; + } + + return newPoints; + } + return points; + } + + /// + /// Renders a LineString to the map. + /// + /// Graphics reference + /// LineString to render + /// Pen style used for rendering + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map) + { + DrawLineString(g, line, pen, map, 0); + } + /// + /// Renders a LineString to the map. + /// + /// Graphics reference + /// LineString to render + /// Pen style used for rendering + /// Map reference + /// Offset by which line will be moved to right + public static void DrawLineString(Graphics g, ILineString line, Pen pen, MapViewport map, float offset) + { + var points = line.TransformToImage(map); + if (points.Length > 1) + { + var gp = new GraphicsPath(); + if (offset != 0d) + points = OffsetRight(points, offset); + gp.AddLines(LimitValues(points, ExtremeValueLimit)); + + g.DrawPath(pen, gp); + } + } + + /// + /// Renders a multipolygon byt rendering each polygon in the collection by calling DrawPolygon. + /// + /// Graphics reference + /// MultiPolygon to render + /// Brush used for filling (null or transparent for no filling) + /// Outline pen style (null if no outline) + /// Specifies whether polygon clipping should be applied + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawMultiPolygon(Graphics g, IMultiPolygon pols, Brush brush, Pen pen, bool clip, MapViewport map) + { + for (var i = 0; i < pols.NumGeometries;i++ ) + { + var p = (IPolygon) pols[i]; + DrawPolygon(g, p, brush, pen, clip, map); + } + } + + /// + /// Renders a polygon to the map. + /// + /// Graphics reference + /// Polygon to render + /// Brush used for filling (null or transparent for no filling) + /// Outline pen style (null if no outline) + /// Specifies whether polygon clipping should be applied + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawPolygon(Graphics g, IPolygon pol, Brush brush, Pen pen, bool clip, MapViewport map) + { + if (pol.ExteriorRing == null) + return; + + var points = pol.ExteriorRing.TransformToImage(map); + if (points.Length > 2) + { + //Use a graphics path instead of DrawPolygon. DrawPolygon has a problem with several interior holes + var gp = new GraphicsPath(); + + //Add the exterior polygon + if (!clip) + gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); + else + DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, map.Size.Height); + + //Add the interior polygons (holes) + if (pol.NumInteriorRings > 0) + { + foreach (ILinearRing ring in pol.InteriorRings) + { + points = ring.TransformToImage(map); + if (!clip) + gp.AddPolygon(LimitValues(points, ExtremeValueLimit)); + else + DrawPolygonClipped(gp, LimitValues(points, ExtremeValueLimit), map.Size.Width, + map.Size.Height); + } + } + + + // Only render inside of polygon if the brush isn't null or isn't transparent + if (brush != null && brush != Brushes.Transparent) + g.FillPath(brush, gp); + + // Create an outline if a pen style is available + if (pen != null) + g.DrawPath(pen, gp); + } + } + + private static void DrawPolygonClipped(GraphicsPath gp, PointF[] polygon, int width, int height) + { + var clipState = DetermineClipState(polygon, width, height); + if (clipState == ClipState.Within) + { + gp.AddPolygon(polygon); + } + else if (clipState == ClipState.Intersecting) + { + var clippedPolygon = ClipPolygon(polygon, width, height); + if (clippedPolygon.Length > 2) + gp.AddPolygon(clippedPolygon); + } + } + + /// + /// Purpose of this method is to prevent the 'overflow error' exception in the FillPath method. + /// This Exception is thrown when the coordinate values become too big (values over -2E+9f always + /// throw an exception, values under 1E+8f seem to be okay). This method limits the coordinates to + /// the values given by the second parameter (plus an minus). Theoretically the lines to and from + /// these limited points are not correct but GDI+ paints incorrect even before that limit is reached. + /// + /// The vertices that need to be limited + /// The limit at which coordinate values will be cutoff + /// The limited vertices + public static PointF[] LimitValues(PointF[] vertices, float limit) + { + for (var i = 0; i < vertices.Length; i++) + { + vertices[i].X = Math.Max(-limit, Math.Min(limit, vertices[i].X)); + vertices[i].Y = Math.Max(-limit, Math.Min(limit, vertices[i].Y)); + } + return vertices; + } + + /// + /// Signature for a function that evaluates the length of a string when rendered on a Graphics object with a given font + /// + /// object + /// the text to render + /// the font to use + /// the size + public delegate SizeF SizeOfStringDelegate(Graphics g, string text, Font font); + + private static SizeOfStringDelegate _sizeOfString; + + /// + /// Delegate used to determine the of a given string. + /// + public static SizeOfStringDelegate SizeOfString + { + get { return _sizeOfString ?? (_sizeOfString = SizeOfStringCeiling); } + set + { + if (value != null ) + _sizeOfString = value; + } + } + + /// + /// Function to get the of a string when rendered with the given font. + /// + /// object + /// the text to render + /// the font to use + /// the size + public static SizeF SizeOfStringBase(Graphics g, string text, Font font) + { + return g.MeasureString(text, font); + } + + /// + /// Function to get the of a string when rendered with the given font. + /// + /// object + /// the text to render + /// the font to use + /// the size + [Obsolete] + public static SizeF SizeOfString74(Graphics g, string text, Font font) + { + var s = g.MeasureString(text, font); + return new SizeF(s.Width * 0.74f+1f, s.Height * 0.74f); + } + /// + /// Function to get the of a string when rendered with the given font. + /// + /// object + /// the text to render + /// the font to use + /// the size + public static SizeF SizeOfStringCeiling(Graphics g, string text, Font font) + { + SizeF f = g.MeasureString(text, font); + return new SizeF((float)Math.Ceiling(f.Width), (float)Math.Ceiling(f.Height)); + } + + public static RectangleF DrawLabelEx(Graphics g, PointF labelPoint, PointF offset, Font font, Color forecolor, + Brush backcolor, Pen halo, float rotation, string text, MapViewport map, + LabelStyle.HorizontalAlignmentEnum alignment = LabelStyle.HorizontalAlignmentEnum.Left, + PointF? rotationPoint = null) + { + //Calculate the size of the text + var labelSize = _sizeOfString(g, text, font); + + //Add label offset + labelPoint.X += offset.X; + labelPoint.Y += offset.Y; + + //Translate alignment to stringalignment + StringAlignment salign; + switch (alignment) + { + case LabelStyle.HorizontalAlignmentEnum.Left: + salign = StringAlignment.Near; + break; + case LabelStyle.HorizontalAlignmentEnum.Center: + salign = StringAlignment.Center; + break; + default: + salign = StringAlignment.Far; + break; + } + + if (rotation != 0 && !float.IsNaN(rotation)) + { + rotationPoint = rotationPoint ?? labelPoint; + + g.FillEllipse(Brushes.LawnGreen, rotationPoint.Value.X - 1, rotationPoint.Value.Y - 1, 2, 2); + + using (var t = g.Transform.Clone()) + { + g.TranslateTransform(rotationPoint.Value.X, rotationPoint.Value.Y); + g.RotateTransform(rotation); + //g.TranslateTransform(-labelSize.Width/2, -labelSize.Height/2); + + labelPoint = new PointF(labelPoint.X - rotationPoint.Value.X, + labelPoint.Y - rotationPoint.Value.Y); + + //labelSize = new SizeF(labelSize.Width*0.74f + 1f, labelSize.Height*0.74f); + if (backcolor != null && backcolor != Brushes.Transparent) + g.FillRectangle(backcolor, labelPoint.X, labelPoint.Y, labelSize.Width, labelSize.Height); + + var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int) font.Style, font.Size, + new RectangleF(labelPoint, labelSize) /* labelPoint*/, + new StringFormat { Alignment = salign } /*null*/); + if (halo != null) + g.DrawPath(halo, path); + + g.FillPath(new SolidBrush(forecolor), path); + //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), 0, 0); + g.Transform = t; + + return new RectangleF(); + }; + } + else + { + if (backcolor != null && backcolor != Brushes.Transparent) + g.FillRectangle(backcolor, labelPoint.X, labelPoint.Y, labelSize.Width, + labelSize.Height); + + var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int) font.Style, font.Size, + new RectangleF(labelPoint, labelSize) /* labelPoint*/, + new StringFormat { Alignment = salign } /*null*/); + if (halo != null) + g.DrawPath(halo, path); + g.FillPath(new SolidBrush(forecolor), path); + //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), LabelPoint.X, LabelPoint.Y); + + //return path.GetBounds(); + return new RectangleF(labelPoint.X, labelPoint.Y, labelSize.Width,labelSize.Height); + } + } + + /// + /// Renders a label to the map. + /// + /// Graphics reference + /// Label placement + /// Offset of label in screen coordinates + /// Font used for rendering + /// Font forecolor + /// Background color + /// Color of halo + /// Text rotation in degrees + /// Text to render + /// Map reference + /// Horizontal alignment for multi line labels. If not set is used + /// Point where the rotation should take place + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawLabel(Graphics g, PointF labelPoint, PointF offset, Font font, Color forecolor, + Brush backcolor, Pen halo, float rotation, string text, MapViewport map, + LabelStyle.HorizontalAlignmentEnum alignment = LabelStyle.HorizontalAlignmentEnum.Left, + PointF? rotationPoint = null) + + { + DrawLabelEx(g, labelPoint, offset, font, forecolor, backcolor, halo, rotation, text, map, alignment, rotationPoint); + } + + private static ClipState DetermineClipState(PointF[] vertices, int width, int height) + { + float minX = float.MaxValue; + float minY = float.MaxValue; + float maxX = float.MinValue; + float maxY = float.MinValue; + + for (int i = 0; i < vertices.Length; i++) + { + minX = Math.Min(minX, vertices[i].X); + minY = Math.Min(minY, vertices[i].Y); + maxX = Math.Max(maxX, vertices[i].X); + maxY = Math.Max(maxY, vertices[i].Y); + } + + if (maxX < 0) return ClipState.Outside; + if (maxY < 0) return ClipState.Outside; + if (minX > width) return ClipState.Outside; + if (minY > height) return ClipState.Outside; + if (minX > 0 && maxX < width && minY > 0 && maxY < height) return ClipState.Within; + return ClipState.Intersecting; + } + + /// + /// Clips a polygon to the view. + /// Based on UMN Mapserver renderer + /// + /// vertices in image coordinates + /// Width of map in image coordinates + /// Height of map in image coordinates + /// Clipped polygon + internal static PointF[] ClipPolygon(PointF[] vertices, int width, int height) + { + var line = new List(); + if (vertices.Length <= 1) /* nothing to clip */ + return vertices; + + for (int i = 0; i < vertices.Length - 1; i++) + { + var x1 = vertices[i].X; + var y1 = vertices[i].Y; + var x2 = vertices[i + 1].X; + var y2 = vertices[i + 1].Y; + + var deltax = x2 - x1; + if (deltax == 0f) + { + // bump off of the vertical + deltax = (x1 > 0) ? -NearZero : NearZero; + } + var deltay = y2 - y1; + if (deltay == 0f) + { + // bump off of the horizontal + deltay = (y1 > 0) ? -NearZero : NearZero; + } + + float xin; + float xout; + if (deltax > 0) + { + // points to right + xin = 0; + xout = width; + } + else + { + xin = width; + xout = 0; + } + + float yin; + float yout; + if (deltay > 0) + { + // points up + yin = 0; + yout = height; + } + else + { + yin = height; + yout = 0; + } + + var tinx = (xin - x1)/deltax; + var tiny = (yin - y1)/deltay; + + float tin1; + float tin2; + if (tinx < tiny) + { + // hits x first + tin1 = tinx; + tin2 = tiny; + } + else + { + // hits y first + tin1 = tiny; + tin2 = tinx; + } + + if (1 >= tin1) + { + if (0 < tin1) + line.Add(new PointF(xin, yin)); + + if (1 >= tin2) + { + var toutx = (xout - x1)/deltax; + var touty = (yout - y1)/deltay; + + var tout = (toutx < touty) ? toutx : touty; + + if (0 < tin2 || 0 < tout) + { + if (tin2 <= tout) + { + if (0 < tin2) + { + line.Add(tinx > tiny + ? new PointF(xin, y1 + tinx*deltay) + : new PointF(x1 + tiny*deltax, yin)); + } + + if (1 > tout) + { + line.Add(toutx < touty + ? new PointF(xout, y1 + toutx*deltay) + : new PointF(x1 + touty*deltax, yout)); + } + else + line.Add(new PointF(x2, y2)); + } + else + { + line.Add(tinx > tiny ? new PointF(xin, yout) : new PointF(xout, yin)); + } + } + } + } + } + if (line.Count > 0) + line.Add(new PointF(line[0].X, line[0].Y)); + + return line.ToArray(); + } + + /// + /// Renders a point to the map. + /// + /// Graphics reference + /// Point to render + /// Brush reference + /// Size of drawn Point + /// Symbol offset af scale=1 + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawPoint(Graphics g, IPoint point, Brush b, float size, PointF offset, MapViewport map) + { + if (point == null) + return; + + var pp = map.WorldToImage(point.Coordinate); + //var startingTransform = g.Transform; + + var width = size; + var height = size; + + g.FillEllipse(b, (int)pp.X - width / 2 + offset.X , + (int)pp.Y - height / 2 + offset.Y , width, height); + } + + /// + /// Renders a point to the map. + /// + /// Graphics reference + /// Point to render + /// Symbolizer to decorate point + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawPoint(IPointSymbolizer symbolizer, Graphics g, IPoint point, MapViewport map) + { + if (point == null) + return; + + symbolizer.Render(map, point, g); + } + + /// + /// Renders a point to the map. + /// + /// Graphics reference + /// Point to render + /// Symbol to place over point + /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling + /// Symbol offset af scale=1 + /// Symbol rotation in degrees + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawPoint(Graphics g, IPoint point, Image symbol, float symbolscale, PointF offset, + float rotation, MapViewport map) + { + if (point == null) + return; + + if (symbol == null) //We have no point style - Use a default symbol + symbol = Defaultsymbol; + + + var pp = map.WorldToImage(point.Coordinate); + + lock (symbol) + { + if (rotation != 0 && !Single.IsNaN(rotation)) + { + var startingTransform = g.Transform.Clone(); + + var transform = g.Transform; + var rotationCenter = pp; + transform.RotateAt(rotation, rotationCenter); + g.Transform = transform; + + //if (symbolscale == 1f) + //{ + // g.DrawImage(symbol, (pp.X - symbol.Width/2f + offset.X), + // (pp.Y - symbol.Height/2f + offset.Y)); + //} + //else + //{ + // var width = symbol.Width*symbolscale; + // var height = symbol.Height*symbolscale; + // g.DrawImage(symbol, (int) pp.X - width/2 + offset.X*symbolscale, + // (int) pp.Y - height/2 + offset.Y*symbolscale, width, height); + //} + var width = symbol.Width * symbolscale; + var height = symbol.Height * symbolscale; + g.DrawImage(symbol, pp.X - width / 2 + offset.X * symbolscale, + pp.Y - height / 2 + offset.Y * symbolscale, width, height); + g.Transform = startingTransform; + } + else + { + //if (symbolscale == 1f) + //{ + // g.DrawImageUnscaled(symbol, (int) (pp.X - symbol.Width/2f + offset.X), + // (int) (pp.Y - symbol.Height/2f + offset.Y)); + //} + //else + //{ + // var width = symbol.Width*symbolscale; + // var height = symbol.Height*symbolscale; + // g.DrawImage(symbol, (int) pp.X - width/2 + offset.X*symbolscale, + // (int) pp.Y - height/2 + offset.Y*symbolscale, width, height); + //} + var width = symbol.Width * symbolscale; + var height = symbol.Height * symbolscale; + g.DrawImage(symbol, pp.X - width / 2 + offset.X * symbolscale, + pp.Y - height / 2 + offset.Y * symbolscale, width, height); + } + } + } + + /// + /// Renders a to the map. + /// + /// Graphics reference + /// MultiPoint to render + /// Symbol to place over point + /// The amount that the symbol should be scaled. A scale of '1' equals to no scaling + /// Symbol offset af scale=1 + /// Symbol rotation in degrees + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawMultiPoint(Graphics g, IMultiPoint points, Image symbol, float symbolscale, PointF offset, + float rotation, MapViewport map) + { + for (var i = 0; i < points.NumGeometries; i++) + { + var point = (IPoint) points[i]; + DrawPoint(g, point, symbol, symbolscale, offset, rotation, map); + } + } + + /// + /// Renders a to the map. + /// + /// Graphics reference + /// MultiPoint to render + /// Symbolizer to decorate point + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawMultiPoint(IPointSymbolizer symbolizer, Graphics g, IMultiPoint points, MapViewport map) + { + symbolizer.Render(map, points, g); + } + + /// + /// Renders a to the map. + /// + /// Graphics reference + /// MultiPoint to render + /// Brush reference + /// Size of drawn Point + /// Symbol offset af scale=1 + /// Map reference + [MethodImpl(MethodImplOptions.Synchronized)] + public static void DrawMultiPoint(Graphics g, IMultiPoint points, Brush brush, float size, PointF offset, MapViewport map) + { + for (var i = 0; i < points.NumGeometries; i++) + { + var point = (IPoint) points[i]; + DrawPoint(g, point, brush, size, offset, map); + } + } + + #region Nested type: ClipState + + private enum ClipState + { + Within, + Outside, + Intersecting + } ; + + #endregion + + public static RectangleF RectExpandToInclude(RectangleF first, RectangleF second) + { + if (second.IsEmpty) return first; + if (first.IsEmpty) return second; + + var maxX = Math.Max(first.Right, second.Right); + var maxY = Math.Max(first.Bottom,second.Bottom); + + if (first.X > second.X) first.X = second.X; + if (first.Y > second.Y) first.Y = second.Y; + + if (first.Right < maxX) first.Width = maxX - first.X; + if (first.Bottom < maxY) first.Height = maxY - first.Y; + + return first; + } + } +} From 774040bc37af21f5db816fd63df1a1fe657213a4 Mon Sep 17 00:00:00 2001 From: Tim C Date: Sun, 26 Jan 2020 21:55:23 +1300 Subject: [PATCH 27/51] Complete affectedArea implementation for LabelLayer --- SharpMap/Layers/LabelLayer.cs | 26 +- .../Rendering/Symbolizer/WarpPathToPath.cs | 952 ++++----- SharpMap/Rendering/TextOnPath.cs | 1707 +++++++++-------- SharpMap/Rendering/VectorRenderer.cs | 23 +- 4 files changed, 1358 insertions(+), 1350 deletions(-) diff --git a/SharpMap/Layers/LabelLayer.cs b/SharpMap/Layers/LabelLayer.cs index b8a3f154..9595cca3 100644 --- a/SharpMap/Layers/LabelLayer.cs +++ b/SharpMap/Layers/LabelLayer.cs @@ -586,8 +586,6 @@ public override void Render(Graphics g, MapViewport map) label.Style.BackColor, label.Style.Halo, label.Rotation, label.Text, map, label.Style.HorizontalAlignment, label.LabelPoint); - - affectedArea = VectorRenderer.RectExpandToInclude(affectedArea, rect); } else { @@ -602,44 +600,30 @@ public override void Render(Graphics g, MapViewport map) //g.FillPolygon(labels[i].Style.BackColor, labels[i].TextOnPathLabel.PointsText.ToArray()); } } - - label.TextOnPathLabel.DrawTextOnPath(); + rect = label.TextOnPathLabel.DrawTextOnPathEx(); } } else if (labels[i] is PathLabel) { var plbl = labels[i] as PathLabel; var lblStyle = plbl.Style; - g.DrawString(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), plbl.Text, + rect = g.DrawStringEx(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), plbl.Text, lblStyle.Font.FontFamily, (int) lblStyle.Font.Style, lblStyle.Font.Size, lblStyle.GetStringFormat(), lblStyle.IgnoreLength, plbl.Location); } + affectedArea = VectorRenderer.RectExpandToInclude(affectedArea, rect); } if (!affectedArea.IsEmpty) { - // TODO optimise for Rotation = 0. var pts = new PointF[] { new PointF(affectedArea.Left - 1, affectedArea.Top - 1), - new PointF(affectedArea.Right + 1, affectedArea.Top - 1), new PointF(affectedArea.Right + 1, affectedArea.Bottom + 1), - new PointF(affectedArea.Left - 1, affectedArea.Bottom + 1) }; - - var coords = map.ImageToWorld(pts, true); - - var minX = Math.Min(coords[0].X, Math.Min(coords[1].X, Math.Min(coords[2].X, coords[3].X))); - var maxX = Math.Max(coords[0].X, Math.Max(coords[1].X, Math.Max(coords[2].X, coords[3].X))); - var minY = Math.Min(coords[0].Y, Math.Min(coords[1].Y, Math.Min(coords[2].Y, coords[3].Y))); - var maxY = Math.Max(coords[0].Y, Math.Max(coords[1].Y, Math.Max(coords[2].Y, coords[3].Y))); - - var env = new Envelope(minX, maxX, minY, maxY); - //env.ExpandBy(env.Width * 0.05, env.Height * 0.05); - - base._affectedArea.ExpandToInclude(env); + var coords = map.ImageToWorld(pts, false); + base._affectedArea.ExpandToInclude(new Envelope(coords[0], coords[1])); } - base.Render(g, map); } diff --git a/SharpMap/Rendering/Symbolizer/WarpPathToPath.cs b/SharpMap/Rendering/Symbolizer/WarpPathToPath.cs index 43db5efb..d3bf1d54 100644 --- a/SharpMap/Rendering/Symbolizer/WarpPathToPath.cs +++ b/SharpMap/Rendering/Symbolizer/WarpPathToPath.cs @@ -1,472 +1,480 @@ -// Copyright 2011 - Felix Obermaier (www.ivv-aachen.de) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; - -namespace SharpMap.Rendering.Symbolizer -{ - /// - /// Class that warps one path to another path, e.g. a pattern to a linestring - /// - internal static class WarpPathToPath - { - internal class GraphSegment - { - internal readonly PointF First; - internal readonly PointF Second; - private readonly float _startOffset; - private readonly double _ndx, _ndy; - private readonly double _length; - - internal GraphSegment(float startOffset, PointF first, PointF second) - { - _startOffset = startOffset; - First = first; - Second = second; - double dx = second.X - first.X; - double dy = second.Y - first.Y; - _length = (float)Math.Sqrt(dx * dx + dy * dy); - _ndx = dx / _length; - _ndy = dy / _length; - } - - internal float DX { get { return (float)_ndx; } } - internal float DY { get { return (float)_ndy; } } - internal float Length { get { return (float)_length; } } - - internal PointF GetLinePoint(float ordinateX) - { - ordinateX -= _startOffset; - return new PointF(First.X + (float)(ordinateX * _ndx), - First.Y + (float)(ordinateX * _ndy)); - } - } - - /// - /// Warps one path to the flattened () version of another path. - /// This comes in handy for - /// - /// Linestyles that cannot be created with available -properties - /// Warping Text to curves - /// ... - /// - /// - /// The path to warp to. This path is flattened before being used, so there is no need to call prior to this function call. - /// The path to warp - /// Defines whether is a pattern or not. If is a pattern, it is repeated until it has the total length of - /// The interval in which the pattern should be repeated - /// Warped - /// If either pathToWarpTo or pathToWarp is null - public static GraphicsPath Warp(GraphicsPath pathToWarpTo, GraphicsPath pathToWarp, bool isPattern, float interval) - { - //Test for valid arguments - if (pathToWarpTo == null) - throw new ArgumentNullException("pathToWarpTo"); - if (pathToWarp == null) - throw new ArgumentNullException("pathToWarp"); - - //Remove all curves from path to warp to, get total length. - SortedList edges; - pathToWarpTo.Flatten(); - Double pathLength = GetPathLength(pathToWarpTo, out edges); - if (pathLength == 0) - return pathToWarp; - - //Prepare path to warp - pathToWarp = PreparePathToWarp(pathToWarp, isPattern, pathLength, interval); - if (pathToWarp == null || pathToWarp.PointCount == 0) return null; - GraphicsPath warpedPath = new GraphicsPath(pathToWarp.FillMode); - using (GraphicsPathIterator iter = new GraphicsPathIterator(pathToWarp)) - { - GraphicsPath subPath = new GraphicsPath(); - int currentIndex = 0; - if (iter.SubpathCount > 1) - { - bool isClosed; - while (iter.NextSubpath(subPath, out isClosed) > 0) - { - GraphicsPath warpedSubPath = WarpSubpath(subPath, edges, ref currentIndex); - if (isClosed) warpedSubPath.CloseFigure(); - warpedPath.AddPath(warpedSubPath, true); - warpedPath.SetMarkers(); - } - } - else - { - warpedPath = WarpSubpath(pathToWarp, edges, ref currentIndex); - } - } - return warpedPath; - } - - private static GraphicsPath WarpSubpath(GraphicsPath pathToWarp, SortedList edges, ref int currentIndex) - { - //midpoint of figure - float minX = float.MaxValue, maxX = float.MinValue; - PointF[] pathPoints = pathToWarp.PathPoints; - for (int i = 0; i < pathToWarp.PointCount; i++) - { - minX = Math.Min(minX, pathPoints[i].X); - maxX = Math.Max(maxX, pathPoints[i].X); - } - float ordinateX = (minX + maxX) * 0.5f; - - //seek pathToWarpTo segment - while (true) - { - if (edges.Keys[currentIndex] <= ordinateX && - ((currentIndex == edges.Keys.Count - 1) || ordinateX < edges.Keys[currentIndex + 1])) - { - break; - } - if (ordinateX < edges.Keys[currentIndex]) - { - if (currentIndex == 0) break; - currentIndex--; - } - else - { - if (currentIndex == edges.Count + 1) break; - currentIndex++; - } - } - - GraphSegment s = edges.Values[currentIndex]; - float dy = s.DX; - float dx = s.DY; - PointF[] warpedPathPoints = new PointF[pathToWarp.PointCount]; - for (int i = 0; i < pathToWarp.PointCount; i++) - { - float ptX = pathPoints[i].X; - float ptY = pathPoints[i].Y; - - PointF linePt = s.GetLinePoint(ptX); - ptX = -ptY * dx; - ptY = ptY * dy; - - warpedPathPoints[i] = new PointF(linePt.X + ptX, linePt.Y + ptY); - } - return new GraphicsPath(warpedPathPoints, pathToWarp.PathTypes, pathToWarp.FillMode); - } - - /// - /// Calculates the length of all segments in the Path, visible or not - /// - /// A flattened - /// A containing edges of the path. - /// the length - internal static double GetPathLength(GraphicsPath path, out SortedList edges) - { - float length = 0; - edges = new SortedList(path.PointCount - 2); - PointF ptLast = new PointF(); - PointF ptClose = new PointF(); - for (int i = 0; i < path.PointCount; i++) - { - var ptCurr = path.PathPoints[i]; - if (path.PathTypes[i] == (byte)PathPointType.Start) - ptClose = ptCurr; - if (ptCurr.Equals(ptLast)) continue; - - if (!ptLast.IsEmpty) - { - - var gs = new GraphSegment(length, ptLast, ptCurr); - edges.Add(length, gs); - length += gs.Length; - if ((path.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) - { - gs = new GraphSegment(length, ptCurr, ptClose); - edges.Add(length, gs); - length += gs.Length; - ptLast = ptClose; - } - else - { - ptLast = ptCurr; - } - } - else - { - ptLast = ptCurr; - } - } - return length; - } - - /// - /// Prepares a text path to be warped to a path - /// This operation performs the following tasks - /// - /// Check if the length is less than or is set to true - /// Translate according to horizontal alignment - /// - /// The text path - /// The total length of the path to warp to - /// value indicating if the text path should only be handled if the path fits the total length - /// The string format - /// The prepared text path - internal static GraphicsPath PrepareTextPathToWarp(GraphicsPath path, Double totalPathLength, bool ignoreLength, StringFormat format) - { - var rect = path.GetBounds(); - - double maxX = rect.Right; - double minX = rect.Left; - - //Check if text path fits or is really wanted - double len = maxX - minX; - if (len > totalPathLength && !ignoreLength) - return null; - - //Offset for center - double xStart; - switch (format.Alignment) - { - default: - xStart = (totalPathLength) * 0.5d; - break; - case StringAlignment.Near: - xStart = 0; - break; - case StringAlignment.Far: - xStart = totalPathLength; - break; - } - path.Transform(new Matrix(1f, 0f, 0f, 1f, (float)xStart, 0f)); - - return path; - } - - /// - /// - /// - /// - /// - /// - /// - /// - internal static GraphicsPath PreparePathToWarp(GraphicsPath path, bool isPattern, Double totalPathLength, Double interval) - { - var rect = path.GetBounds(new Matrix()); - /* - var rect = //System.Drawing.RectangleF.Empty; - try - { - rect = GetPathBounds(path); - } - catch (Exception ex) - { - return null; - } - */ - - double maxX = rect.Right; - double minX = rect.Left; - - /* - PointF[] pathPoints = path.PathPoints; - for (int i = 0; i < path.PointCount; i++) - { - minX = Math.Min(minX, pathPoints[i].X); - maxX = Math.Max(maxX, pathPoints[i].X); - } - */ - - //if path has x-ordinates less than 0, we need to shift the path to values greater than 0. - if (minX < 0d) - { - Matrix m = new Matrix(1f, 0f, 0f, 1f, (float)minX, 0f); - path.Transform(m); - maxX -= minX; - } - - //Complete pattern - if (isPattern) // && maxX < totalPathLength) - { - GraphicsPath patternPath = new GraphicsPath(); - double pathLength = maxX; - interval = interval - pathLength; - if (interval < pathLength) interval = pathLength; - Matrix m = new Matrix(1f, 0f, 0f, 1f, (float)interval, 0f); - while (maxX < totalPathLength) - { - patternPath.StartFigure(); - patternPath.AddPath(path, true); - maxX += interval; - path.Transform(m); - } - GraphicsPath clippedPattern = ClipPath(path, (float)totalPathLength); - if (clippedPattern.PointCount > 0) - patternPath.AddPath(clippedPattern, false); - return patternPath; - } - return path; - } - - private static RectangleF GetPathBounds(GraphicsPath path) - { - var pts = path.PathPoints; - if (pts.Length == 0) return RectangleF.Empty; - - var r = new RectangleF(pts[0], Size.Empty); - for (var i = 1; i < pts.Length; i++) - r = RectangleF.Union(r, new RectangleF(pts[i], Size.Empty)); - return r; - } - - /// - /// Clip the path to a provided length. This is tricky - /// - /// the path to be clipped - /// the maximum length of the path - /// - internal static GraphicsPath ClipPath(GraphicsPath patternPath, float totalPathLength) - { - patternPath.Flatten(); - var returnPath = new GraphicsPath(patternPath.FillMode); - - using (var iter = new GraphicsPathIterator(patternPath)) - { - var pathPoints = new List(patternPath.PointCount); - var pathTypes = new List(patternPath.PointCount); - var lastPoint = new PointF(); - var lastValid = true; - for (int i = 0; i < patternPath.PointCount; i++) - { - //current point is valid - if (CheckMaxX(patternPath.PathPoints[i].X, totalPathLength)) - { - //last point was not valid, we need to add intersection point first - if (!lastValid) - { - PointF borderIntersectionPoint = BorderIntersectionPoint(lastPoint, - patternPath.PathPoints[i], - totalPathLength); - if (!borderIntersectionPoint.Equals(patternPath.PathPoints[i])) - { - pathPoints.Add(borderIntersectionPoint); - pathTypes.Add((byte)PathPointType.Line); - } - lastValid = true; - } - //Add the valid point - pathPoints.Add(patternPath.PathPoints[i]); - pathTypes.Add(patternPath.PathTypes[i]); - } - else - { - //last point was valid - if (lastValid && i > 0) - { - pathPoints.Add(BorderIntersectionPoint(lastPoint, patternPath.PathPoints[i], totalPathLength)); - pathTypes.Add(patternPath.PathTypes[i]); - } - lastValid = false; - } - lastPoint = patternPath.PathPoints[i]; - } - - //All or part of the subpath is valid - if (pathPoints.Count > 0) - { - //remove last point, if dangeling - if (pathTypes[pathTypes.Count - 1] == 0) - { - pathPoints.RemoveAt(pathTypes.Count - 1); - pathTypes.RemoveAt(pathTypes.Count - 1); - } - } - - if (pathPoints.Count > 0) - { - GraphicsPath addPath = new GraphicsPath(pathPoints.ToArray(), pathTypes.ToArray(), - patternPath.FillMode); - returnPath.AddPath(addPath, true); - } - } - return returnPath; - } - - /// - /// - /// - /// - /// - /// - /// - private static PointF BorderIntersectionPoint(PointF lastPoint, PointF pathPoint, float totalPathLength) - { - Single dxBorder = totalPathLength - lastPoint.X; - Single dx = pathPoint.X - lastPoint.X; - Single dy = pathPoint.Y - lastPoint.Y; - - Single fraction = dxBorder / dx; - return new PointF(lastPoint.X + fraction * dx, lastPoint.Y + fraction * dy); - } - - private static bool CheckMaxX(Single x, Single maxX) - { - return x <= maxX; - } - - /// - /// Renders text along the path - /// - /// The graphics object - /// The pen to render the halo outline - /// The brush to fill the text - /// The text to render - /// The font family to use - /// The style - /// The size - /// The format - /// - /// - public static void DrawString(this Graphics self, Pen halo, Brush fill, string text, FontFamily fontFamily, int style, float emSize, StringFormat format, bool ignoreLength, GraphicsPath path) - { - if (path == null || path.PointCount == 0) - return; - - var gp = new GraphicsPath(); - gp.AddString(text, fontFamily, style, emSize, new Point(0, 0), format); - - SortedList edges; - var totalLength = GetPathLength(path, out edges); - - var warpedPath = PrepareTextPathToWarp(gp, totalLength, ignoreLength, format); - - if (warpedPath != null) - { - var wp = Warp(path, warpedPath, false, 0f); - if (wp != null) - { - if (halo != null) - self.DrawPath(halo, wp); - if (fill != null) - self.FillPath(fill, wp); - wp.Dispose(); - } - warpedPath.Dispose(); - } - gp.Dispose(); - } - - } -} +// Copyright 2011 - Felix Obermaier (www.ivv-aachen.de) +// +// This file is part of SharpMap. +// SharpMap is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// SharpMap is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with SharpMap; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace SharpMap.Rendering.Symbolizer +{ + /// + /// Class that warps one path to another path, e.g. a pattern to a linestring + /// + internal static class WarpPathToPath + { + internal class GraphSegment + { + internal readonly PointF First; + internal readonly PointF Second; + private readonly float _startOffset; + private readonly double _ndx, _ndy; + private readonly double _length; + + internal GraphSegment(float startOffset, PointF first, PointF second) + { + _startOffset = startOffset; + First = first; + Second = second; + double dx = second.X - first.X; + double dy = second.Y - first.Y; + _length = (float)Math.Sqrt(dx * dx + dy * dy); + _ndx = dx / _length; + _ndy = dy / _length; + } + + internal float DX { get { return (float)_ndx; } } + internal float DY { get { return (float)_ndy; } } + internal float Length { get { return (float)_length; } } + + internal PointF GetLinePoint(float ordinateX) + { + ordinateX -= _startOffset; + return new PointF(First.X + (float)(ordinateX * _ndx), + First.Y + (float)(ordinateX * _ndy)); + } + } + + /// + /// Warps one path to the flattened () version of another path. + /// This comes in handy for + /// + /// Linestyles that cannot be created with available -properties + /// Warping Text to curves + /// ... + /// + /// + /// The path to warp to. This path is flattened before being used, so there is no need to call prior to this function call. + /// The path to warp + /// Defines whether is a pattern or not. If is a pattern, it is repeated until it has the total length of + /// The interval in which the pattern should be repeated + /// Warped + /// If either pathToWarpTo or pathToWarp is null + public static GraphicsPath Warp(GraphicsPath pathToWarpTo, GraphicsPath pathToWarp, bool isPattern, float interval) + { + //Test for valid arguments + if (pathToWarpTo == null) + throw new ArgumentNullException("pathToWarpTo"); + if (pathToWarp == null) + throw new ArgumentNullException("pathToWarp"); + + //Remove all curves from path to warp to, get total length. + SortedList edges; + pathToWarpTo.Flatten(); + Double pathLength = GetPathLength(pathToWarpTo, out edges); + if (pathLength == 0) + return pathToWarp; + + //Prepare path to warp + pathToWarp = PreparePathToWarp(pathToWarp, isPattern, pathLength, interval); + if (pathToWarp == null || pathToWarp.PointCount == 0) return null; + GraphicsPath warpedPath = new GraphicsPath(pathToWarp.FillMode); + using (GraphicsPathIterator iter = new GraphicsPathIterator(pathToWarp)) + { + GraphicsPath subPath = new GraphicsPath(); + int currentIndex = 0; + if (iter.SubpathCount > 1) + { + bool isClosed; + while (iter.NextSubpath(subPath, out isClosed) > 0) + { + GraphicsPath warpedSubPath = WarpSubpath(subPath, edges, ref currentIndex); + if (isClosed) warpedSubPath.CloseFigure(); + warpedPath.AddPath(warpedSubPath, true); + warpedPath.SetMarkers(); + } + } + else + { + warpedPath = WarpSubpath(pathToWarp, edges, ref currentIndex); + } + } + return warpedPath; + } + + private static GraphicsPath WarpSubpath(GraphicsPath pathToWarp, SortedList edges, ref int currentIndex) + { + //midpoint of figure + float minX = float.MaxValue, maxX = float.MinValue; + PointF[] pathPoints = pathToWarp.PathPoints; + for (int i = 0; i < pathToWarp.PointCount; i++) + { + minX = Math.Min(minX, pathPoints[i].X); + maxX = Math.Max(maxX, pathPoints[i].X); + } + float ordinateX = (minX + maxX) * 0.5f; + + //seek pathToWarpTo segment + while (true) + { + if (edges.Keys[currentIndex] <= ordinateX && + ((currentIndex == edges.Keys.Count - 1) || ordinateX < edges.Keys[currentIndex + 1])) + { + break; + } + if (ordinateX < edges.Keys[currentIndex]) + { + if (currentIndex == 0) break; + currentIndex--; + } + else + { + if (currentIndex == edges.Count + 1) break; + currentIndex++; + } + } + + GraphSegment s = edges.Values[currentIndex]; + float dy = s.DX; + float dx = s.DY; + PointF[] warpedPathPoints = new PointF[pathToWarp.PointCount]; + for (int i = 0; i < pathToWarp.PointCount; i++) + { + float ptX = pathPoints[i].X; + float ptY = pathPoints[i].Y; + + PointF linePt = s.GetLinePoint(ptX); + ptX = -ptY * dx; + ptY = ptY * dy; + + warpedPathPoints[i] = new PointF(linePt.X + ptX, linePt.Y + ptY); + } + return new GraphicsPath(warpedPathPoints, pathToWarp.PathTypes, pathToWarp.FillMode); + } + + /// + /// Calculates the length of all segments in the Path, visible or not + /// + /// A flattened + /// A containing edges of the path. + /// the length + internal static double GetPathLength(GraphicsPath path, out SortedList edges) + { + float length = 0; + edges = new SortedList(path.PointCount - 2); + PointF ptLast = new PointF(); + PointF ptClose = new PointF(); + for (int i = 0; i < path.PointCount; i++) + { + var ptCurr = path.PathPoints[i]; + if (path.PathTypes[i] == (byte)PathPointType.Start) + ptClose = ptCurr; + if (ptCurr.Equals(ptLast)) continue; + + if (!ptLast.IsEmpty) + { + + var gs = new GraphSegment(length, ptLast, ptCurr); + edges.Add(length, gs); + length += gs.Length; + if ((path.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) + { + gs = new GraphSegment(length, ptCurr, ptClose); + edges.Add(length, gs); + length += gs.Length; + ptLast = ptClose; + } + else + { + ptLast = ptCurr; + } + } + else + { + ptLast = ptCurr; + } + } + return length; + } + + /// + /// Prepares a text path to be warped to a path + /// This operation performs the following tasks + /// + /// Check if the length is less than or is set to true + /// Translate according to horizontal alignment + /// + /// The text path + /// The total length of the path to warp to + /// value indicating if the text path should only be handled if the path fits the total length + /// The string format + /// The prepared text path + internal static GraphicsPath PrepareTextPathToWarp(GraphicsPath path, Double totalPathLength, bool ignoreLength, StringFormat format) + { + var rect = path.GetBounds(); + + double maxX = rect.Right; + double minX = rect.Left; + + //Check if text path fits or is really wanted + double len = maxX - minX; + if (len > totalPathLength && !ignoreLength) + return null; + + //Offset for center + double xStart; + switch (format.Alignment) + { + default: + xStart = (totalPathLength) * 0.5d; + break; + case StringAlignment.Near: + xStart = 0; + break; + case StringAlignment.Far: + xStart = totalPathLength; + break; + } + path.Transform(new Matrix(1f, 0f, 0f, 1f, (float)xStart, 0f)); + + return path; + } + + /// + /// + /// + /// + /// + /// + /// + /// + internal static GraphicsPath PreparePathToWarp(GraphicsPath path, bool isPattern, Double totalPathLength, Double interval) + { + var rect = path.GetBounds(new Matrix()); + /* + var rect = //System.Drawing.RectangleF.Empty; + try + { + rect = GetPathBounds(path); + } + catch (Exception ex) + { + return null; + } + */ + + double maxX = rect.Right; + double minX = rect.Left; + + /* + PointF[] pathPoints = path.PathPoints; + for (int i = 0; i < path.PointCount; i++) + { + minX = Math.Min(minX, pathPoints[i].X); + maxX = Math.Max(maxX, pathPoints[i].X); + } + */ + + //if path has x-ordinates less than 0, we need to shift the path to values greater than 0. + if (minX < 0d) + { + Matrix m = new Matrix(1f, 0f, 0f, 1f, (float)minX, 0f); + path.Transform(m); + maxX -= minX; + } + + //Complete pattern + if (isPattern) // && maxX < totalPathLength) + { + GraphicsPath patternPath = new GraphicsPath(); + double pathLength = maxX; + interval = interval - pathLength; + if (interval < pathLength) interval = pathLength; + Matrix m = new Matrix(1f, 0f, 0f, 1f, (float)interval, 0f); + while (maxX < totalPathLength) + { + patternPath.StartFigure(); + patternPath.AddPath(path, true); + maxX += interval; + path.Transform(m); + } + GraphicsPath clippedPattern = ClipPath(path, (float)totalPathLength); + if (clippedPattern.PointCount > 0) + patternPath.AddPath(clippedPattern, false); + return patternPath; + } + return path; + } + + private static RectangleF GetPathBounds(GraphicsPath path) + { + var pts = path.PathPoints; + if (pts.Length == 0) return RectangleF.Empty; + + var r = new RectangleF(pts[0], Size.Empty); + for (var i = 1; i < pts.Length; i++) + r = RectangleF.Union(r, new RectangleF(pts[i], Size.Empty)); + return r; + } + + /// + /// Clip the path to a provided length. This is tricky + /// + /// the path to be clipped + /// the maximum length of the path + /// + internal static GraphicsPath ClipPath(GraphicsPath patternPath, float totalPathLength) + { + patternPath.Flatten(); + var returnPath = new GraphicsPath(patternPath.FillMode); + + using (var iter = new GraphicsPathIterator(patternPath)) + { + var pathPoints = new List(patternPath.PointCount); + var pathTypes = new List(patternPath.PointCount); + var lastPoint = new PointF(); + var lastValid = true; + for (int i = 0; i < patternPath.PointCount; i++) + { + //current point is valid + if (CheckMaxX(patternPath.PathPoints[i].X, totalPathLength)) + { + //last point was not valid, we need to add intersection point first + if (!lastValid) + { + PointF borderIntersectionPoint = BorderIntersectionPoint(lastPoint, + patternPath.PathPoints[i], + totalPathLength); + if (!borderIntersectionPoint.Equals(patternPath.PathPoints[i])) + { + pathPoints.Add(borderIntersectionPoint); + pathTypes.Add((byte)PathPointType.Line); + } + lastValid = true; + } + //Add the valid point + pathPoints.Add(patternPath.PathPoints[i]); + pathTypes.Add(patternPath.PathTypes[i]); + } + else + { + //last point was valid + if (lastValid && i > 0) + { + pathPoints.Add(BorderIntersectionPoint(lastPoint, patternPath.PathPoints[i], totalPathLength)); + pathTypes.Add(patternPath.PathTypes[i]); + } + lastValid = false; + } + lastPoint = patternPath.PathPoints[i]; + } + + //All or part of the subpath is valid + if (pathPoints.Count > 0) + { + //remove last point, if dangeling + if (pathTypes[pathTypes.Count - 1] == 0) + { + pathPoints.RemoveAt(pathTypes.Count - 1); + pathTypes.RemoveAt(pathTypes.Count - 1); + } + } + + if (pathPoints.Count > 0) + { + GraphicsPath addPath = new GraphicsPath(pathPoints.ToArray(), pathTypes.ToArray(), + patternPath.FillMode); + returnPath.AddPath(addPath, true); + } + } + return returnPath; + } + + /// + /// + /// + /// + /// + /// + /// + private static PointF BorderIntersectionPoint(PointF lastPoint, PointF pathPoint, float totalPathLength) + { + Single dxBorder = totalPathLength - lastPoint.X; + Single dx = pathPoint.X - lastPoint.X; + Single dy = pathPoint.Y - lastPoint.Y; + + Single fraction = dxBorder / dx; + return new PointF(lastPoint.X + fraction * dx, lastPoint.Y + fraction * dy); + } + + private static bool CheckMaxX(Single x, Single maxX) + { + return x <= maxX; + } + + /// + /// Renders text along the path + /// + /// The graphics object + /// The pen to render the halo outline + /// The brush to fill the text + /// The text to render + /// The font family to use + /// The style + /// The size + /// The format + /// + /// + public static void DrawString(this Graphics self, Pen halo, Brush fill, string text, FontFamily fontFamily, int style, float emSize, StringFormat format, bool ignoreLength, GraphicsPath path) + { + DrawStringEx(self, halo, fill, text, fontFamily, style, emSize, format, ignoreLength, path); + } + + public static RectangleF DrawStringEx(this Graphics self, Pen halo, Brush fill, string text, FontFamily fontFamily, int style, float emSize, StringFormat format, bool ignoreLength, GraphicsPath path) + { + var rect = new RectangleF(); + if (path == null || path.PointCount == 0) + return rect; + + var gp = new GraphicsPath(); + gp.AddString(text, fontFamily, style, emSize, new Point(0, 0), format); + + SortedList edges; + var totalLength = GetPathLength(path, out edges); + + var warpedPath = PrepareTextPathToWarp(gp, totalLength, ignoreLength, format); + + if (warpedPath != null) + { + var wp = Warp(path, warpedPath, false, 0f); + if (wp != null) + { + if (halo != null) + self.DrawPath(halo, wp); + if (fill != null) + self.FillPath(fill, wp); + + rect = wp.GetBounds(); + wp.Dispose(); + } + warpedPath.Dispose(); + } + gp.Dispose(); + return rect; + } + } +} diff --git a/SharpMap/Rendering/TextOnPath.cs b/SharpMap/Rendering/TextOnPath.cs index f35f3e77..550957f8 100644 --- a/SharpMap/Rendering/TextOnPath.cs +++ b/SharpMap/Rendering/TextOnPath.cs @@ -1,849 +1,858 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Globalization; - -namespace SharpMap.Rendering -{ - /// - /// Horizontal alignment options for texts on path - /// - public enum TextPathAlign - { - /// - /// Aligned on the left - /// - Left = 0, - /// - /// Aligned in the middle - /// - Center = 1, - /// - /// Aligned on the right - /// - Right = 2 - } - - /// - /// Vertical alignment option for texts on path - /// - public enum TextPathPosition - { - /// - /// Above the path - /// - OverPath = 0, - /// - /// Center - /// - CenterPath = 1, - /// - /// Below the path - /// - UnderPath = 2 - } - - /// - /// Extensions methods for text on path label rendering - /// - public static class GraphicsExtension - { - /// - /// Method to measure the length of a string - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The describing the - /// An array of s - public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, GraphicsPath graphicsPath) - { - return MeasureString(graphics, s, font, brush, TextPathAlign.Left, TextPathPosition.CenterPath, 100, graphicsPath); - } - - /// - /// Method to measure the length of a string - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// The describing the - /// An array of s - public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, GraphicsPath graphicsPath) - { - return MeasureString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, graphicsPath); - } - - /// - /// Method to draw on the provided - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The describing the - public static void DrawString(Graphics graphics, string s, Font font, Brush brush, GraphicsPath graphicsPath) - { - DrawString(graphics, s, font, brush, TextPathAlign.Left, TextPathPosition.CenterPath, 100, graphicsPath); - } - - /// - /// Method to draw on the provided - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// The describing the - public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, GraphicsPath graphicsPath) - { - DrawString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, graphicsPath); - } - - /// - /// Method to measure the length of a string along a - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// A value controlling the spacing between letters - /// The describing the - /// An array of s - public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, GraphicsPath graphicsPath) - { - var angles = new List(); - var pointsText = new List(); - var pointsTextUp = new List(); - return MeasureString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, 0, graphicsPath, ref angles, ref pointsText, ref pointsTextUp); - } - - /// - /// Method to measure the length of a string along a - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// A value controlling the spacing between letters - /// The describing the - /// An array of s - public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, GraphicsPath graphicsPath) - { - DrawString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, 0, graphicsPath,false); - } - - /// - /// Method to draw a string on a of a string - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// A value controlling the spacing between letters - /// A value controlling the rotation of - /// The along which to render. - /// A list of angle values (in degrees), one for each letter - /// A list of positions, one for each letter - /// A list of points (don't know what for) - public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, - TextPathAlign textPathAlign, TextPathPosition textPathPosition, - int letterSpace, float rotateDegree, GraphicsPath graphicsPath, - ref List angles, ref List pointsText, - ref List pointsUp) - { - var top = TextOnPath.TextOnPathInstance; - top.Text = s; - top.Font = font; - top.FillColorTop = brush; - top.TextPathPathPosition = textPathPosition; - top.TextPathAlignTop = textPathAlign; - top.PathDataTop = graphicsPath.PathData; - top.LetterSpacePercentage = letterSpace; - top.Graphics = graphics; - top.GraphicsPath = graphicsPath; - top.MeasureString = true; - top.RotateDegree = rotateDegree; - top.DrawTextOnPath(); - angles = top.Angles; - pointsText = top.PointsText; - pointsUp = top.PointsTextUp; - return top.RegionList.ToArray(); - } - - /// - /// Method to draw a string on a of a string - /// - /// The object to use - /// The string to measure - /// The to use - /// The to use - /// The horizontal position on the - /// The vertical position on the - /// A value controlling the spacing between letters - /// A value controlling the rotation of - /// The describing the - /// A value indicating if the should be drawn, too. - public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, float rotateDegree, GraphicsPath graphicsPath, bool showPath) - { - var top = TextOnPath.TextOnPathInstance; - - top.Text = s; - top.Font = font; - top.FillColorTop = brush; - top.TextPathPathPosition = textPathPosition; - top.TextPathAlignTop = textPathAlign; - top.PathDataTop = graphicsPath.PathData; - top.LetterSpacePercentage = letterSpace; - top.Graphics = graphics; - top.GraphicsPath = graphicsPath; - top.MeasureString = false; - top.RotateDegree = rotateDegree; - top.ShowPath = showPath; - top.DrawTextOnPath(); - - } - } - - /// - /// Text on path generator class - /// - public class TextOnPath - { - internal readonly static TextOnPath TextOnPathInstance = new TextOnPath(); - - private PathData _pathdata; - - private bool _measureString; - private Font _font; - private Pen _colorHalo = new Pen(Color.Black,1); - private Brush _fillBrush = new SolidBrush(Color.Black); - private TextPathAlign _pathalign = TextPathAlign.Center; - private double _letterspacepercentage = 1; - private TextPathPosition _textPathPathPosition = TextPathPosition.CenterPath; - - //ToDo, this is a result, intermediate? - private List _regionList = new List(); - private List _pointText = new List(); - private List _pointTextUp = new List(); - private readonly List _angles = new List(); - - /// - /// The last catched exception is stored here - /// - public Exception LastError; - - - /// - /// Gets or sets the object - /// - public Graphics Graphics { get; set; } - - /// - /// Gets or sets a value indicating the used to render text along - /// - public GraphicsPath GraphicsPath { get; set; } - - /// - /// Gets or sets a value indicating whether the string should be measured - /// - public bool MeasureString - { - get { return _measureString; } - set { _measureString = value; } - } - - - /// - /// Gets or sets a list of regions - /// - public List RegionList - { - get { return _regionList; } - set { _regionList = value; } - } - - /// - /// Gets or sets a list of s - /// - public List PointsText - { - get { return _pointText; } - set { _pointText = value; } - } - - /// - /// Gets or sets a list of s - /// - public List PointsTextUp - { - get { return _pointTextUp; } - set { _pointTextUp = value; } - } - - /// - /// Gets or sets a list of angles (in radians?) - /// - public List Angles - { - get { return _angles; } - } - - /// - /// Gets or sets a value indicating the rotation - /// - public float RotateDegree { get; set; } - - /// - /// Creates an instance of this class - /// - internal TextOnPath() - { - PathColorTop = Color.LightBlue; - } - - /// - /// Gets or sets a value indicating the vertical alignment - /// - public TextPathPosition TextPathPathPosition - { - get { return _textPathPathPosition; } - set { _textPathPathPosition = value; } - } - - /// - /// Gets or sets a value indicating the path's data - /// - public PathData PathDataTop - { - get { return _pathdata; } - set { _pathdata = value; } - } - - /// - /// Gets or sets a value indicating the text to render - /// - public string Text { get; set; } - - /// - /// Gets or sets the to use for drawing the text - /// - public Font Font - { - get { return _font; } - set { _font = value; } - } - - /// - /// Gets or sets the to use for halo'ing the text - /// - public Pen ColorHalo - { - get { return _colorHalo; } - set { _colorHalo = value; } - } - - /// - /// Gets or sets a value indicating the used to fill the text path - /// - public Brush FillColorTop - { - get { return _fillBrush; } - set { _fillBrush = value; } - } - - /// - /// Get or sets a value indicating the horizontal alignment of the path - /// - public TextPathAlign TextPathAlignTop - { - get { return _pathalign; } - set { _pathalign = value; } - } - - /// - /// Gets or sets a value indicating the color of the - /// - public Color PathColorTop { get; set; } - - - /// - /// Gets or sets a value controlling the space between letters - /// - /// The default value is 100 - public int LetterSpacePercentage - { - get { return (int)(100 * _letterspacepercentage); } - set { _letterspacepercentage = value/100d; } - } - - /// - /// Gets or sets a value indicating that the used path should be rendered as well - /// - public bool ShowPath { get; set; } - - /// - /// - /// - /// The path data - /// The text - /// The font - /// The halo pen - /// The brush to fill letters - /// The - public void DrawTextOnPath(PathData pathdata, string text, Font font, Pen haloPen, Brush fillcolor, int letterspacepercentage) - { - - _pathdata = pathdata; - Text = text; - _font = font; - _colorHalo= haloPen; - _fillBrush = fillcolor; - _letterspacepercentage = letterspacepercentage / 100d; - - DrawTextOnPath(); - } - - /// - /// Method to draw the text on the - /// - public void DrawTextOnPath() - { - var points = new PointF[25001]; - var count = 0; - var gp = new GraphicsPath(_pathdata.Points, _pathdata.Types) { FillMode = FillMode.Winding }; - _regionList.Clear(); - _angles.Clear(); - _pointText.Clear(); - _pointTextUp.Clear(); - gp.Flatten(null, 1); - try - { - var tmpPoint = gp.PathPoints[0]; - int i; - PointF[] tmpPoints; - for (i = 0; i <= gp.PathPoints.Length - 2; i++) - { - if (gp.PathTypes[i + 1] == (byte)PathPointType.Start | (gp.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) - { - tmpPoints = GetLinePoints(gp.PathPoints[i], tmpPoint, 1); - Array.ConstrainedCopy(tmpPoints, 0, points, count, tmpPoints.Length); - count += 1; - tmpPoint = gp.PathPoints[i + 1]; - } - else - { - tmpPoints = GetLinePoints(gp.PathPoints[i], gp.PathPoints[i + 1], 1); - Array.ConstrainedCopy(tmpPoints, 0, points, count, tmpPoints.Length); - count += tmpPoints.Length - 1; - - } - } - tmpPoints = new PointF[count]; - Array.Copy(points, tmpPoints, count); - points = CleanPoints(tmpPoints); - - count = points.Length - 1; - if (IsVisible(points, count)) - { - // if can show all letter - DrawText(Graphics, points, count); - } - gp.Dispose(); - //DrawText(points, count); - } - catch (Exception ex) - { - LastError = ex; - - - } - } - /// - /// Method to remove consecutive same/equal s from the array - /// - /// An array of s - /// An array of s without repeated values. - private static PointF[] CleanPoints(PointF[] points) - { - if (points == null) - return null; - if (points.Length == 0) - return new PointF[0]; - var tmpPoints = new List(points.Length); - tmpPoints.Add(points[0]); - var lastIndex = 0; - for (var i = 1; i <= points.Length - 1; i++) - { - if (!tmpPoints[lastIndex].Equals(points[i])) - { - tmpPoints.Add(points[i]); - lastIndex++; - } - } - - return tmpPoints.ToArray(); - } - - /// - /// Method to evaluate visibility - /// - /// An array of s - /// The maximum number of points - /// - public bool IsVisible(PointF[] points, int maxPoints) - { - var result = true; - var g = Graphics; - var count = 0; - var maxWidthText = default(double); - int i; - for (i = 0; i <= Text.Length - 1; i++) - { - maxWidthText += StringRegion(g, i) * LetterSpacePercentage / 100; - } - switch (_pathalign) - { - case TextPathAlign.Left: - count = 0; - break; - case TextPathAlign.Center: - count = (int)((maxPoints - maxWidthText) / 2); - break; - case TextPathAlign.Right: - count = (int)(maxPoints - maxWidthText - (double)StringRegion(g, Text.Length - 1) * LetterSpacePercentage / 100); - break; - } - var lStrWidth = (int)(StringRegion(g, 0) * LetterSpacePercentage / 100); - if ((count + lStrWidth / 2) < 0) - { - count = -(lStrWidth / 2); - } - double currentWidthText = 0; - for (int j =count+lStrWidth/2; j <= Text.Length - 1; j++) - { - currentWidthText += StringRegion(g, j) * LetterSpacePercentage / 100; - } - if ((int)currentWidthText >= maxPoints) - { - result = false; - } - return result; - - } - - private void DrawText(Graphics g, PointF[] points, int maxPoints) - { - - //GraphicsPath gp = new GraphicsPath(_pathdata.Points, _pathdata.Types) { FillMode = FillMode.Winding }; - //gp.Flatten(); - //gp.Dispose(); - //var g = _graphics; - //GraphicsContainer graphicsContainer= g.BeginContainer(); - //g.TranslateTransform(_graphicsPath.GetBounds().X, _graphicsPath.GetBounds().Y); - if (ShowPath) - { - var pen = new Pen(PathColorTop); - foreach (var p in points) - { - g.DrawEllipse(pen, p.X, p.Y, 1, 1); - } - pen.Dispose(); - } - var count = 0; - var point1 = default(PointF); - var charStep = 0; - var maxWidthText = default(double); - int i; - - for (i = 0; i <= Text.Length - 1; i++) - { - maxWidthText += StringRegion(g, i) * LetterSpacePercentage / 100; - } - - switch (_pathalign) - { - case TextPathAlign.Left: - point1 = points[0]; - count = 0; - break; - case TextPathAlign.Center: - count = (int)((maxPoints - maxWidthText) / 2); - point1 = count > 0 ? points[count] : points[0]; - - break; - case TextPathAlign.Right: - count = (int)(maxPoints - maxWidthText - (double)StringRegion(g, Text.Length - 1) * LetterSpacePercentage / 100); - point1 = count > 0 ? points[count] : points[0]; - - break; - } - var lStrWidth = (int)(StringRegion(g, charStep) * LetterSpacePercentage / 100); - if ((count + lStrWidth / 2) < 0) - { - count = -(lStrWidth / 2); - } - while (!(charStep > Text.Length - 1)) - { - lStrWidth = (int)(StringRegion(g, charStep) * LetterSpacePercentage / 100); - if ((count + lStrWidth / 2) >= 0 && (count + lStrWidth) <= maxPoints) - { - count += lStrWidth; - var point2 = points[count]; - //PointF point = points[count - lStrWidth / 2]; - var point = new PointF((point2.X+point1.X)/2,(point2.Y+point1.Y)/2); - var angle = GetAngle(point1, point2); - DrawRotatedText(g, Text[charStep].ToString(CultureInfo.InvariantCulture), (float)angle, point); - point1 = points[count]; - } - else - { - count += lStrWidth; - } - charStep += 1; - } - - } - - private RectangleF StringRegionValue(Graphics g, int textpos) - { - - var measureString = Text.Substring(textpos, 1); - var numChars = measureString.Length; - var characterRanges = new CharacterRange[numChars + 1]; - var stringFormat = new StringFormat - { - Trimming = StringTrimming.None, - FormatFlags = - StringFormatFlags.NoClip | StringFormatFlags.NoWrap | - StringFormatFlags.LineLimit - }; - var size = g.MeasureString(Text, _font, LetterSpacePercentage); - var layoutRect = new RectangleF(0f, 0f, size.Width, size.Height); - characterRanges[0] = new CharacterRange(0, 1); - stringFormat.FormatFlags = StringFormatFlags.NoClip; - stringFormat.SetMeasurableCharacterRanges(characterRanges); - stringFormat.Alignment = StringAlignment.Center; - var stringRegions = g.MeasureCharacterRanges(Text.Substring(textpos), _font, layoutRect, stringFormat); - return stringRegions[0].GetBounds(g); - } - - private float StringRegion(Graphics g, int textpos) - { - return StringRegionValue(g, textpos).Width; - } - - /// - /// Method to compute the angle of a segment |, | compared to the horizontal line. - /// - /// The 1st point of the segment - /// The 2nd point of the segment - /// An angle in degrees - private static double GetAngle(PointF point1, PointF point2) - { - const double rad2Deg = 180d/Math.PI; - - var c = Math.Sqrt(Math.Pow((point2.X - point1.X), 2) + Math.Pow((point2.Y - point1.Y), 2)); - if (c == 0d) - { - return 0; - } - - var res = Math.Asin((point2.Y - point1.Y)/c)*rad2Deg; - return point1.X > point2.X - ? res - 180 - : res; - } - - /// - /// Method to draw , rotated by around . - /// - /// The object to use. - /// The text string - /// The rotation angle - /// The center point around which to rotate - private void DrawRotatedText(Graphics gr, string text, float angle, PointF pointCenter) - { - angle -= RotateDegree; - var stringFormat = new StringFormat { Alignment = StringAlignment.Center }; - - //gr.SmoothingMode = SmoothingMode.HighQuality; - //gr.CompositingQuality = CompositingQuality.HighQuality; - //gr.TextRenderingHint = TextRenderingHint.AntiAlias; - using (var graphicsPath = new GraphicsPath()) - { - var x = (int) pointCenter.X; - var y = (int) pointCenter.Y; - var pOrigin = new Point(); - switch (TextPathPathPosition) - { - case TextPathPosition.OverPath: - pOrigin = new Point(x, (int) (y - _font.Size)); - graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, - new Point(x, (int) (y - _font.Size)), stringFormat); - break; - case TextPathPosition.CenterPath: - pOrigin = new Point(x, (int) (y - _font.Size/2)); - graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, - new Point(x, (int) (y - _font.Size/2)), stringFormat); - break; - case TextPathPosition.UnderPath: - pOrigin = new Point(x, y); - graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, new Point(x, y), - stringFormat); - break; - } - - - var rotationMatrix = gr.Transform.Clone(); // new Matrix(); - rotationMatrix.RotateAt(angle, new PointF(x, y)); - graphicsPath.Transform(rotationMatrix); - if (!_measureString) - { - if (_colorHalo != null) - { - gr.DrawPath(_colorHalo, graphicsPath); - } - gr.FillPath(_fillBrush, graphicsPath); - - } - else - { - _regionList.Add(graphicsPath.GetBounds()); - _angles.Add(angle); - _pointText.Add(pointCenter); - _pointTextUp.Add(pOrigin); - } - } - } - - /// - /// Metod to get - /// - /// - /// - /// - /// - public PointF[] GetLinePoints(PointF p1, PointF p2, int stepWitdth) - { - - int lCount = 0; - var tmpPoints = new PointF[10001]; - long ix; - long iy; - int dd; - int id; - int lStep = stepWitdth; - - p1.X = (int)p1.X; - p1.Y = (int)p1.Y; - p2.X = (int)p2.X; - p2.Y = (int)p2.Y; - long width = (long)(p2.X - p1.X); - long height = (long)(p2.Y - p1.Y); - long d = 0; - - if (width < 0) - { - width = -width; - ix = -1; - } - else - { - ix = 1; - } - - if (height < 0) - { - height = -height; - iy = -1; - } - else - { - iy = 1; - } - - if (width > height) - { - dd = (int)(width + width); - id = (int)(height + height); - - do - { - if (lStep == stepWitdth) - { - tmpPoints[lCount].X = p1.X; - tmpPoints[lCount].Y = p1.Y; - lCount += 1; - } - else - { - lStep = lStep + stepWitdth; - } - if ((int)p1.X == (int)p2.X) break; - - p1.X = p1.X + ix; - d = d + id; - - if (d > width) - { - p1.Y = p1.Y + iy; - d = d - dd; - } - } - - while (true); - } - else - { - dd = (int)(height + height); - id = (int)(width + width); - - do - { - if (lStep == stepWitdth) - { - tmpPoints[lCount].X = p1.X; - tmpPoints[lCount].Y = p1.Y; - lCount += 1; - } - else - { - lStep = lStep + stepWitdth; - } - if ((int)p1.Y == (int)p2.Y) break; - - p1.Y = p1.Y + iy; - d = d + id; - - if (d > height) - { - p1.X = p1.X + ix; - d = d - dd; - } - } - while (true); - } - - var tmpPoints2 = new PointF[lCount]; - - Array.Copy(tmpPoints, tmpPoints2, lCount); - - return tmpPoints2; - } - } -} - +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Globalization; + +namespace SharpMap.Rendering +{ + /// + /// Horizontal alignment options for texts on path + /// + public enum TextPathAlign + { + /// + /// Aligned on the left + /// + Left = 0, + /// + /// Aligned in the middle + /// + Center = 1, + /// + /// Aligned on the right + /// + Right = 2 + } + + /// + /// Vertical alignment option for texts on path + /// + public enum TextPathPosition + { + /// + /// Above the path + /// + OverPath = 0, + /// + /// Center + /// + CenterPath = 1, + /// + /// Below the path + /// + UnderPath = 2 + } + + /// + /// Extensions methods for text on path label rendering + /// + public static class GraphicsExtension + { + /// + /// Method to measure the length of a string + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The describing the + /// An array of s + public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, GraphicsPath graphicsPath) + { + return MeasureString(graphics, s, font, brush, TextPathAlign.Left, TextPathPosition.CenterPath, 100, graphicsPath); + } + + /// + /// Method to measure the length of a string + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// The describing the + /// An array of s + public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, GraphicsPath graphicsPath) + { + return MeasureString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, graphicsPath); + } + + /// + /// Method to draw on the provided + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The describing the + public static void DrawString(Graphics graphics, string s, Font font, Brush brush, GraphicsPath graphicsPath) + { + DrawString(graphics, s, font, brush, TextPathAlign.Left, TextPathPosition.CenterPath, 100, graphicsPath); + } + + /// + /// Method to draw on the provided + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// The describing the + public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, GraphicsPath graphicsPath) + { + DrawString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, graphicsPath); + } + + /// + /// Method to measure the length of a string along a + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// A value controlling the spacing between letters + /// The describing the + /// An array of s + public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, GraphicsPath graphicsPath) + { + var angles = new List(); + var pointsText = new List(); + var pointsTextUp = new List(); + return MeasureString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, 0, graphicsPath, ref angles, ref pointsText, ref pointsTextUp); + } + + /// + /// Method to measure the length of a string along a + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// A value controlling the spacing between letters + /// The describing the + /// An array of s + public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, GraphicsPath graphicsPath) + { + DrawString(graphics, s, font, brush, textPathAlign, textPathPosition, 100, 0, graphicsPath,false); + } + + /// + /// Method to draw a string on a of a string + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// A value controlling the spacing between letters + /// A value controlling the rotation of + /// The along which to render. + /// A list of angle values (in degrees), one for each letter + /// A list of positions, one for each letter + /// A list of points (don't know what for) + public static RectangleF[] MeasureString(Graphics graphics, string s, Font font, Brush brush, + TextPathAlign textPathAlign, TextPathPosition textPathPosition, + int letterSpace, float rotateDegree, GraphicsPath graphicsPath, + ref List angles, ref List pointsText, + ref List pointsUp) + { + var top = TextOnPath.TextOnPathInstance; + top.Text = s; + top.Font = font; + top.FillColorTop = brush; + top.TextPathPathPosition = textPathPosition; + top.TextPathAlignTop = textPathAlign; + top.PathDataTop = graphicsPath.PathData; + top.LetterSpacePercentage = letterSpace; + top.Graphics = graphics; + top.GraphicsPath = graphicsPath; + top.MeasureString = true; + top.RotateDegree = rotateDegree; + top.DrawTextOnPath(); + angles = top.Angles; + pointsText = top.PointsText; + pointsUp = top.PointsTextUp; + return top.RegionList.ToArray(); + } + + /// + /// Method to draw a string on a of a string + /// + /// The object to use + /// The string to measure + /// The to use + /// The to use + /// The horizontal position on the + /// The vertical position on the + /// A value controlling the spacing between letters + /// A value controlling the rotation of + /// The describing the + /// A value indicating if the should be drawn, too. + public static void DrawString(Graphics graphics, string s, Font font, Brush brush, TextPathAlign textPathAlign, TextPathPosition textPathPosition, int letterSpace, float rotateDegree, GraphicsPath graphicsPath, bool showPath) + { + var top = TextOnPath.TextOnPathInstance; + + top.Text = s; + top.Font = font; + top.FillColorTop = brush; + top.TextPathPathPosition = textPathPosition; + top.TextPathAlignTop = textPathAlign; + top.PathDataTop = graphicsPath.PathData; + top.LetterSpacePercentage = letterSpace; + top.Graphics = graphics; + top.GraphicsPath = graphicsPath; + top.MeasureString = false; + top.RotateDegree = rotateDegree; + top.ShowPath = showPath; + top.DrawTextOnPath(); + + } + } + + /// + /// Text on path generator class + /// + public class TextOnPath + { + internal readonly static TextOnPath TextOnPathInstance = new TextOnPath(); + + private PathData _pathdata; + + private bool _measureString; + private Font _font; + private Pen _colorHalo = new Pen(Color.Black,1); + private Brush _fillBrush = new SolidBrush(Color.Black); + private TextPathAlign _pathalign = TextPathAlign.Center; + private double _letterspacepercentage = 1; + private TextPathPosition _textPathPathPosition = TextPathPosition.CenterPath; + + //ToDo, this is a result, intermediate? + private List _regionList = new List(); + private List _pointText = new List(); + private List _pointTextUp = new List(); + private readonly List _angles = new List(); + + /// + /// The last catched exception is stored here + /// + public Exception LastError; + + + /// + /// Gets or sets the object + /// + public Graphics Graphics { get; set; } + + /// + /// Gets or sets a value indicating the used to render text along + /// + public GraphicsPath GraphicsPath { get; set; } + + /// + /// Gets or sets a value indicating whether the string should be measured + /// + public bool MeasureString + { + get { return _measureString; } + set { _measureString = value; } + } + + + /// + /// Gets or sets a list of regions + /// + public List RegionList + { + get { return _regionList; } + set { _regionList = value; } + } + + /// + /// Gets or sets a list of s + /// + public List PointsText + { + get { return _pointText; } + set { _pointText = value; } + } + + /// + /// Gets or sets a list of s + /// + public List PointsTextUp + { + get { return _pointTextUp; } + set { _pointTextUp = value; } + } + + /// + /// Gets or sets a list of angles (in radians?) + /// + public List Angles + { + get { return _angles; } + } + + /// + /// Gets or sets a value indicating the rotation + /// + public float RotateDegree { get; set; } + + /// + /// Creates an instance of this class + /// + internal TextOnPath() + { + PathColorTop = Color.LightBlue; + } + + /// + /// Gets or sets a value indicating the vertical alignment + /// + public TextPathPosition TextPathPathPosition + { + get { return _textPathPathPosition; } + set { _textPathPathPosition = value; } + } + + /// + /// Gets or sets a value indicating the path's data + /// + public PathData PathDataTop + { + get { return _pathdata; } + set { _pathdata = value; } + } + + /// + /// Gets or sets a value indicating the text to render + /// + public string Text { get; set; } + + /// + /// Gets or sets the to use for drawing the text + /// + public Font Font + { + get { return _font; } + set { _font = value; } + } + + /// + /// Gets or sets the to use for halo'ing the text + /// + public Pen ColorHalo + { + get { return _colorHalo; } + set { _colorHalo = value; } + } + + /// + /// Gets or sets a value indicating the used to fill the text path + /// + public Brush FillColorTop + { + get { return _fillBrush; } + set { _fillBrush = value; } + } + + /// + /// Get or sets a value indicating the horizontal alignment of the path + /// + public TextPathAlign TextPathAlignTop + { + get { return _pathalign; } + set { _pathalign = value; } + } + + /// + /// Gets or sets a value indicating the color of the + /// + public Color PathColorTop { get; set; } + + + /// + /// Gets or sets a value controlling the space between letters + /// + /// The default value is 100 + public int LetterSpacePercentage + { + get { return (int)(100 * _letterspacepercentage); } + set { _letterspacepercentage = value/100d; } + } + + /// + /// Gets or sets a value indicating that the used path should be rendered as well + /// + public bool ShowPath { get; set; } + + /// + /// + /// + /// The path data + /// The text + /// The font + /// The halo pen + /// The brush to fill letters + /// The + public void DrawTextOnPath(PathData pathdata, string text, Font font, Pen haloPen, Brush fillcolor, int letterspacepercentage) + { + + _pathdata = pathdata; + Text = text; + _font = font; + _colorHalo= haloPen; + _fillBrush = fillcolor; + _letterspacepercentage = letterspacepercentage / 100d; + + DrawTextOnPath(); + } + + /// + /// Method to draw the text on the + /// + public void DrawTextOnPath() + { + DrawTextOnPathEx(); + } + + public RectangleF DrawTextOnPathEx() + { + var affectedArea = new RectangleF(); + + var points = new PointF[25001]; + var count = 0; + var gp = new GraphicsPath(_pathdata.Points, _pathdata.Types) { FillMode = FillMode.Winding }; + _regionList.Clear(); + _angles.Clear(); + _pointText.Clear(); + _pointTextUp.Clear(); + gp.Flatten(null, 1); + try + { + var tmpPoint = gp.PathPoints[0]; + int i; + PointF[] tmpPoints; + for (i = 0; i <= gp.PathPoints.Length - 2; i++) + { + if (gp.PathTypes[i + 1] == (byte)PathPointType.Start | (gp.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) + { + tmpPoints = GetLinePoints(gp.PathPoints[i], tmpPoint, 1); + Array.ConstrainedCopy(tmpPoints, 0, points, count, tmpPoints.Length); + count += 1; + tmpPoint = gp.PathPoints[i + 1]; + } + else + { + tmpPoints = GetLinePoints(gp.PathPoints[i], gp.PathPoints[i + 1], 1); + Array.ConstrainedCopy(tmpPoints, 0, points, count, tmpPoints.Length); + count += tmpPoints.Length - 1; + + } + } + tmpPoints = new PointF[count]; + Array.Copy(points, tmpPoints, count); + points = CleanPoints(tmpPoints); + + count = points.Length - 1; + if (IsVisible(points, count)) + { + // if can show all letter + DrawText(Graphics, points, count); + } + + affectedArea = gp.GetBounds(); + gp.Dispose(); + //DrawText(points, count); + } + catch (Exception ex) + { + LastError = ex; + } + + return affectedArea; + } + /// + /// Method to remove consecutive same/equal s from the array + /// + /// An array of s + /// An array of s without repeated values. + private static PointF[] CleanPoints(PointF[] points) + { + if (points == null) + return null; + if (points.Length == 0) + return new PointF[0]; + var tmpPoints = new List(points.Length); + tmpPoints.Add(points[0]); + var lastIndex = 0; + for (var i = 1; i <= points.Length - 1; i++) + { + if (!tmpPoints[lastIndex].Equals(points[i])) + { + tmpPoints.Add(points[i]); + lastIndex++; + } + } + + return tmpPoints.ToArray(); + } + + /// + /// Method to evaluate visibility + /// + /// An array of s + /// The maximum number of points + /// + public bool IsVisible(PointF[] points, int maxPoints) + { + var result = true; + var g = Graphics; + var count = 0; + var maxWidthText = default(double); + int i; + for (i = 0; i <= Text.Length - 1; i++) + { + maxWidthText += StringRegion(g, i) * LetterSpacePercentage / 100; + } + switch (_pathalign) + { + case TextPathAlign.Left: + count = 0; + break; + case TextPathAlign.Center: + count = (int)((maxPoints - maxWidthText) / 2); + break; + case TextPathAlign.Right: + count = (int)(maxPoints - maxWidthText - (double)StringRegion(g, Text.Length - 1) * LetterSpacePercentage / 100); + break; + } + var lStrWidth = (int)(StringRegion(g, 0) * LetterSpacePercentage / 100); + if ((count + lStrWidth / 2) < 0) + { + count = -(lStrWidth / 2); + } + double currentWidthText = 0; + for (int j =count+lStrWidth/2; j <= Text.Length - 1; j++) + { + currentWidthText += StringRegion(g, j) * LetterSpacePercentage / 100; + } + if ((int)currentWidthText >= maxPoints) + { + result = false; + } + return result; + + } + + private void DrawText(Graphics g, PointF[] points, int maxPoints) + { + + //GraphicsPath gp = new GraphicsPath(_pathdata.Points, _pathdata.Types) { FillMode = FillMode.Winding }; + //gp.Flatten(); + //gp.Dispose(); + //var g = _graphics; + //GraphicsContainer graphicsContainer= g.BeginContainer(); + //g.TranslateTransform(_graphicsPath.GetBounds().X, _graphicsPath.GetBounds().Y); + if (ShowPath) + { + var pen = new Pen(PathColorTop); + foreach (var p in points) + { + g.DrawEllipse(pen, p.X, p.Y, 1, 1); + } + pen.Dispose(); + } + var count = 0; + var point1 = default(PointF); + var charStep = 0; + var maxWidthText = default(double); + int i; + + for (i = 0; i <= Text.Length - 1; i++) + { + maxWidthText += StringRegion(g, i) * LetterSpacePercentage / 100; + } + + switch (_pathalign) + { + case TextPathAlign.Left: + point1 = points[0]; + count = 0; + break; + case TextPathAlign.Center: + count = (int)((maxPoints - maxWidthText) / 2); + point1 = count > 0 ? points[count] : points[0]; + + break; + case TextPathAlign.Right: + count = (int)(maxPoints - maxWidthText - (double)StringRegion(g, Text.Length - 1) * LetterSpacePercentage / 100); + point1 = count > 0 ? points[count] : points[0]; + + break; + } + var lStrWidth = (int)(StringRegion(g, charStep) * LetterSpacePercentage / 100); + if ((count + lStrWidth / 2) < 0) + { + count = -(lStrWidth / 2); + } + while (!(charStep > Text.Length - 1)) + { + lStrWidth = (int)(StringRegion(g, charStep) * LetterSpacePercentage / 100); + if ((count + lStrWidth / 2) >= 0 && (count + lStrWidth) <= maxPoints) + { + count += lStrWidth; + var point2 = points[count]; + //PointF point = points[count - lStrWidth / 2]; + var point = new PointF((point2.X+point1.X)/2,(point2.Y+point1.Y)/2); + var angle = GetAngle(point1, point2); + DrawRotatedText(g, Text[charStep].ToString(CultureInfo.InvariantCulture), (float)angle, point); + point1 = points[count]; + } + else + { + count += lStrWidth; + } + charStep += 1; + } + + } + + private RectangleF StringRegionValue(Graphics g, int textpos) + { + + var measureString = Text.Substring(textpos, 1); + var numChars = measureString.Length; + var characterRanges = new CharacterRange[numChars + 1]; + var stringFormat = new StringFormat + { + Trimming = StringTrimming.None, + FormatFlags = + StringFormatFlags.NoClip | StringFormatFlags.NoWrap | + StringFormatFlags.LineLimit + }; + var size = g.MeasureString(Text, _font, LetterSpacePercentage); + var layoutRect = new RectangleF(0f, 0f, size.Width, size.Height); + characterRanges[0] = new CharacterRange(0, 1); + stringFormat.FormatFlags = StringFormatFlags.NoClip; + stringFormat.SetMeasurableCharacterRanges(characterRanges); + stringFormat.Alignment = StringAlignment.Center; + var stringRegions = g.MeasureCharacterRanges(Text.Substring(textpos), _font, layoutRect, stringFormat); + return stringRegions[0].GetBounds(g); + } + + private float StringRegion(Graphics g, int textpos) + { + return StringRegionValue(g, textpos).Width; + } + + /// + /// Method to compute the angle of a segment |, | compared to the horizontal line. + /// + /// The 1st point of the segment + /// The 2nd point of the segment + /// An angle in degrees + private static double GetAngle(PointF point1, PointF point2) + { + const double rad2Deg = 180d/Math.PI; + + var c = Math.Sqrt(Math.Pow((point2.X - point1.X), 2) + Math.Pow((point2.Y - point1.Y), 2)); + if (c == 0d) + { + return 0; + } + + var res = Math.Asin((point2.Y - point1.Y)/c)*rad2Deg; + return point1.X > point2.X + ? res - 180 + : res; + } + + /// + /// Method to draw , rotated by around . + /// + /// The object to use. + /// The text string + /// The rotation angle + /// The center point around which to rotate + private void DrawRotatedText(Graphics gr, string text, float angle, PointF pointCenter) + { + angle -= RotateDegree; + var stringFormat = new StringFormat { Alignment = StringAlignment.Center }; + + //gr.SmoothingMode = SmoothingMode.HighQuality; + //gr.CompositingQuality = CompositingQuality.HighQuality; + //gr.TextRenderingHint = TextRenderingHint.AntiAlias; + using (var graphicsPath = new GraphicsPath()) + { + var x = (int) pointCenter.X; + var y = (int) pointCenter.Y; + var pOrigin = new Point(); + switch (TextPathPathPosition) + { + case TextPathPosition.OverPath: + pOrigin = new Point(x, (int) (y - _font.Size)); + graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, + new Point(x, (int) (y - _font.Size)), stringFormat); + break; + case TextPathPosition.CenterPath: + pOrigin = new Point(x, (int) (y - _font.Size/2)); + graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, + new Point(x, (int) (y - _font.Size/2)), stringFormat); + break; + case TextPathPosition.UnderPath: + pOrigin = new Point(x, y); + graphicsPath.AddString(text, _font.FontFamily, (int) _font.Style, _font.Size, new Point(x, y), + stringFormat); + break; + } + + + var rotationMatrix = gr.Transform.Clone(); // new Matrix(); + rotationMatrix.RotateAt(angle, new PointF(x, y)); + graphicsPath.Transform(rotationMatrix); + if (!_measureString) + { + if (_colorHalo != null) + { + gr.DrawPath(_colorHalo, graphicsPath); + } + gr.FillPath(_fillBrush, graphicsPath); + + } + else + { + _regionList.Add(graphicsPath.GetBounds()); + _angles.Add(angle); + _pointText.Add(pointCenter); + _pointTextUp.Add(pOrigin); + } + } + } + + /// + /// Metod to get + /// + /// + /// + /// + /// + public PointF[] GetLinePoints(PointF p1, PointF p2, int stepWitdth) + { + + int lCount = 0; + var tmpPoints = new PointF[10001]; + long ix; + long iy; + int dd; + int id; + int lStep = stepWitdth; + + p1.X = (int)p1.X; + p1.Y = (int)p1.Y; + p2.X = (int)p2.X; + p2.Y = (int)p2.Y; + long width = (long)(p2.X - p1.X); + long height = (long)(p2.Y - p1.Y); + long d = 0; + + if (width < 0) + { + width = -width; + ix = -1; + } + else + { + ix = 1; + } + + if (height < 0) + { + height = -height; + iy = -1; + } + else + { + iy = 1; + } + + if (width > height) + { + dd = (int)(width + width); + id = (int)(height + height); + + do + { + if (lStep == stepWitdth) + { + tmpPoints[lCount].X = p1.X; + tmpPoints[lCount].Y = p1.Y; + lCount += 1; + } + else + { + lStep = lStep + stepWitdth; + } + if ((int)p1.X == (int)p2.X) break; + + p1.X = p1.X + ix; + d = d + id; + + if (d > width) + { + p1.Y = p1.Y + iy; + d = d - dd; + } + } + + while (true); + } + else + { + dd = (int)(height + height); + id = (int)(width + width); + + do + { + if (lStep == stepWitdth) + { + tmpPoints[lCount].X = p1.X; + tmpPoints[lCount].Y = p1.Y; + lCount += 1; + } + else + { + lStep = lStep + stepWitdth; + } + if ((int)p1.Y == (int)p2.Y) break; + + p1.Y = p1.Y + iy; + d = d + id; + + if (d > height) + { + p1.X = p1.X + ix; + d = d - dd; + } + } + while (true); + } + + var tmpPoints2 = new PointF[lCount]; + + Array.Copy(tmpPoints, tmpPoints2, lCount); + + return tmpPoints2; + } + } +} + diff --git a/SharpMap/Rendering/VectorRenderer.cs b/SharpMap/Rendering/VectorRenderer.cs index 09258c65..e06a6d21 100644 --- a/SharpMap/Rendering/VectorRenderer.cs +++ b/SharpMap/Rendering/VectorRenderer.cs @@ -349,14 +349,14 @@ public static RectangleF DrawLabelEx(Graphics g, PointF labelPoint, PointF offse { rotationPoint = rotationPoint ?? labelPoint; - g.FillEllipse(Brushes.LawnGreen, rotationPoint.Value.X - 1, rotationPoint.Value.Y - 1, 2, 2); - + //g.FillEllipse(Brushes.LawnGreen, rotationPoint.Value.X - 1, rotationPoint.Value.Y - 1, 2, 2); + RectangleF bounds; using (var t = g.Transform.Clone()) { g.TranslateTransform(rotationPoint.Value.X, rotationPoint.Value.Y); g.RotateTransform(rotation); //g.TranslateTransform(-labelSize.Width/2, -labelSize.Height/2); - + labelPoint = new PointF(labelPoint.X - rotationPoint.Value.X, labelPoint.Y - rotationPoint.Value.Y); @@ -372,11 +372,19 @@ public static RectangleF DrawLabelEx(Graphics g, PointF labelPoint, PointF offse g.DrawPath(halo, path); g.FillPath(new SolidBrush(forecolor), path); + //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), 0, 0); - g.Transform = t; - return new RectangleF(); + using (var inv = new Matrix()) + { + inv.Translate(rotationPoint.Value.X, rotationPoint.Value.Y); + inv.Rotate(rotation); + bounds = path.GetBounds(inv); + } + + g.Transform = t; }; + return bounds; } else { @@ -392,9 +400,8 @@ public static RectangleF DrawLabelEx(Graphics g, PointF labelPoint, PointF offse g.DrawPath(halo, path); g.FillPath(new SolidBrush(forecolor), path); //g.DrawString(text, font, new System.Drawing.SolidBrush(forecolor), LabelPoint.X, LabelPoint.Y); - - //return path.GetBounds(); - return new RectangleF(labelPoint.X, labelPoint.Y, labelSize.Width,labelSize.Height); + + return path.GetBounds(); } } From f3390d5216653db0818385941a178edb3c64c269 Mon Sep 17 00:00:00 2001 From: Tim C Date: Sun, 26 Jan 2020 21:56:45 +1300 Subject: [PATCH 28/51] Add UnitTest for LabelLayer affectedArea --- UnitTests/Layers/Layer_AffectedAreaTest.cs | 243 +++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 UnitTests/Layers/Layer_AffectedAreaTest.cs diff --git a/UnitTests/Layers/Layer_AffectedAreaTest.cs b/UnitTests/Layers/Layer_AffectedAreaTest.cs new file mode 100644 index 00000000..87c3e861 --- /dev/null +++ b/UnitTests/Layers/Layer_AffectedAreaTest.cs @@ -0,0 +1,243 @@ +using System.Data; +using System.Drawing; +using System.IO; +using System.Text; +using GeoAPI.Geometries; +using NetTopologySuite.Geometries; +using NUnit.Framework; +using SharpMap; +using SharpMap.Data; +using SharpMap.Data.Providers; +using SharpMap.Layers; +using SharpMap.Rendering.Decoration; +using LabelStyle = SharpMap.Styles.LabelStyle; + +namespace UnitTests.Layers +{ + [TestFixture, Category("RequiresWindows")] + public class Layer_AffectedAreaTest + { + public enum LabelLayerMode + { + BasicLabel, + BasicLabelRot, + TextOnPath, + PathOnLabel + } + + private readonly float[] _rotations = new float[] {0f, 30f, 60f, 90f, 120f, 150f, 180f, 210f, 240f, 270f, 310f, 330f}; + + /// + /// Validate calculated affectedArea on 3 primary code paths of LabelLayer + /// + /// + /// + [NUnit.Framework.TestCase(LabelLayerMode.BasicLabel, true)] + [NUnit.Framework.TestCase(LabelLayerMode.BasicLabelRot, true)] + [NUnit.Framework.TestCase(LabelLayerMode.TextOnPath, true)] + [NUnit.Framework.TestCase(LabelLayerMode.PathOnLabel, true)] + public void LabelLayer_AffectedArea(LabelLayerMode mode, bool testRotations) + { + using (var map = new Map()) + { + ConfigureMap(map); + + Envelope extents = null; + switch (mode) + { + case LabelLayerMode.BasicLabel: + case LabelLayerMode.BasicLabelRot: + AddBasicLabelLayers(map, mode); + extents = map.GetExtents(); + extents.ExpandBy(0.125); + break; + case LabelLayerMode.TextOnPath: + AddTextOnPathLayers(map); + extents = map.GetExtents(); + break; + case LabelLayerMode.PathOnLabel: + AddPathOnLabelLayers(map); + extents = map.GetExtents(); + extents.ExpandBy(0.125); + break; + } + + foreach (var rot in _rotations) + { + SetMapTransform(map, rot); + map.ZoomToBox(extents, true); + + var affectedArea = GetAffectedArea(map, (Layer)map.Layers[1]); + AddAffectedAreaLayer(map, affectedArea); + + using (var img = map.GetMap()) + img.Save(Path.Combine(UnitTestsFixture.GetImageDirectory(this), $"LabelLayer_{mode.ToString()}_{rot:000}.png"), + System.Drawing.Imaging.ImageFormat.Png); + + // remove affected area layer + map.Layers.RemoveAt(2); + if (!testRotations) break; + } + } + } + private void ConfigureMap(Map map) + { + map.Size = new Size(800, 640); + map.BackColor = Color.AliceBlue; + map.SRID = 4326; + map.Decorations.Add(new NorthArrow()); + map.Decorations.Add(new Disclaimer() + { + Text = "Affected Area envelope should surround label" + }); + } + private void SetMapTransform(Map map, float rotationDeg) + { + if (rotationDeg.Equals(0f)) + { + map.MapTransform = null; + } + else + { + var matrix = new System.Drawing.Drawing2D.Matrix(); + matrix.RotateAt(rotationDeg, new PointF(map.Size.Width * 0.5f, map.Size.Height * 0.5f)); + map.MapTransform = matrix; + } + } + private Envelope GetAffectedArea(Map map, Layer layer) + { + using (var img = new Bitmap(map.Size.Width, map.Size.Height)) + using (var g = Graphics.FromImage(img)) + { + layer.Render(g, (MapViewport) map, out var affectedArea); + return affectedArea; + } + } + private void AddAffectedAreaLayer(Map map, Envelope affectedArea) + { + var coords = new Coordinate[] + { + new Coordinate(affectedArea.TopLeft()), + new Coordinate(affectedArea.TopRight()), + new Coordinate(affectedArea.BottomRight()), + new Coordinate(affectedArea.BottomLeft()), + new Coordinate(affectedArea.TopLeft()) + }; + + var polygon = new Polygon(new LinearRing(coords)); + var gp = new GeometryProvider(polygon); + var vLayer = new VectorLayer("Affected Area") + { + DataSource = gp, + SRID = map.SRID + }; + vLayer.Style.Fill = null; + vLayer.Style.EnableOutline = true; + + map.Layers.Add(vLayer); + } + private void AddBasicLabelLayers(Map map, LabelLayerMode mode) + { + var fdt = new FeatureDataTable(); + fdt.Columns.Add(new DataColumn("ID", typeof(int))); + fdt.Columns.Add(new DataColumn("LABEL", typeof(string))); + + var factory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(4236); + var fdr = fdt.NewRow(); + fdr[0] = 1; + fdr[1] = "Test Label"; + fdr.Geometry = factory.CreatePoint(new Coordinate(99, 13)); + fdt.AddRow(fdr); + + var vLyr = new VectorLayer("Basic Point", new GeometryFeatureProvider(fdt)); + map.Layers.Add(vLyr); + + var labLyr = new LabelLayer("Basic Point Labels") + { + DataSource = vLyr.DataSource, + Enabled = true, + LabelColumn = "LABEL", + Style = new LabelStyle + { + Offset = new PointF(20, -20), + HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Left, + VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Bottom + } + }; + + if (mode == LabelLayerMode.BasicLabelRot) + labLyr.Style.Rotation = 315f; + + map.Layers.Add(labLyr); + } + private void AddTextOnPathLayers(Map map) + { + string shapefile = System.IO.Path.GetFullPath( + TestContext.CurrentContext.TestDirectory + + @"\..\..\..\..\Examples\WinFormSamples\GeoData\World/shp_textonpath/DeMo_Quan5.shp"); + + // TextOnPath (adopted from WinformSamples) + var vLyr = new VectorLayer("TextOnPath"); + vLyr.DataSource = new ShapeFile(shapefile); + (vLyr.DataSource as ShapeFile).Encoding = Encoding.UTF8; + vLyr.Style.Fill = new SolidBrush(Color.Yellow); + vLyr.Style.Line = new Pen(Color.Yellow, 4); + vLyr.Style.Outline = new Pen(Color.Black, 5); + ; + vLyr.Style.EnableOutline = true; + vLyr.SRID = map.SRID; + (vLyr.DataSource as ShapeFile).FilterDelegate = TextOnPathFilter; + map.Layers.Add(vLyr); + + var labLyr = new LabelLayer("TextOnPath labels"); + labLyr.DataSource = vLyr.DataSource; + labLyr.Enabled = true; + labLyr.LabelColumn = "tenduong"; + labLyr.LabelFilter = SharpMap.Rendering.LabelCollisionDetection.ThoroughCollisionDetection; + labLyr.Style = new LabelStyle(); + labLyr.Style.ForeColor = Color.White; + labLyr.Style.Font = new Font(FontFamily.GenericSerif, 9f, FontStyle.Bold); + labLyr.Style.Halo = new Pen(Color.Black, 2f); + labLyr.Style.IsTextOnPath = true; + labLyr.Style.CollisionDetection = false; + labLyr.Style.HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center; + labLyr.Style.VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Top; + labLyr.SRID = 4326; + map.Layers.Add(labLyr); + } + public static bool TextOnPathFilter(FeatureDataRow fdr) + { + return fdr[1].ToString() == "Trần Phú"; + } + private void AddPathOnLabelLayers(Map map) + { + var fdt = new FeatureDataTable(); + fdt.Columns.Add(new DataColumn("ID", typeof(int))); + fdt.Columns.Add(new DataColumn("LABEL", typeof(string))); + + var factory = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory(4236); + var fdr = fdt.NewRow(); + fdr[0] = 1; + fdr[1] = "Test Label"; + fdr.Geometry = factory.CreateLineString(new[] + { + new Coordinate(98.5, 12.5), + new Coordinate(99.5, 13.5) + }); + fdt.AddRow(fdr); + + var vLyr = new VectorLayer("Basic Line", new GeometryFeatureProvider(fdt)); + vLyr.Style.Line = new Pen(Color.DodgerBlue, 2f); + map.Layers.Add(vLyr); + + var labLyr = new LabelLayer("Basic Line Labels") + { + DataSource = vLyr.DataSource, + Enabled = true, + LabelColumn = "LABEL", + }; + labLyr.Style.IsTextOnPath = false; + map.Layers.Add(labLyr); + } + } +} From 30f3de3e79385bf5be10a54125925a999d52cd29 Mon Sep 17 00:00:00 2001 From: Tim C Date: Mon, 10 Feb 2020 21:58:14 +1300 Subject: [PATCH 29/51] Render PathLabel using LenghtIndexedLine (replaces TextOnPath) --- SharpMap/Layers/LabelLayer.cs | 806 +++++++--------- SharpMap/Rendering/Label.cs | 913 +++++++++--------- .../Rendering/Symbolizer/WarpPathToPath.cs | 42 +- SharpMap/Rendering/TextOnPath.cs | 4 + SharpMap/Styles/LabelStyle.cs | 745 +++++++------- UnitTests/Layers/Layer_AffectedAreaTest.cs | 242 ++++- 6 files changed, 1417 insertions(+), 1335 deletions(-) diff --git a/SharpMap/Layers/LabelLayer.cs b/SharpMap/Layers/LabelLayer.cs index 9595cca3..9108832e 100644 --- a/SharpMap/Layers/LabelLayer.cs +++ b/SharpMap/Layers/LabelLayer.cs @@ -17,7 +17,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Text; @@ -25,12 +24,15 @@ using SharpMap.Data; using SharpMap.Data.Providers; using GeoAPI.Geometries; +using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Utilities; using SharpMap.Rendering; using SharpMap.Rendering.Thematics; using SharpMap.Styles; -using Transform = SharpMap.Utilities.Transform; using SharpMap.Rendering.Symbolizer; + namespace SharpMap.Layers { /// @@ -410,36 +412,100 @@ public override void Render(Graphics g, MapViewport map) { if (DataSource == null) throw (new ApplicationException("DataSource property not set on layer '" + LayerName + "'")); - g.TextRenderingHint = TextRenderingHint; - g.SmoothingMode = SmoothingMode; - - var mapEnvelope = map.Envelope; - var layerEnvelope = ToSource(mapEnvelope); //View to render - var lineClipping = new CohenSutherlandLineClipping(mapEnvelope.MinX, mapEnvelope.MinY, - mapEnvelope.MaxX, mapEnvelope.MaxY); - - var ds = new FeatureDataSet(); - DataSource.Open(); - DataSource.ExecuteIntersectionQuery(layerEnvelope, ds); - DataSource.Close(); - if (ds.Tables.Count == 0) + + var layerEnvelope = ToSource(map.Envelope); //View to render + List labels = null; + + using (var ds = new FeatureDataSet()) + { + DataSource.Open(); + DataSource.ExecuteIntersectionQuery(layerEnvelope, ds); + DataSource.Close(); + if (ds.Tables.Count > 0) + { + g.TextRenderingHint = TextRenderingHint; + g.SmoothingMode = SmoothingMode; + + labels = CreateLabelDefinitions(g, map, ds.Tables[0]); + } + } + + if (labels == null || labels.Count == 0) { base.Render(g, map); return; } - var features = ds.Tables[0]; + if (Style.CollisionDetection) + _labelFilter?.Invoke(labels); - //Initialize label collection - var labels = new List(); + var affectedAreaGraphics = new RectangleF(); + var affectedAreaWorld = new Envelope(); + + for (int i = 0; i < labels.Count; i++) + { + var baseLabel = labels[i]; + if (!baseLabel.Show) + continue; - //List LabelBoxes; //Used for collision detection - //Render labels + var font = baseLabel.Style.GetFontForGraphics(g); + + if (labels[i] is Label) + { + var label = baseLabel as Label; + affectedAreaGraphics = VectorRenderer.DrawLabel( + g, label.Location, label.Style.Offset, + font, label.Style.ForeColor, label.Style.BackColor, + label.Style.Halo, label.Rotation, label.Text, map, + label.Style.HorizontalAlignment, label.LabelPoint); + + affectedAreaGraphics = VectorRenderer.RectExpandToInclude(affectedAreaGraphics, affectedAreaGraphics); + } + else if (labels[i] is PathLabel) + { + var pathLabel = labels[i] as PathLabel; + var lblStyle = pathLabel.Style; + + if (lblStyle.BackColor != null && lblStyle.BackColor != Brushes.Transparent) + using (var gp = new GraphicsPath()) + { + var pts = pathLabel.AffectedArea.ExteriorRing.TransformToImage(map); + gp.AddPolygon(pts); + g.FillPath(lblStyle.BackColor, gp); + } + + g.DrawString(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), + pathLabel.Text, font.FontFamily, (int) font.Style, font.Size, + lblStyle.GetStringFormat(), lblStyle.IgnoreLength, pathLabel.Location, pathLabel.Box.Height); + + affectedAreaWorld.ExpandToInclude(pathLabel.AffectedArea.EnvelopeInternal); + } + } + + if (!affectedAreaGraphics.IsEmpty) + { + var pts = new[] + { + new PointF(affectedAreaGraphics.Left - 1, affectedAreaGraphics.Top - 1), + new PointF(affectedAreaGraphics.Right + 1, affectedAreaGraphics.Bottom + 1), + }; + var coords = map.ImageToWorld(pts, false); + _affectedArea.ExpandToInclude(new Envelope(coords[0], coords[1])); + } + + _affectedArea.ExpandToInclude(affectedAreaWorld); + + base.Render(g, map); + } + + private List CreateLabelDefinitions(Graphics g, MapViewport map, FeatureDataTable features) + { + var labels = new List(); + var factory = new GeometryFactory(); for (int i = 0; i < features.Count; i++) { var feature = features[i]; - feature.Geometry = ToTarget(feature.Geometry); LabelStyle style; if (Theme != null) //If thematics is enabled, lets override the style @@ -474,30 +540,38 @@ public override void Render(Graphics g, MapViewport map) text = feature[LabelColumn].ToString(); if (String.IsNullOrEmpty(text)) continue; - - // for lineal geometries, try clipping to ensure proper labeling + + // Geometry + feature.Geometry = ToTarget(feature.Geometry); + + // for lineal geometries, clip to ensure proper labeling if (feature.Geometry is ILineal) + feature.Geometry = ClipLinealGeomToViewExtents(map, feature.Geometry); + + if (feature.Geometry is IPolygonal) { - if (feature.Geometry is ILineString) - feature.Geometry = lineClipping.ClipLineString(feature.Geometry as ILineString); - else if (feature.Geometry is IMultiLineString) - feature.Geometry = lineClipping.ClipLineString(feature.Geometry as IMultiLineString); + // TO CONSIDER clip to ViewExtents? + // This will ensure that polygons only partly in view will be labelled + // but perhaps need complexity threshold (eg Pts < 500) so as not to impact rendering + // or new prop bool PartialPolygonalLabel } - + if (feature.Geometry is IGeometryCollection) { if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.All) { - foreach (var geom in (feature.Geometry as IGeometryCollection)) + var geoms = feature.Geometry as IGeometryCollection; + for (int j = 0; j < geoms.Count; j++) { - BaseLabel lbl = CreateLabel(feature, geom, text, rotation, priority, style, map, g, _getLocationMethod); + BaseLabel lbl = CreateLabelDefinition(feature, geoms.GetGeometryN(j), text, rotation, + priority, style, map, g, _getLocationMethod, factory); if (lbl != null) labels.Add(lbl); } } else if (MultipartGeometryBehaviour == MultipartGeometryBehaviourEnum.CommonCenter) { - BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); + BaseLabel lbl = CreateLabelDefinition(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod, factory); if (lbl != null) labels.Add(lbl); } @@ -505,8 +579,8 @@ public override void Render(Graphics g, MapViewport map) { if ((feature.Geometry as IGeometryCollection).NumGeometries > 0) { - BaseLabel lbl = CreateLabel(feature, (feature.Geometry as IGeometryCollection).GetGeometryN(0), text, - rotation, style, map, g); + BaseLabel lbl = CreateLabelDefinition(feature, (feature.Geometry as IGeometryCollection).GetGeometryN(0), text, + rotation, priority, style, map, g, _getLocationMethod, factory); if (lbl != null) labels.Add(lbl); } @@ -543,8 +617,8 @@ public override void Render(Graphics g, MapViewport map) } } - BaseLabel lbl = CreateLabel(feature, coll.GetGeometryN(idxOfLargest), text, rotation, priority, style, - map, g, _getLocationMethod); + BaseLabel lbl = CreateLabelDefinition(feature, coll.GetGeometryN(idxOfLargest), text, rotation, priority, style, + map, g, _getLocationMethod, factory); if (lbl != null) labels.Add(lbl); } @@ -552,137 +626,31 @@ public override void Render(Graphics g, MapViewport map) } else { - BaseLabel lbl = CreateLabel(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod); + BaseLabel lbl = CreateLabelDefinition(feature, feature.Geometry, text, rotation, priority, style, map, g, _getLocationMethod, factory); if (lbl != null) labels.Add(lbl); } } - if (labels.Count == 0) - return; - - if (Style.CollisionDetection && _labelFilter != null) - _labelFilter(labels); - - RectangleF affectedArea = new RectangleF(); - - for (int i = 0; i < labels.Count; i++) - { - // Don't show the label if not necessary - if (!labels[i].Show) - { - continue; - } - - RectangleF rect; - - if (labels[i] is Label) - { - var label = labels[i] as Label; - if (label.Style.IsTextOnPath == false || label.TextOnPathLabel == null) - { - rect = VectorRenderer.DrawLabelEx(g, label.Location, label.Style.Offset, - label.Style.GetFontForGraphics(g), label.Style.ForeColor, - label.Style.BackColor, label.Style.Halo, label.Rotation, - label.Text, map, label.Style.HorizontalAlignment, - label.LabelPoint); - } - else - { - if (label.Style.BackColor != null && - label.Style.BackColor != System.Drawing.Brushes.Transparent) - { - //draw background - if (label.TextOnPathLabel.RegionList.Count > 0) - { - g.FillRectangles(labels[i].Style.BackColor, - labels[i].TextOnPathLabel.RegionList.ToArray()); - //g.FillPolygon(labels[i].Style.BackColor, labels[i].TextOnPathLabel.PointsText.ToArray()); - } - } - rect = label.TextOnPathLabel.DrawTextOnPathEx(); - } - } - else if (labels[i] is PathLabel) - { - var plbl = labels[i] as PathLabel; - var lblStyle = plbl.Style; - rect = g.DrawStringEx(lblStyle.Halo, new SolidBrush(lblStyle.ForeColor), plbl.Text, - lblStyle.Font.FontFamily, (int) lblStyle.Font.Style, lblStyle.Font.Size, - lblStyle.GetStringFormat(), lblStyle.IgnoreLength, plbl.Location); - } - affectedArea = VectorRenderer.RectExpandToInclude(affectedArea, rect); - } - - if (!affectedArea.IsEmpty) - { - var pts = new PointF[] - { - new PointF(affectedArea.Left - 1, affectedArea.Top - 1), - new PointF(affectedArea.Right + 1, affectedArea.Bottom + 1), - }; - var coords = map.ImageToWorld(pts, false); - base._affectedArea.ExpandToInclude(new Envelope(coords[0], coords[1])); - } - base.Render(g, map); - } - - - private BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, LabelStyle style, MapViewport map, Graphics g) - { - return CreateLabel(fdr, feature, text, rotation, Priority, style, map, g, _getLocationMethod); + return labels; } - - private static BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, string text, float rotation, int priority, LabelStyle style, MapViewport map, Graphics g, GetLocationMethod _getLocationMethod) + private static BaseLabel CreateLabelDefinition( FeatureDataRow fdr, IGeometry geom, string text, float rotation, + int priority, LabelStyle style, MapViewport map, Graphics g, GetLocationMethod _getLocationMethod, + GeometryFactory factory) { - if (feature == null) return null; + if (geom == null) + return null; BaseLabel lbl = null; var font = style.GetFontForGraphics(g); SizeF size = VectorRenderer.SizeOfString(g, text, font); - if (feature is ILineal) - { - var line = feature as ILineString; - if (line != null) - { - if (style.IsTextOnPath == false) - { - if (size.Width < 0.95 * line.Length / map.PixelWidth || style.IgnoreLength) - { - var positiveLineString = PositiveLineString(line, false); - var lineStringPath = LineStringToPath(positiveLineString, map /*, false*/); - var rect = lineStringPath.GetBounds(); - - if (style.CollisionDetection && !style.CollisionBuffer.IsEmpty) - { - var cbx = style.CollisionBuffer.Width; - var cby = style.CollisionBuffer.Height; - rect.Inflate(2 * cbx, 2 * cby); - rect.Offset(-cbx, -cby); - } - var labelBox = new LabelBox(rect); - - lbl = new PathLabel(text, lineStringPath, 0, priority, labelBox, style); - } - } - else - { - //get centriod - System.Drawing.PointF position2 = map.WorldToImage(feature.EnvelopeInternal.Centre); - lbl = new Label(text, position2, rotation, priority, style); - if (size.Width < 0.95 * line.Length / map.PixelWidth || !style.IgnoreLength) - { - CalculateLabelAroundOnLineString(line, ref lbl, map, g, size); - } - } - } - return lbl; - } + if (geom is ILineal) + return CreatePathLabel((ILineString) geom,text, size, priority, style, map, g, factory); var worldPosition = _getLocationMethod == null - ? feature.EnvelopeInternal.Centre + ? geom.EnvelopeInternal.Centre : _getLocationMethod(fdr); if (worldPosition == null) return null; @@ -711,348 +679,282 @@ private static BaseLabel CreateLabel(FeatureDataRow fdr, IGeometry feature, stri { LabelPoint = position }; } - /* - if (feature is LineString) - { - var line = feature as LineString; - - //Only label feature if it is long enough, or it is definately wanted - if (line.Length / map.PixelSize > size.Width || style.IgnoreLength) - { - CalculateLabelOnLinestring(line, ref lbl, map); - } - else - return null; - } - */ return lbl; } - /// - /// Very basic test to check for positive direction of Linestring - /// - /// The linestring to test - /// Value indicating whether labels are to be printed right to left - /// The positively directed linestring - private static ILineString PositiveLineString(ILineString line, bool isRightToLeft) + private static BaseLabel CreatePathLabel(ILineString line, string text, SizeF textMeasure, + int priority, LabelStyle style, MapViewport map, Graphics g, GeometryFactory factory) { - var s = line.StartPoint; - var e = line.EndPoint; + if (line == null) + return null; - var dx = e.X - s.X; - if (isRightToLeft && dx < 0) - return line; - - if (!isRightToLeft && dx >= 0) - return line; + var labelLength = textMeasure.Width * map.PixelWidth; + var labelHeight = textMeasure.Height * map.PixelHeight; - var revCoord = new Stack(line.Coordinates); + var offsetX = style.Offset.X * map.PixelWidth; // positive = increasing measure + var offsetY = style.Offset.Y * map.PixelHeight; // positive = right side of line - return line.Factory.CreateLineString(revCoord.ToArray()); - } + var start = 0d; + if (style.HorizontalAlignment == LabelStyle.HorizontalAlignmentEnum.Center) + start = line.Length * 0.5 - labelLength * 0.5; + else if (style.HorizontalAlignment == LabelStyle.HorizontalAlignmentEnum.Right) + start = line.Length - labelLength; - //private static void WarpedLabel(MultiLineString line, ref BaseLabel baseLabel, Map map) - //{ - // var path = MultiLineStringToPath(line, map, true); + start += offsetX; - // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); - // baseLabel = pathLabel; - //} - - //private static void WarpedLabel(LineString line, ref BaseLabel baseLabel, Map map) - //{ - - // var path = LineStringToPath(line, map, false); + // Constrain label length + if (labelLength > 0.95 * line.Length && !style.IgnoreLength || + start + labelLength < 0 || start > line.Length) + return null; - // var pathLabel = new PathLabel(baseLabel.Text, path, 0f, baseLabel.Priority, new LabelBox(path.GetBounds()), baseLabel.Style); - // baseLabel = pathLabel; - //} + // LengthIndexedLine idea courtesy FObermaier + NetTopologySuite.LinearReferencing.LengthIndexedLine lil; + // optimize for detailed lines (eg labelling rivers at continental level) + // ratio and NumPoints based on instinct.... feel free to revise + var mid = start + labelLength / 2.0; + if (labelLength / line.Length < 0.5 && line.NumPoints > 200 && mid >= 0 && mid < line.Length) + { + lil = new NetTopologySuite.LinearReferencing.LengthIndexedLine(line); + var midPt = lil.ExtractPoint(mid); + // extract slightly more than label length to ensure offsetCurve follows line geometry + var halfLen = labelLength * 0.6; + // ensure non-negative indexes constrained to line length (due to special LengthIndexLine functionality) + line = (LineString) lil.ExtractLine(Math.Max(0, mid - halfLen), Math.Min(mid + halfLen, line.Length)); + // reset start + lil = new NetTopologySuite.LinearReferencing.LengthIndexedLine(line); + mid = lil.IndexOf(midPt); + start = mid - labelLength / 2.0; + } - /// - /// Function to transform a linestring to a graphics path for further processing - /// - /// The Linestring - /// The map - /// - /// A GraphicsPath - public static GraphicsPath LineStringToPath(ILineString lineString, MapViewport map/*, bool useClipping*/) - { - var gp = new GraphicsPath(FillMode.Alternate); - //if (!useClipping) - gp.AddLines(lineString.TransformToImage(map)); - //else - //{ - // var bb = map.Envelope; - // var cohenSutherlandLineClipping = new CohenSutherlandLineClipping(bb.Left, bb.Bottom, bb.Right, bb.Top); - // var clippedLineStrings = cohenSutherlandLineClipping.ClipLineString(lineString); - // foreach (var clippedLineString in clippedLineStrings.LineStrings) - // { - // var s = clippedLineString.StartPoint; - // var e = clippedLineString.EndPoint; - - // var dx = e.X - s.X; - // //var dy = e.Y - s.Y; + // basic extend + var end = start + labelLength; + if (start < 0 || end > line.Length) + { + line = ExtendLine(line, + start < 0 ? -1 * start : 0, + end > line.Length ? end - line.Length : 0); + start = 0; + end = start + labelLength; + } - // LineString revcls = null; - // if (dx < 0) - // revcls = ReverseLineString(clippedLineString); - - // gp.StartFigure(); - // gp.AddLines(revcls == null ? clippedLineString.TransformToImage(map) : revcls.TransformToImage(map)); - // } - //} - return gp; - } + lil = new NetTopologySuite.LinearReferencing.LengthIndexedLine(line); + // reverse + var startPt = lil.ExtractPoint(start); + var endPt = lil.ExtractPoint(end); + if (LineNeedsReversing(startPt, endPt, false, map)) + { + start = end; + end = start - labelLength; + } + line = (ILineString) lil.ExtractLine(start, end); - //private static LineString ReverseLineString(LineString clippedLineString) - //{ - // var coords = new Stack(clippedLineString.Vertices); - // return new LineString(coords.ToArray()); - //} - - ///// - ///// Function to transform a linestring to a graphics path for further processing - ///// - ///// The Linestring - ///// The map - ///// A value indicating whether clipping should be applied or not - ///// A GraphicsPath - //public static GraphicsPath MultiLineStringToPath(MultiLineString multiLineString, Map map, bool useClipping) - //{ - // var gp = new GraphicsPath(FillMode.Alternate); - // foreach (var lineString in multiLineString.LineStrings) - // gp.AddPath(LineStringToPath(lineString, map, useClipping), false); - - // return gp; - //} - - //private static GraphicsPath LineToGraphicsPath(LineString line, Map map) - //{ - // GraphicsPath path = new GraphicsPath(); - // path.AddLines(line.TransformToImage(map)); - // return path; - //} - - private static void CalculateLabelOnLinestring(ILineString line, ref BaseLabel baseLabel, Map map) - { - double dx, dy; - var label = baseLabel as Label; + // Build offset curve + ILineString offsetCurve; + var bufferParameters = + new NetTopologySuite.Operation.Buffer.BufferParameters(4, + GeoAPI.Operation.Buffer.EndCapStyle.Flat); - // first find the middle segment of the line - var vertices = line.Coordinates; - int midPoint = (vertices.Length - 1)/2; - if (vertices.Length > 2) + // determine offset curve that will run through the vertical centre of the text + if (style.VerticalAlignment != LabelStyle.VerticalAlignmentEnum.Middle) { - dx = vertices[midPoint + 1].X - vertices[midPoint].X; - dy = vertices[midPoint + 1].Y - vertices[midPoint].Y; + var ocb = new NetTopologySuite.Operation.Buffer.OffsetCurveBuilder(factory.PrecisionModel, + bufferParameters); + + // Left side positive + var offsetCurvePoints = ocb.GetOffsetCurve(line.Coordinates, + ((int) style.VerticalAlignment - 1) * 0.5 * labelHeight - offsetY); + offsetCurve = factory.CreateLineString(offsetCurvePoints); } else { - midPoint = 0; - dx = vertices[1].X - vertices[0].X; - dy = vertices[1].Y - vertices[0].Y; + offsetCurve = line; } - if (dy == 0) - label.Rotation = 0; - else if (dx == 0) - label.Rotation = 90; - else + + // basic extend + var ratio = labelLength / offsetCurve.Length; + if (ratio > 1.01) { - // calculate angle of line - double angle = -Math.Atan(dy/dx) + Math.PI*0.5; - angle *= (180d/Math.PI); // convert radians to degrees - label.Rotation = (float) angle - 90; // -90 text orientation + var diff = labelLength - offsetCurve.Length; + offsetCurve = ExtendLine(offsetCurve, diff / 2d, diff / 2d); } - var tmpx = vertices[midPoint].X + (dx*0.5); - var tmpy = vertices[midPoint].Y + (dy*0.5); - label.Location = map.WorldToImage(new Coordinate(tmpx, tmpy)); + + // enclosing polygon in world coords + var affectedArea = (IPolygon) offsetCurve.Buffer(0.5d * labelHeight, bufferParameters); + + // fast, basic check (technically should use polygons for rotated views) + if (!map.Envelope.Contains(affectedArea.EnvelopeInternal)) + return null; + + // using labelBox to pass text height to WarpedPath + return new PathLabel(text, LineStringToPath(offsetCurve, map), 0, priority, + new LabelBox(0,0,textMeasure.Width,textMeasure.Height), style) + { + AffectedArea = affectedArea + }; } - private static void CalculateLabelAroundOnLineString(ILineString line, ref BaseLabel label, MapViewport map, System.Drawing.Graphics g, System.Drawing.SizeF textSize) + private static ILineString ExtendLine(ILineString line, double startDist, double endDist) { - var sPoints = line.Coordinates; + var numPts = (startDist > 0 ? 1 : 0) + (endDist > 0 ? 1 : 0); + if (numPts == 0) return line; - // only get point in enverlop of map - var colPoint = new Collection(); - var bCheckStarted = false; - //var testEnvelope = map.Envelope.Grow(map.PixelSize*10); - for (var j = 0; j < sPoints.Length; j++) + var cs = line.Factory.CoordinateSequenceFactory.Create(line.CoordinateSequence.Count + numPts, line.CoordinateSequence.Dimension); + var offset = 0; + + if (startDist > 0) { - if (map.Envelope.Contains(sPoints[j])) - { - //points[j] = map.WorldToImage(sPoints[j]); - colPoint.Add(map.WorldToImage(sPoints[j])); - bCheckStarted = true; - } - else if (bCheckStarted) + var rad = Azimuth(line.Coordinates[1], line.Coordinates[0]); + var coords = new[] { - // fix bug curved line out of map in center segment of line - break; - } + Traverse(line.Coordinates[0], rad, startDist) + }; + var startSeq = line.Factory.CoordinateSequenceFactory.Create(coords); + CoordinateSequences.Copy(startSeq, 0, cs, offset++, 1); } - if (colPoint.Count > 1) + CoordinateSequences.Copy(line.CoordinateSequence, 0, cs, offset, line.CoordinateSequence.Count); + offset += line.CoordinateSequence.Count; + + if (endDist > 0) { - label.TextOnPathLabel = new TextOnPath(); - switch (label.Style.HorizontalAlignment) + var endPoints = new [] { - case LabelStyle.HorizontalAlignmentEnum.Left: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Left; - break; - case LabelStyle.HorizontalAlignmentEnum.Right: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Right; - break; - case LabelStyle.HorizontalAlignmentEnum.Center: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; - break; - default: - label.TextOnPathLabel.TextPathAlignTop = TextPathAlign.Center; - break; - } - switch (label.Style.VerticalAlignment) + line.Coordinates[line.Coordinates.Length - 2], + line.Coordinates[line.Coordinates.Length - 1] + }; + + var rad = Azimuth(endPoints[0], endPoints[1]); + var coords = new[] { - case LabelStyle.VerticalAlignmentEnum.Bottom: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.UnderPath; - break; - case LabelStyle.VerticalAlignmentEnum.Top: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.OverPath; - break; - case LabelStyle.VerticalAlignmentEnum.Middle: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; - break; - default: - label.TextOnPathLabel.TextPathPathPosition = TextPathPosition.CenterPath; - break; - } + Traverse(endPoints[1], rad, endDist) + }; + var es = line.Factory.CoordinateSequenceFactory.Create(coords); + CoordinateSequences.Copy(es, 0, cs, offset, 1); + } + + return line.Factory.CreateLineString(cs); + } + + private static double Azimuth( Coordinate c1, Coordinate c2) + { + var dX = c2.X - c1.X; + var dY = c2.Y - c1.Y; + return Math.PI / 2 - Math.Atan2(dY, dX); + } + + private static Coordinate Traverse(Coordinate coord, double azimuth, double dist) + { + return new Coordinate( + coord.X + dist * Math.Sin(azimuth), + coord.Y + dist * Math.Cos(azimuth) + ); + } + private IGeometry ClipLinealGeomToViewExtents(MapViewport map, IGeometry geom) + { + if (map.MapTransform.IsIdentity) + { + var lineClipping = new CohenSutherlandLineClipping(map.Envelope.MinX, map.Envelope.MinY, + map.Envelope.MaxX, map.Envelope.MaxY); - var idxStartPath = 0; - var numberPoint = colPoint.Count; - // start Optimzes Path points + if (geom is ILineString) + return lineClipping.ClipLineString(geom as ILineString); - var step = 100; - if (colPoint.Count >= step * 2) - { - numberPoint = step * 2; ; - switch (label.Style.HorizontalAlignment) - { - case LabelStyle.HorizontalAlignmentEnum.Left: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Left; - idxStartPath = 0; - break; - case LabelStyle.HorizontalAlignmentEnum.Right: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Right; - idxStartPath = colPoint.Count - step; - break; - case LabelStyle.HorizontalAlignmentEnum.Center: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; - idxStartPath = (int)colPoint.Count / 2 - step; - break; - default: - //label.TextOnPathLabel.TextPathAlignTop = SharpMap.Rendering.TextPathAlign.Center; - idxStartPath = (int)colPoint.Count / 2 - step; - break; - } - } - // end optimize path point - var points = new PointF[numberPoint]; - var count = 0; - if (colPoint[0].X <= colPoint[colPoint.Count - 1].X) - { - for (var l = idxStartPath; l < numberPoint + idxStartPath; l++) - { - points[count] = colPoint[l]; - count++; - } - } - else - { - //reverse the path - for (var k = numberPoint - 1 + idxStartPath; k >= idxStartPath; k--) + if (geom is IMultiLineString) + return lineClipping.ClipLineString(geom as IMultiLineString); + } + else + { + var clipPolygon = new Polygon(new LinearRing(new[] { - points[count] = colPoint[k]; - count++; + new Coordinate(map.Center.X - map.Zoom * .5, map.Center.Y - map.MapHeight * .5), + new Coordinate(map.Center.X - map.Zoom * .5, map.Center.Y + map.MapHeight * .5), + new Coordinate(map.Center.X + map.Zoom * .5, map.Center.Y + map.MapHeight * .5), + new Coordinate(map.Center.X + map.Zoom * .5, map.Center.Y - map.MapHeight * .5), + new Coordinate(map.Center.X - map.Zoom * .5, map.Center.Y - map.MapHeight * .5) } - } - /* - //get text size in page units ie pixels - float textheight = label.Style.Font.Size; - switch (label.Style.Font.Unit) - { - case GraphicsUnit.Display: - textheight = textheight * g.DpiY / 75; - break; - case GraphicsUnit.Document: - textheight = textheight * g.DpiY / 300; - break; - case GraphicsUnit.Inch: - textheight = textheight * g.DpiY; - break; - case GraphicsUnit.Millimeter: - textheight = (float)(textheight / 25.4 * g.DpiY); - break; - case GraphicsUnit.Pixel: - //do nothing - break; - case GraphicsUnit.Point: - textheight = textheight * g.DpiY / 72; - break; - } - var topFont = new Font(label.Style.Font.FontFamily, textheight, label.Style.Font.Style, GraphicsUnit.Pixel); - */ - var topFont = label.Style.GetFontForGraphics(g); - // - var path = new GraphicsPath(); - path.AddLines(points); - - label.TextOnPathLabel.PathColorTop = System.Drawing.Color.Transparent; - label.TextOnPathLabel.Text = label.Text; - label.TextOnPathLabel.LetterSpacePercentage = 90; - label.TextOnPathLabel.FillColorTop = new System.Drawing.SolidBrush(label.Style.ForeColor); - label.TextOnPathLabel.Font = topFont; - label.TextOnPathLabel.PathDataTop = path.PathData; - label.TextOnPathLabel.Graphics = g; - //label.TextOnPathLabel.ShowPath=true; - //label.TextOnPathLabel.PathColorTop = System.Drawing.Color.YellowGreen; - if (label.Style.Halo != null) - { - label.TextOnPathLabel.ColorHalo = label.Style.Halo; - } - else - { - label.TextOnPathLabel.ColorHalo = null;// new System.Drawing.Pen(label.Style.ForeColor, (float)0.5); - } - path.Dispose(); + )); - // MeasureString to get region - label.TextOnPathLabel.MeasureString = true; - label.TextOnPathLabel.DrawTextOnPath(); - label.TextOnPathLabel.MeasureString = false; - // Get Region label for CollissionDetection here. - var pathRegion = new GraphicsPath(); + var at = AffineTransformation.RotationInstance( + Degrees.ToRadians(map.MapTransformRotation), map.Center.X, map.Center.Y); - if (label.TextOnPathLabel.RegionList.Count > 0) - { - //int idxCenter = (int)label.TextOnPathLabel.PointsText.Count / 2; - //System.Drawing.Drawing2D.Matrix rotationMatrix = g.Transform.Clone();// new Matrix(); - //rotationMatrix.RotateAt(label.TextOnPathLabel.Angles[idxCenter], label.TextOnPathLabel.PointsText[idxCenter]); - //if (label.TextOnPathLabel.PointsTextUp.Count > 0) - //{ - // for (int up = label.TextOnPathLabel.PointsTextUp.Count - 1; up >= 0; up--) - // { - // label.TextOnPathLabel.PointsText.Add(label.TextOnPathLabel.PointsTextUp[up]); - // } - - //} - pathRegion.AddRectangles(label.TextOnPathLabel.RegionList.ToArray()); - - // get box for detect colission here - label.Box = new LabelBox(pathRegion.GetBounds()); - //g.FillRectangle(System.Drawing.Brushes.YellowGreen, label.Box); - } - pathRegion.Dispose(); + clipPolygon = (Polygon) at.Transform(clipPolygon); + + if (geom is ILineString) + return clipPolygon.Intersection(geom as ILineString); + + if (geom is IMultiLineString) + return clipPolygon.Intersection(geom as IMultiLineString); } + return null; + } + + /// + /// Very basic test to check for positive direction of Linestring + /// + /// The linestring to test + /// Value indicating whether labels are to be printed right to left + /// The positively directed linestring + private static ILineString PositiveLineString(ILineString line, bool isRightToLeft) + { + var s = line.StartPoint; + var e = line.EndPoint; + + var dx = e.X - s.X; + if (isRightToLeft && dx < 0) + return line; + + if (!isRightToLeft && dx >= 0) + return line; + + var revCoord = new Stack(line.Coordinates); + + return line.Factory.CreateLineString(revCoord.ToArray()); + } + + /// + /// Very basic test to check for positive direction of Linestring, taking into account map rotation + /// + /// start of text + /// end of text + /// + /// + /// + private static bool LineNeedsReversing(Coordinate start, Coordinate end, bool isRightToLeft, MapViewport map) + { + double startX, endX; + if (map.MapTransform.IsIdentity) + { + startX = start.X; + endX = end.X; + } + else + { + var pts = map.WorldToImage(new[] {start, end}, true); + startX = pts[0].X; + endX = pts[1].X; + } + + var dx = endX - startX; + if (isRightToLeft && dx < 0) + return false; + + return isRightToLeft || !(dx >= 0); + } + + /// + /// Function to transform a linestring to a graphics path for further processing + /// + /// The Linestring + /// The map + /// + /// A GraphicsPath + public static GraphicsPath LineStringToPath(ILineString lineString, MapViewport map/*, bool useClipping*/) + { + var gp = new GraphicsPath(FillMode.Alternate); + gp.AddLines(lineString.TransformToImage(map)); + return gp; } } } diff --git a/SharpMap/Rendering/Label.cs b/SharpMap/Rendering/Label.cs index 0e0ca4e6..f9a17a15 100644 --- a/SharpMap/Rendering/Label.cs +++ b/SharpMap/Rendering/Label.cs @@ -1,453 +1,460 @@ -// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk) -// -// This file is part of SharpMap. -// SharpMap is free software; you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// SharpMap is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. - -// You should have received a copy of the GNU Lesser General Public License -// along with SharpMap; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using SharpMap.Styles; - -namespace SharpMap.Rendering -{ - /// - /// Defines an axis-aligned box around a label, used for collision detection - /// - public class LabelBox : IComparable - { - private float _height; - private float _left; - private float _top; - private float _width; - - /// - /// Initializes a new LabelBox instance - /// - /// Left side of box - /// Top of box - /// Width of the box - /// Height of the box - public LabelBox(float left, float top, float width, float height) - { - _left = left; - _top = top; - _width = width; - _height = height; - } - - /// - /// Initializes a new LabelBox instance based on a rectangle - /// - /// - public LabelBox(RectangleF rectangle) - { - _left = rectangle.X; - _top = rectangle.Y; - _width = rectangle.Width; - _height = rectangle.Height; - } - - /// - /// The Left tie-point for the Label - /// - public float Left - { - get { return _left; } - set { _left = value; } - } - - /// - /// The Top tie-point for the label - /// - public float Top - { - get { return _top; } - set { _top = value; } - } - - /// - /// Width of the box - /// - public float Width - { - get { return _width; } - set { _width = value; } - } - - /// - /// Height of the box - /// - public float Height - { - get { return _height; } - set { _height = value; } - } - - /// - /// Right side of the box - /// - public float Right - { - get { return _left + _width; } - } - - /// - /// Bottom of the box - /// - public float Bottom - { - get { return _top - _height; } - } - - #region IComparable Members - - /// - /// Returns 0 if the boxes intersects each other - /// - /// labelbox to perform intersectiontest with - /// 0 if the intersect - public int CompareTo(LabelBox other) - { - if (Intersects(other)) - return 0; - else if (other.Left > Right || - other.Bottom > Top) - return 1; - else - return -1; - } - - #endregion - - /// - /// Determines whether the boundingbox intersects another boundingbox - /// - /// - /// - public bool Intersects(LabelBox box) - { - return !(box.Left > Right || - box.Right < Left || - box.Bottom > Top || - box.Top < Bottom); - } - } - - /// - /// Class for storing a label instance - /// - public abstract class BaseLabel : IComparable, IComparer - { - private LabelBox _box; - private Font _Font; - //private PointF _LabelPoint; - private int _Priority; - private float _Rotation; - private bool _show; - private LabelStyle _Style; - - private string _Text; - private TextOnPath _textOnPath = null; - /// - /// Render text on path - /// - public TextOnPath TextOnPathLabel - { - get { return _textOnPath; } - set { _textOnPath = value; } - } - /// - /// Initializes a new Label instance - /// - /// Text to write - /// Rotation - /// Label priority used for collision detection - /// Box around label for collision detection - /// The style of the label - protected BaseLabel(string text, float rotation, int priority, LabelBox collisionbox, - LabelStyle style) - { - _Text = text; - //_LabelPoint = labelpoint; - _Rotation = rotation; - _Priority = priority; - _box = collisionbox; - _Style = style; - _show = true; - } - /// - /// Initializes a new Label instance - /// - /// - /// - /// - /// - protected BaseLabel(string text, float rotation, int priority, - LabelStyle style) - { - _Text = text; - //_LabelPoint = labelpoint; - _Rotation = rotation; - _Priority = priority; - _Style = style; - _show = true; - } - - /// - /// Show this label or don't - /// - public bool Show - { - get { return _show; } - set { _show = value; } - } - - /// - /// The text of the label - /// - public string Text - { - get { return _Text; } - set { _Text = value; } - } - - /// - /// Label font - /// - public Font Font - { - get { return _Font; } - set { _Font = value; } - } - - /// - /// Label rotation - /// - public float Rotation - { - get { return _Rotation; } - set { _Rotation = value; } - } - - /// - /// Value indicating rendering priority - /// - public int Priority - { - get { return _Priority; } - set { _Priority = value; } - } - - /// - /// Label box - /// - public LabelBox Box - { - get { return _box; } - set { _box = value; } - } - - /// - /// Gets or sets the of this label - /// - public LabelStyle Style - { - get { return _Style; } - set { _Style = value; } - } - - #region IComparable